Files
hao-ai-lab-FastVideo/v2/models/speculative/loop.py
T
SolitaryThinker 4e331eb7ff [refactor] v2: use absolute imports (v2.*) everywhere instead of relative
Mechanical conversion of every relative import under v2/ to an absolute v2.* path
(from .x / ..x / ...x -> from v2.<pkg>.x) so imports are unambiguous, grep-able, and
stable when code is copied/moved between entrypoints (VideoGenerator, CLI, server).

Surgical prefix-only rewrite: only the 'from <dots><module>' prefix changed — import
names, parentheses, multi-line formatting, comments, and ordering are byte-for-byte
preserved (no collapsing, no reorder, no unrelated reformatting).

- 471 imports across 125 files; v2/tests/ was already absolute (untouched).
- Validated: all 125 files compile, every 'from v2.* import' target resolves to a real
  module/package, zero relative imports remain (full sweep), CPU mini suite green
  (202 passed, 2 skipped).
2026-07-04 17:15:34 +00:00

110 lines
5.6 KiB
Python

"""SpeculativeARLoop — draft-verify (speculative) decoding (design_v3 §2.2; §9.16).
The AR-side analog of content-adaptive compute (§9.12): a cheap **draft** model proposes K tokens, the
**target** model verifies them, and the loop accepts the longest matching prefix plus one target
correction — a *variable* number of tokens per round (a ragged loop the model owns). Two properties:
* **Exact.** Every accepted token is one the target would have produced greedily, and the correction
is the target's own token — so the emitted sequence is *identical* to the target's standalone greedy
decode. The speedup is free (same output, fewer sequential target steps).
* **Latency win.** The target's per-round forwards are one batched verify step (here counted as one
``verify_round``); standalone decoding would take one *sequential* target step per token. So
``verify_rounds`` (the expensive model's latency steps) is well below the token count.
It binds TWO components (``draft`` + ``target``) on one resident instance — two models co-scheduled in
one request — and every round is an ``AR_TOKEN`` WorkUnit.
"""
from __future__ import annotations
from v2._enums import ExecutionProfile, WorkUnitKind
from v2.loop.contracts import (
Done,
LoopResult,
LoopState,
ResourceRequest,
ShapeSignature,
StepResult,
WorkPlan,
)
from v2.request.streams import StreamChunk
class SpeculativeARLoop:
def __init__(self, *, loop_id, draft_id="draft", target_id="target", cost, spec_len=4, max_tokens=12):
self.loop_id = loop_id
self.draft_id = draft_id
self.target_id = target_id
self.cost = cost
self.spec_len = spec_len # K: candidate tokens drafted per round
self.max_tokens = max_tokens
def init(self, req, model, ctx) -> LoopState:
seed = req.sampling.seed if req.sampling.seed is not None else 0
st = LoopState(loop_id=self.loop_id, instance_id=model.card.model_id,
request_id=req.request_id, profile=ctx.profile, seed=seed)
st.scratch["tokens"] = list(ctx.slots.get("prompt_tokens") or [1]) # prefill
st.scratch["generated"] = []
st.scratch["max_tokens"] = min(req.sampling.max_tokens, self.max_tokens)
st.scratch.update(verify_rounds=0, target_forwards=0, draft_forwards=0, accepted=0, drafted=0)
return st
def next(self, st: LoopState):
if len(st.scratch["generated"]) >= st.scratch["max_tokens"]:
return Done()
ctx_tokens = list(st.scratch["tokens"])
k, did, tid = self.spec_len, self.draft_id, self.target_id
def run(model, override=None):
draft, target = model.component(did), model.component(tid)
# 1) DRAFT proposes k candidate tokens autoregressively (cheap)
cand, c = [], list(ctx_tokens)
for _ in range(k):
t = int(draft.ar_forward(c)); cand.append(t); c.append(t)
# 2) TARGET verifies (one batched step): accept the matching prefix, then 1 correction
accepted, tf = [], 0
for j in range(k):
tj = int(target.ar_forward(ctx_tokens + cand[:j])); tf += 1
if tj == cand[j]:
accepted.append(cand[j]) # draft agreed with target → accept (free)
else:
accepted.append(tj) # mismatch → take the target's token, stop
break
else:
accepted.append(int(target.ar_forward(ctx_tokens + cand))); tf += 1 # all k accepted + 1
return StepResult(output={"accepted": accepted, "target_forwards": tf, "drafted": k})
pos = len(st.scratch["generated"])
emits = [StreamChunk(stream_id=st.request_id, modality="text", seq=pos, preview=True)]
return WorkPlan(
loop_id=self.loop_id, instance_id=st.instance_id, kind=WorkUnitKind.AR_TOKEN,
shape_sig=ShapeSignature(WorkUnitKind.AR_TOKEN, dims=(len(ctx_tokens), k)),
resources=ResourceRequest(compute_seconds=self.cost.predict(len(ctx_tokens) + k),
resident_bytes=8 * len(ctx_tokens), peak_activation_bytes=8 * k),
payload={"round": st.scratch["verify_rounds"]}, run=run, emits=emits,
label=f"spec.round{st.scratch['verify_rounds']}")
def advance(self, st: LoopState, result: StepResult) -> LoopState:
room = st.scratch["max_tokens"] - len(st.scratch["generated"])
accepted = list(result.output["accepted"])[:room] # don't overshoot max_tokens
st.scratch["tokens"].extend(accepted)
st.scratch["generated"].extend(accepted)
st.scratch["verify_rounds"] += 1
st.scratch["target_forwards"] += int(result.output["target_forwards"])
st.scratch["drafted"] += int(result.output["drafted"])
st.scratch["accepted"] += len(accepted)
st.step_idx += 1
if st.profile == ExecutionProfile.ROLLOUT:
st.trajectory.append({"round": st.step_idx, "accepted": len(accepted)})
return st
def finalize(self, st: LoopState) -> LoopResult:
toks = st.scratch["generated"]
rounds = max(1, st.scratch["verify_rounds"])
return LoopResult(
outputs={"tokens": toks, "text": "tok:" + ",".join(map(str, toks))},
metrics={"tokens": float(len(toks)), "verify_rounds": float(st.scratch["verify_rounds"]),
"target_forwards": float(st.scratch["target_forwards"]),
"draft_forwards": float(st.scratch["drafted"]),
"tokens_per_round": float(len(toks)) / rounds},
behavior=st.trajectory or None)