flashrt-structures 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flashrt_structures/__init__.py +174 -0
- flashrt_structures/adapters/__init__.py +55 -0
- flashrt_structures/adapters/diffusers_attention.py +237 -0
- flashrt_structures/adapters/diffusers_rotary_attention.py +239 -0
- flashrt_structures/adapters/factored_qk_norm_rope.py +252 -0
- flashrt_structures/adapters/factored_two_way_attention.py +99 -0
- flashrt_structures/adapters/gemma_attention.py +226 -0
- flashrt_structures/adapters/packed_qkv_rope.py +342 -0
- flashrt_structures/adapters/packed_stream_qk_norm_rope.py +376 -0
- flashrt_structures/adapters/qwen_per_head_qk_norm_rope.py +231 -0
- flashrt_structures/adapters/sglang_engine.py +207 -0
- flashrt_structures/adapters/transformers_attention_interface.py +73 -0
- flashrt_structures/adapters/transformers_gated_delta.py +152 -0
- flashrt_structures/adapters/transformers_gated_delta_fused.py +96 -0
- flashrt_structures/adapters/vllm_engine.py +424 -0
- flashrt_structures/adjudicate.py +85 -0
- flashrt_structures/aot.py +191 -0
- flashrt_structures/autobuild.py +2052 -0
- flashrt_structures/beta/__init__.py +43 -0
- flashrt_structures/beta/conform.py +94 -0
- flashrt_structures/beta/joins.py +113 -0
- flashrt_structures/beta/negotiate.py +84 -0
- flashrt_structures/beta/ports.py +140 -0
- flashrt_structures/decisions.py +80 -0
- flashrt_structures/discover.py +623 -0
- flashrt_structures/explain.py +70 -0
- flashrt_structures/frontdoor.py +572 -0
- flashrt_structures/gates.py +465 -0
- flashrt_structures/guard.py +421 -0
- flashrt_structures/handle.py +189 -0
- flashrt_structures/impls/__init__.py +219 -0
- flashrt_structures/impls/adaln_producer/__init__.py +8 -0
- flashrt_structures/impls/adaln_producer/broker.py +116 -0
- flashrt_structures/impls/adaln_producer/fused.py +388 -0
- flashrt_structures/impls/adarms_stack/__init__.py +8 -0
- flashrt_structures/impls/adarms_stack/fp8_chain.py +832 -0
- flashrt_structures/impls/adarms_stack/region.py +102 -0
- flashrt_structures/impls/attention_core/__init__.py +132 -0
- flashrt_structures/impls/attention_core/fa2_seqused.py +458 -0
- flashrt_structures/impls/attention_core/fa4_cute.py +154 -0
- flashrt_structures/impls/attention_core/fa4_fp8.py +178 -0
- flashrt_structures/impls/attention_core/masked_mha.py +158 -0
- flashrt_structures/impls/attention_core/two_way_fa2.py +220 -0
- flashrt_structures/impls/cadence_static/__init__.py +18 -0
- flashrt_structures/impls/cadence_static/buffers.py +122 -0
- flashrt_structures/impls/cadence_static/cross_attention.py +187 -0
- flashrt_structures/impls/chain_elements.py +89 -0
- flashrt_structures/impls/decode_loop/__init__.py +0 -0
- flashrt_structures/impls/decode_loop/fp8_kv.py +206 -0
- flashrt_structures/impls/decode_loop/mtp_speculative.py +245 -0
- flashrt_structures/impls/decode_loop/whole_step.py +852 -0
- flashrt_structures/impls/decoder_block/__init__.py +6 -0
- flashrt_structures/impls/decoder_block/attn_sublayer.py +110 -0
- flashrt_structures/impls/decoder_block/fused.py +167 -0
- flashrt_structures/impls/decoder_ffn/__init__.py +0 -0
- flashrt_structures/impls/decoder_ffn/fp8_static.py +310 -0
- flashrt_structures/impls/decoder_ffn/fp8_static.yaml +22 -0
- flashrt_structures/impls/decoder_ffn/w4a16_static.py +221 -0
- flashrt_structures/impls/decoder_ffn/w8a16_static.py +183 -0
- flashrt_structures/impls/dit_stack/__init__.py +11 -0
- flashrt_structures/impls/dit_stack/fp4_chain.py +417 -0
- flashrt_structures/impls/dit_stack/region.py +86 -0
- flashrt_structures/impls/fixed_iter/__init__.py +29 -0
- flashrt_structures/impls/fixed_iter/openpi.py +264 -0
- flashrt_structures/impls/fixed_iter/protocol.py +94 -0
- flashrt_structures/impls/gated_delta_core/__init__.py +3 -0
- flashrt_structures/impls/gated_delta_core/fused_layer.py +545 -0
- flashrt_structures/impls/gated_delta_core/hub_v3.py +152 -0
- flashrt_structures/impls/graph_lowering/__init__.py +27 -0
- flashrt_structures/impls/graph_lowering/pi052_denoise.py +179 -0
- flashrt_structures/impls/graph_lowering/protocol.py +76 -0
- flashrt_structures/impls/graph_lowering/qwen3_vl.py +364 -0
- flashrt_structures/impls/linear_proj/__init__.py +0 -0
- flashrt_structures/impls/linear_proj/fp8_static.py +270 -0
- flashrt_structures/impls/linear_proj/nvfp4_balance.py +131 -0
- flashrt_structures/impls/linear_proj/nvfp4_dynamic.py +182 -0
- flashrt_structures/impls/linear_proj/w8a16_static.py +230 -0
- flashrt_structures/impls/modnorm_qkv_chain/__init__.py +0 -0
- flashrt_structures/impls/modnorm_qkv_chain/fp8_ptok_table.py +291 -0
- flashrt_structures/impls/moe_experts/__init__.py +9 -0
- flashrt_structures/impls/moe_experts/nvfp4_dynamic.py +208 -0
- flashrt_structures/impls/moe_experts/nvfp4_w4a16.py +129 -0
- flashrt_structures/impls/norm_fused/__init__.py +3 -0
- flashrt_structures/impls/norm_fused/bf16.py +94 -0
- flashrt_structures/impls/norm_fused/fp8_producer.py +84 -0
- flashrt_structures/impls/patch_projection/__init__.py +3 -0
- flashrt_structures/impls/patch_projection/bf16_flat.py +139 -0
- flashrt_structures/impls/prefill_tower/__init__.py +10 -0
- flashrt_structures/impls/prefill_tower/fp8_chain.py +955 -0
- flashrt_structures/impls/prefill_tower/region.py +99 -0
- flashrt_structures/impls/qk_norm_rope/__init__.py +12 -0
- flashrt_structures/impls/qk_norm_rope/per_head_gqa.py +199 -0
- flashrt_structures/impls/qk_norm_rope/projection_bf16.py +165 -0
- flashrt_structures/impls/qkv_pack/__init__.py +5 -0
- flashrt_structures/impls/qkv_pack/bf16.py +110 -0
- flashrt_structures/impls/qkv_pack/fp8_static.py +435 -0
- flashrt_structures/impls/qkv_pack/nvfp4_balance.py +218 -0
- flashrt_structures/impls/qkv_rope/__init__.py +3 -0
- flashrt_structures/impls/qkv_rope/packed_bias_bf16.py +143 -0
- flashrt_structures/impls/step_table.py +113 -0
- flashrt_structures/impls/vision_ffn/__init__.py +0 -0
- flashrt_structures/impls/vision_ffn/fp8_static.py +261 -0
- flashrt_structures/impls/vision_ffn/nvfp4_balance.py +211 -0
- flashrt_structures/impls/vision_tower/__init__.py +7 -0
- flashrt_structures/impls/vision_tower/fp8_chain.py +533 -0
- flashrt_structures/impls/vision_tower/region.py +91 -0
- flashrt_structures/matrix.py +126 -0
- flashrt_structures/points.py +368 -0
- flashrt_structures/prequantized.py +131 -0
- flashrt_structures/quantize_on_adopt.py +94 -0
- flashrt_structures/recipe.py +438 -0
- flashrt_structures/regions.py +208 -0
- flashrt_structures/schemes.py +490 -0
- flashrt_structures/stages.py +298 -0
- flashrt_structures/storage.py +255 -0
- flashrt_structures/swap.py +398 -0
- flashrt_structures/workspace.py +110 -0
- flashrt_structures-0.2.0.dist-info/METADATA +140 -0
- flashrt_structures-0.2.0.dist-info/RECORD +122 -0
- flashrt_structures-0.2.0.dist-info/WHEEL +5 -0
- flashrt_structures-0.2.0.dist-info/licenses/LICENSE +202 -0
- flashrt_structures-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
"""decode_loop — the whole-loop serving form for cached LLM decode.
|
|
2
|
+
|
|
3
|
+
Seam swaps accelerate the pieces; this family owns the loop. The host's
|
|
4
|
+
``generate`` walks Python glue between every token, repoints its KV
|
|
5
|
+
cache with ``torch.cat``, and rebuilds masks per step — none of which
|
|
6
|
+
can replay inside a CUDA graph. The whole-step form drives the decoder
|
|
7
|
+
layers directly with static buffers:
|
|
8
|
+
|
|
9
|
+
- a duck-typed static hybrid cache (preallocated KV written by
|
|
10
|
+
``index_copy_`` at an in-graph position buffer; recurrent/conv slots
|
|
11
|
+
as the layers expect them) — no host cache class is imported, only
|
|
12
|
+
the surface the layers actually touch is implemented;
|
|
13
|
+
- the causal row itself masks the padded tail of the static KV, so the
|
|
14
|
+
decode mask is one ``index_select`` on the position — no mask
|
|
15
|
+
bookkeeping;
|
|
16
|
+
- argmax and the position increment run in-graph, and the step is
|
|
17
|
+
optionally wrapped in ``torch.compile`` so the elementwise regions
|
|
18
|
+
between custom kernels fuse before capture.
|
|
19
|
+
|
|
20
|
+
The loop is greedy and exact: its tokens are gated against the host's
|
|
21
|
+
own generation by the caller's probe. Structure swaps (fused layers,
|
|
22
|
+
precision bands) compose underneath — build the loop after
|
|
23
|
+
``auto_swaps`` and it captures whatever is attached.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import torch
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _StaticHybridCache:
|
|
32
|
+
"""The surface a hybrid (attention + gated-delta) stack touches.
|
|
33
|
+
|
|
34
|
+
``update`` serves attention layers from preallocated buffers;
|
|
35
|
+
``conv_states``/``recurrent_states`` are plain per-layer slots the
|
|
36
|
+
gated-delta layers read and write; ``has_previous_state`` mirrors
|
|
37
|
+
the host convention (a filled conv slot means decode).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, n_layers, attn_layers, kv_heads, head_dim,
|
|
41
|
+
max_len, device, dtype=torch.bfloat16):
|
|
42
|
+
self.conv_states = [None] * n_layers
|
|
43
|
+
self.recurrent_states = [None] * n_layers
|
|
44
|
+
self.key_cache = [None] * n_layers
|
|
45
|
+
self.value_cache = [None] * n_layers
|
|
46
|
+
self._max = int(max_len)
|
|
47
|
+
self._cp = None
|
|
48
|
+
# true prompt-progress counter for HOST-side forwards: host glue
|
|
49
|
+
# branches on "is there history" (rope deltas, prefill vs
|
|
50
|
+
# continue); the loop's own fwd never consults it
|
|
51
|
+
self._seen = 0
|
|
52
|
+
for i in attn_layers:
|
|
53
|
+
self.key_cache[i] = torch.zeros(
|
|
54
|
+
1, kv_heads, self._max, head_dim, device=device,
|
|
55
|
+
dtype=dtype)
|
|
56
|
+
self.value_cache[i] = torch.zeros_like(self.key_cache[i])
|
|
57
|
+
|
|
58
|
+
def update(self, k, v, layer_idx, cache_kwargs=None):
|
|
59
|
+
self.key_cache[layer_idx].index_copy_(2, self._cp, k)
|
|
60
|
+
self.value_cache[layer_idx].index_copy_(2, self._cp, v)
|
|
61
|
+
band = getattr(self, "frt_fp8_band", None)
|
|
62
|
+
if band is not None:
|
|
63
|
+
# dual-store: the same post-rope rows land in the FP8 pages
|
|
64
|
+
# the XQA read path consumes; BF16 stays the prefill arm
|
|
65
|
+
band.write(layer_idx, k, v, self._cp)
|
|
66
|
+
return self.key_cache[layer_idx], self.value_cache[layer_idx]
|
|
67
|
+
|
|
68
|
+
def get_seq_length(self, layer_idx=0):
|
|
69
|
+
return self._seen
|
|
70
|
+
|
|
71
|
+
def get_mask_sizes(self, query_length, layer_idx):
|
|
72
|
+
return self._max, 0
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def has_previous_state(self):
|
|
76
|
+
return any(s is not None for s in self.conv_states)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _find_stack(model):
|
|
80
|
+
"""Locate the decoder stack by the slots it must carry, not names."""
|
|
81
|
+
for mod in (getattr(model, "model", model),):
|
|
82
|
+
for cand in (getattr(mod, "language_model", None), mod):
|
|
83
|
+
if cand is None:
|
|
84
|
+
continue
|
|
85
|
+
if (hasattr(cand, "layers") and hasattr(cand, "embed_tokens")
|
|
86
|
+
and hasattr(cand, "norm")
|
|
87
|
+
and hasattr(cand, "rotary_emb")):
|
|
88
|
+
return cand
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"refused: no decoder stack with (layers, embed_tokens, norm, "
|
|
91
|
+
"rotary_emb) slots found on this host")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WholeStepDecodeLoop:
|
|
95
|
+
"""Compiled, graph-captured greedy decode over the attached model."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, model, *, max_len, compile_step=True,
|
|
98
|
+
compile_prefill=True, kv_band=None):
|
|
99
|
+
lm = _find_stack(model)
|
|
100
|
+
self._model = model
|
|
101
|
+
self._lm = lm
|
|
102
|
+
self._compile_prefill = bool(compile_prefill)
|
|
103
|
+
head = getattr(model, "lm_head", None)
|
|
104
|
+
if head is None:
|
|
105
|
+
raise ValueError("refused: host carries no lm_head")
|
|
106
|
+
self._layers = lm.layers
|
|
107
|
+
self._rotary = lm.rotary_emb
|
|
108
|
+
self._norm = lm.norm
|
|
109
|
+
self._embed = lm.embed_tokens
|
|
110
|
+
self._head = head
|
|
111
|
+
self._full = [i for i, lyr in enumerate(self._layers)
|
|
112
|
+
if hasattr(lyr, "self_attn")]
|
|
113
|
+
cfg = getattr(model.config, "text_config", model.config)
|
|
114
|
+
kvh = int(cfg.num_key_value_heads)
|
|
115
|
+
hd = int(getattr(cfg, "head_dim",
|
|
116
|
+
cfg.hidden_size // cfg.num_attention_heads))
|
|
117
|
+
dev = head.weight.device if hasattr(head, "weight") else "cuda"
|
|
118
|
+
# the static window rounds up to a 16-byte row: the fused
|
|
119
|
+
# attention kernels vector-read the mask row in aligned chunks,
|
|
120
|
+
# and an unaligned tail overreads into neighbouring allocations
|
|
121
|
+
# — outputs then drift with allocator contents (the 2K repeat
|
|
122
|
+
# gate caught it; compute-sanitizer named the aligned kernel).
|
|
123
|
+
# Rounding the window keeps mask and KV rows contiguous and
|
|
124
|
+
# aligned; the padded tail rows stay -inf-masked and zero.
|
|
125
|
+
self._max = -(-int(max_len) // 8) * 8
|
|
126
|
+
self.cache = _StaticHybridCache(
|
|
127
|
+
len(self._layers), self._full, kvh, hd, self._max, dev)
|
|
128
|
+
# the loop routes attention through its own interface even
|
|
129
|
+
# without a band: the maskless prompt pass needs the square
|
|
130
|
+
# causal slice to stay off SDPA's materialising math path
|
|
131
|
+
from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
|
|
132
|
+
|
|
133
|
+
from .fp8_kv import _INTERFACE_NAME, _interface
|
|
134
|
+
if _INTERFACE_NAME not in ALL_ATTENTION_FUNCTIONS:
|
|
135
|
+
ALL_ATTENTION_FUNCTIONS.register(_INTERFACE_NAME, _interface)
|
|
136
|
+
for i in self._full:
|
|
137
|
+
self._layers[i].self_attn.config._attn_implementation = \
|
|
138
|
+
_INTERFACE_NAME
|
|
139
|
+
self._kv_band = None
|
|
140
|
+
if kv_band == "fp8":
|
|
141
|
+
from .fp8_kv import install
|
|
142
|
+
|
|
143
|
+
self._kv_band = install(model, lm, self.cache, self._max)
|
|
144
|
+
if self._kv_band is None:
|
|
145
|
+
raise ValueError(
|
|
146
|
+
"refused: fp8 kv band serves the kernel's v1 head "
|
|
147
|
+
"profile (24/4/256); this host keeps BF16 KV")
|
|
148
|
+
elif kv_band is not None:
|
|
149
|
+
raise ValueError(f"refused: unknown kv band {kv_band!r}")
|
|
150
|
+
# no quadratic causal table: the decode mask is one static row
|
|
151
|
+
# rebuilt in-graph from the position buffer, the prompt pass
|
|
152
|
+
# rides SDPA's own causal flag (top-left alignment matches the
|
|
153
|
+
# window layout), and offset multi-row passes build their rows
|
|
154
|
+
# on demand — at deep windows the [max, max] table and the
|
|
155
|
+
# prefill's materialised [S, max] mask are gigabytes
|
|
156
|
+
self._ar = torch.arange(self._max, device=dev)
|
|
157
|
+
self._mrow = torch.zeros(1, 1, 1, self._max, device=dev,
|
|
158
|
+
dtype=torch.bfloat16)
|
|
159
|
+
self._cur = torch.empty(1, 1, dtype=torch.long, device=dev)
|
|
160
|
+
self._pos = torch.empty(1, dtype=torch.long, device=dev)
|
|
161
|
+
self._rope_delta = torch.zeros(1, dtype=torch.long, device=dev)
|
|
162
|
+
self._use_compile = bool(compile_step)
|
|
163
|
+
if compile_step:
|
|
164
|
+
# the layer loop specialises per layer index; the default
|
|
165
|
+
# recompile budget is smaller than a deep stack
|
|
166
|
+
torch._dynamo.config.cache_size_limit = max(
|
|
167
|
+
torch._dynamo.config.cache_size_limit,
|
|
168
|
+
4 * len(self._layers))
|
|
169
|
+
self._step = torch.compile(self._fwd, dynamic=False)
|
|
170
|
+
else:
|
|
171
|
+
self._step = self._fwd
|
|
172
|
+
self._graph = None
|
|
173
|
+
self._aot_pf = None
|
|
174
|
+
|
|
175
|
+
def _fwd(self, tok_ids, pos_t):
|
|
176
|
+
h = self._embed(tok_ids)
|
|
177
|
+
# KV slot index and rotary position are different things on
|
|
178
|
+
# multimodal hosts: the host's rope delta shifts the latter
|
|
179
|
+
pe = self._rotary(h, (pos_t + self._rope_delta).view(1, -1))
|
|
180
|
+
self.cache._cp = pos_t
|
|
181
|
+
if self._kv_band is not None:
|
|
182
|
+
self._kv_band.set_len(pos_t[-1:] + 1)
|
|
183
|
+
if tok_ids.shape[1] == 1:
|
|
184
|
+
self._mrow.zero_()
|
|
185
|
+
self._mrow.masked_fill_(
|
|
186
|
+
(self._ar > pos_t[-1]).view(1, 1, 1, -1), float("-inf"))
|
|
187
|
+
m4 = self._mrow
|
|
188
|
+
else:
|
|
189
|
+
# prompt rows start at zero: SDPA's causal flag is this
|
|
190
|
+
# exact mask, with nothing materialised
|
|
191
|
+
m4 = None
|
|
192
|
+
for i, lyr in enumerate(self._layers):
|
|
193
|
+
h = lyr(h, position_embeddings=pe,
|
|
194
|
+
attention_mask=(m4 if i in self._full_set else None),
|
|
195
|
+
past_key_values=self.cache, use_cache=True,
|
|
196
|
+
cache_position=pos_t)
|
|
197
|
+
return self._head(self._norm(h)[:, -1:])
|
|
198
|
+
|
|
199
|
+
@property
|
|
200
|
+
def _full_set(self):
|
|
201
|
+
s = getattr(self, "_full_cache", None)
|
|
202
|
+
if s is None:
|
|
203
|
+
s = set(self._full)
|
|
204
|
+
self._full_cache = s
|
|
205
|
+
return s
|
|
206
|
+
|
|
207
|
+
def _prefill_callable(self):
|
|
208
|
+
"""The prompt pass gets its own compile, pinned deterministic.
|
|
209
|
+
|
|
210
|
+
The fast step's inductor artifacts use non-deterministic
|
|
211
|
+
reductions that are harmless inside a captured replay but flip
|
|
212
|
+
long-prompt logits between runs — the 2K-context receipts
|
|
213
|
+
caught exactly that. Prefill therefore compiles separately with
|
|
214
|
+
the backend's deterministic mode, keeping the eager arm's
|
|
215
|
+
repeatability at compiled speed. Shapes specialise per prompt
|
|
216
|
+
length, same as the step.
|
|
217
|
+
"""
|
|
218
|
+
if not self._compile_prefill:
|
|
219
|
+
return self._fwd
|
|
220
|
+
if getattr(self, "_pf", None) is None:
|
|
221
|
+
self._pf = torch.compile(self._fwd, dynamic=False,
|
|
222
|
+
options={"deterministic": True})
|
|
223
|
+
return self._pf
|
|
224
|
+
|
|
225
|
+
def _gstep(self):
|
|
226
|
+
logits = self._step(self._cur, self._pos)
|
|
227
|
+
# hosts sample from FP32 logits; argmax there too, or BF16
|
|
228
|
+
# ties break differently and free runs diverge
|
|
229
|
+
self._cur.copy_(logits.float().argmax(-1))
|
|
230
|
+
self._pos.add_(1)
|
|
231
|
+
|
|
232
|
+
@torch.no_grad()
|
|
233
|
+
def generate(self, input_ids, max_new_tokens):
|
|
234
|
+
"""Greedy generation; the first call warms, compiles, captures."""
|
|
235
|
+
L = int(input_ids.shape[1])
|
|
236
|
+
if L + max_new_tokens > self._max:
|
|
237
|
+
raise ValueError(
|
|
238
|
+
f"refused: {L}+{max_new_tokens} exceeds the static "
|
|
239
|
+
f"window {self._max}")
|
|
240
|
+
if self._graph is None and not getattr(self, "_warming", False):
|
|
241
|
+
# the loop's true first pass is a throwaway: it compiles,
|
|
242
|
+
# captures, and settles the library workspaces whose
|
|
243
|
+
# content the earliest replays see differently from every
|
|
244
|
+
# later call. Returned generations all come from the
|
|
245
|
+
# settled regime, which repeats bitwise
|
|
246
|
+
self._warming = True
|
|
247
|
+
try:
|
|
248
|
+
self.generate(input_ids, max_new_tokens)
|
|
249
|
+
finally:
|
|
250
|
+
self._warming = False
|
|
251
|
+
self.cache.frt_continue = False
|
|
252
|
+
self._rope_delta.zero_()
|
|
253
|
+
if self._kv_band is not None:
|
|
254
|
+
self._kv_band.reset()
|
|
255
|
+
# canonical window: rows past the prompt zero before any
|
|
256
|
+
# produced token. Masked tail rows are not numerically inert
|
|
257
|
+
# in every attention backend, and the repeat gate compares
|
|
258
|
+
# calls whose tails would otherwise carry different residue
|
|
259
|
+
for i in self._full:
|
|
260
|
+
self.cache.key_cache[i][:, :, L:].zero_()
|
|
261
|
+
self.cache.value_cache[i][:, :, L:].zero_()
|
|
262
|
+
pf = self._aot_pf or self._prefill_callable()
|
|
263
|
+
logits = pf(input_ids,
|
|
264
|
+
torch.arange(L, device=input_ids.device))
|
|
265
|
+
self._cur.copy_(logits.float().argmax(-1))
|
|
266
|
+
self._pos.fill_(L)
|
|
267
|
+
toks = [self._cur.clone()]
|
|
268
|
+
self._decode_tail(max_new_tokens, toks)
|
|
269
|
+
return torch.cat([input_ids] + toks, dim=1)
|
|
270
|
+
|
|
271
|
+
def _decode_tail(self, max_new_tokens, toks):
|
|
272
|
+
warm = min(3, max_new_tokens - 1)
|
|
273
|
+
if self._graph is None:
|
|
274
|
+
# first capture takes the uniform path: an eager warmup
|
|
275
|
+
# step and a replayed step need not be bit-identical, so
|
|
276
|
+
# no produced token may come off the eager arm — the first
|
|
277
|
+
# call would disagree with every steady call at the first
|
|
278
|
+
# post-warmup token. Warm without producing tokens,
|
|
279
|
+
# capture, roll the state back, and produce every token
|
|
280
|
+
# from the replay. KV rows (BF16 and FP8 pages alike)
|
|
281
|
+
# rewrite the same slots on replay, so only the
|
|
282
|
+
# gated-delta slots and cur/pos need snapshots; those
|
|
283
|
+
# stage to host — this runs once, and the device is
|
|
284
|
+
# already packed to the rim.
|
|
285
|
+
snaps = [(i, self.cache.conv_states[i].to("cpu"),
|
|
286
|
+
self.cache.recurrent_states[i].to("cpu"))
|
|
287
|
+
for i in self._gdn_slots()]
|
|
288
|
+
cur0, pos0 = self._cur.clone(), self._pos.clone()
|
|
289
|
+
side = torch.cuda.Stream()
|
|
290
|
+
side.wait_stream(torch.cuda.current_stream())
|
|
291
|
+
with torch.cuda.stream(side):
|
|
292
|
+
for _ in range(warm):
|
|
293
|
+
self._gstep()
|
|
294
|
+
torch.cuda.current_stream().wait_stream(side)
|
|
295
|
+
self._graph = torch.cuda.CUDAGraph()
|
|
296
|
+
with torch.cuda.graph(self._graph):
|
|
297
|
+
self._gstep()
|
|
298
|
+
# prime the pool: capture records without executing, so
|
|
299
|
+
# the first replay ever reads workspace buffers no replay
|
|
300
|
+
# has written; its output is discarded and the state
|
|
301
|
+
# rolled back, so every produced token comes from a replay
|
|
302
|
+
# whose workspace a previous replay populated
|
|
303
|
+
self._graph.replay()
|
|
304
|
+
for i, cs, rs in snaps:
|
|
305
|
+
self.cache.conv_states[i].copy_(cs)
|
|
306
|
+
self.cache.recurrent_states[i].copy_(rs)
|
|
307
|
+
self._cur.copy_(cur0)
|
|
308
|
+
self._pos.copy_(pos0)
|
|
309
|
+
# the rollback extends to the written rows: warmup steps
|
|
310
|
+
# and the priming replay committed KV past the prompt, and
|
|
311
|
+
# the canonical window holds that region zero before
|
|
312
|
+
# produced tokens
|
|
313
|
+
p0 = int(pos0[0])
|
|
314
|
+
span = warm + 1
|
|
315
|
+
for i in self._full:
|
|
316
|
+
self.cache.key_cache[i][:, :, p0:p0 + span].zero_()
|
|
317
|
+
self.cache.value_cache[i][:, :, p0:p0 + span].zero_()
|
|
318
|
+
if self._kv_band is not None:
|
|
319
|
+
self._kv_band.clear_rows(
|
|
320
|
+
torch.arange(span, device=pos0.device) + pos0)
|
|
321
|
+
for _ in range(max_new_tokens - 1):
|
|
322
|
+
self._graph.replay()
|
|
323
|
+
toks.append(self._cur.clone())
|
|
324
|
+
else:
|
|
325
|
+
# steady calls run every decode step as a replay
|
|
326
|
+
for _ in range(max_new_tokens - 1):
|
|
327
|
+
self._graph.replay()
|
|
328
|
+
toks.append(self._cur.clone())
|
|
329
|
+
|
|
330
|
+
@torch.no_grad()
|
|
331
|
+
def aot_prefill(self, example_ids, package_path,
|
|
332
|
+
inductor_configs=None):
|
|
333
|
+
"""Package the prompt pass whole and route ``generate`` through it.
|
|
334
|
+
|
|
335
|
+
S-specialised like the compiled prefill. The package is built
|
|
336
|
+
weights-external: the graph binary carries no constants, and at
|
|
337
|
+
load it borrows the live parameters and the loop's own KV rows
|
|
338
|
+
in place — a second copy of the weights never exists on the
|
|
339
|
+
card, and the packaged KV writes are writes into the cache the
|
|
340
|
+
captured decode step reads. The same-token gate downstream
|
|
341
|
+
checks exactly that hand-off.
|
|
342
|
+
"""
|
|
343
|
+
if self._kv_band is not None:
|
|
344
|
+
raise ValueError(
|
|
345
|
+
"refused: aot_prefill serves the BF16-KV loop; the FP8 "
|
|
346
|
+
"band's prefill dual-store is not packaged yet")
|
|
347
|
+
from ...aot import aot_load, aot_package_external
|
|
348
|
+
|
|
349
|
+
dev = example_ids.device
|
|
350
|
+
vehicle = _PrefillVehicle(self)
|
|
351
|
+
pos = torch.arange(int(example_ids.shape[1]), device=dev)
|
|
352
|
+
self.cache.frt_continue = False
|
|
353
|
+
self._rope_delta.zero_()
|
|
354
|
+
try:
|
|
355
|
+
path, weights = aot_package_external(
|
|
356
|
+
vehicle, args=(example_ids, pos),
|
|
357
|
+
package_path=package_path,
|
|
358
|
+
inductor_configs=inductor_configs)
|
|
359
|
+
finally:
|
|
360
|
+
# tracing rebinds the cache lists through the vehicle's
|
|
361
|
+
# buffer attributes and leaves fake proxies behind; put the
|
|
362
|
+
# real tensors back whatever happened
|
|
363
|
+
for i in self._full:
|
|
364
|
+
self.cache.key_cache[i] = getattr(vehicle, f"frt_k{i}")
|
|
365
|
+
self.cache.value_cache[i] = getattr(vehicle, f"frt_v{i}")
|
|
366
|
+
self._aot_pf = aot_load(path, weights=weights)
|
|
367
|
+
return path
|
|
368
|
+
|
|
369
|
+
@torch.no_grad()
|
|
370
|
+
def generate_from(self, inputs, max_new_tokens):
|
|
371
|
+
"""Host-side prefill, loop-side decode.
|
|
372
|
+
|
|
373
|
+
The host forward runs the whole multimodal front (vision tower,
|
|
374
|
+
embed merge, mrope) and writes its KV into the loop's cache;
|
|
375
|
+
the captured step takes over from the first generated token.
|
|
376
|
+
``inputs`` is the processor's dict; batch of one, no padding.
|
|
377
|
+
"""
|
|
378
|
+
inputs = {k: v for k, v in inputs.items()
|
|
379
|
+
if k != "attention_mask"}
|
|
380
|
+
ids = inputs["input_ids"]
|
|
381
|
+
L = int(ids.shape[1])
|
|
382
|
+
if L + max_new_tokens > self._max:
|
|
383
|
+
raise ValueError(
|
|
384
|
+
f"refused: {L}+{max_new_tokens} exceeds the static "
|
|
385
|
+
f"window {self._max}")
|
|
386
|
+
self.cache.frt_continue = False
|
|
387
|
+
self.cache._seen = 0 # host must take its prefill branch
|
|
388
|
+
for i in self._full:
|
|
389
|
+
self.cache.key_cache[i][:, :, L:].zero_()
|
|
390
|
+
self.cache.value_cache[i][:, :, L:].zero_()
|
|
391
|
+
cp = torch.arange(L, device=ids.device)
|
|
392
|
+
self.cache._cp = cp
|
|
393
|
+
out = self._model(**inputs, past_key_values=self.cache,
|
|
394
|
+
use_cache=True, cache_position=cp)
|
|
395
|
+
self.cache._seen = L
|
|
396
|
+
delta = getattr(
|
|
397
|
+
getattr(self._model, "model", self._model),
|
|
398
|
+
"rope_deltas", None)
|
|
399
|
+
self._rope_delta.fill_(
|
|
400
|
+
int(delta.reshape(-1)[0]) if torch.is_tensor(delta) else 0)
|
|
401
|
+
self._cur.copy_(out.logits[:, -1:].float().argmax(-1))
|
|
402
|
+
self._pos.fill_(L)
|
|
403
|
+
toks = [self._cur.clone()]
|
|
404
|
+
self._decode_tail(max_new_tokens, toks)
|
|
405
|
+
return torch.cat([ids] + toks, dim=1)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@torch.no_grad()
|
|
409
|
+
def enable_mtp(self, ckpt_dir=None, head=None,
|
|
410
|
+
projection_format=None, default_k=6,
|
|
411
|
+
verify_capture=True):
|
|
412
|
+
"""Attach the checkpoint's draft head.
|
|
413
|
+
|
|
414
|
+
The draft's precision axes are explicit and answer to acceptance
|
|
415
|
+
length alone (the verify pass anchors the output either way):
|
|
416
|
+
``projection_format`` picks the draft expert-bank arm —
|
|
417
|
+
``"bf16"`` (default, conservative) or ``"nvfp4_dynamic"``
|
|
418
|
+
(measured AL-equal, smaller) — and the draft always carries a
|
|
419
|
+
private W8 view of the shared head, because the model's own
|
|
420
|
+
head must stay on the step/verify numeric family. Pass a
|
|
421
|
+
prebuilt ``head`` to load it early, while the device still has
|
|
422
|
+
assembly headroom; a prebuilt head already carries its formats
|
|
423
|
+
and a conflicting ``projection_format`` here is refused, not
|
|
424
|
+
silently ignored.
|
|
425
|
+
|
|
426
|
+
``verify_capture=False`` keeps the M=K+1 verify and rewrite
|
|
427
|
+
passes eager — the diagnostic arm; the captured passes are the
|
|
428
|
+
production form."""
|
|
429
|
+
from .mtp_speculative import MtpDraftHead, check_draft_formats
|
|
430
|
+
|
|
431
|
+
if projection_format is not None:
|
|
432
|
+
check_draft_formats("w8a16_static", projection_format)
|
|
433
|
+
if head is not None and getattr(head, "formats", {}).get(
|
|
434
|
+
"experts") not in (None, projection_format):
|
|
435
|
+
raise ValueError(
|
|
436
|
+
f"refused: prebuilt draft head carries experts "
|
|
437
|
+
f"format {head.formats['experts']!r}, caller asked "
|
|
438
|
+
f"for {projection_format!r}")
|
|
439
|
+
slot = len(self._layers)
|
|
440
|
+
ref = self.cache.key_cache[self._full[0]]
|
|
441
|
+
self.cache.key_cache.append(torch.zeros_like(ref))
|
|
442
|
+
self.cache.value_cache.append(torch.zeros_like(ref))
|
|
443
|
+
self.cache.conv_states.append(None)
|
|
444
|
+
self.cache.recurrent_states.append(None)
|
|
445
|
+
if head is None:
|
|
446
|
+
head = MtpDraftHead(
|
|
447
|
+
self._model, self._lm, ckpt_dir, slot,
|
|
448
|
+
experts_format=(projection_format or "bf16"))
|
|
449
|
+
if head.slot != slot:
|
|
450
|
+
raise ValueError(
|
|
451
|
+
f"refused: draft head was built for slot {head.slot}, "
|
|
452
|
+
f"this loop's slot is {slot}")
|
|
453
|
+
self._mtp = head
|
|
454
|
+
self._mtp_formats = dict(getattr(head, "formats", {}))
|
|
455
|
+
self._default_k = int(default_k)
|
|
456
|
+
self._verify_capture = bool(verify_capture)
|
|
457
|
+
return self._mtp
|
|
458
|
+
|
|
459
|
+
def _fwd_full(self, tok_ids, pos_t):
|
|
460
|
+
h = self._embed(tok_ids)
|
|
461
|
+
pe = self._rotary(h, pos_t.view(1, -1))
|
|
462
|
+
self.cache._cp = pos_t
|
|
463
|
+
if self._kv_band is not None:
|
|
464
|
+
self._kv_band.set_len(pos_t[-1:] + 1)
|
|
465
|
+
m4 = self._row(pos_t)
|
|
466
|
+
for i, lyr in enumerate(self._layers):
|
|
467
|
+
h = lyr(h, position_embeddings=pe,
|
|
468
|
+
attention_mask=(m4 if i in self._full_set else None),
|
|
469
|
+
past_key_values=self.cache, use_cache=True,
|
|
470
|
+
cache_position=pos_t)
|
|
471
|
+
hn = self._norm(h)
|
|
472
|
+
return self._head(hn), hn
|
|
473
|
+
|
|
474
|
+
def _row(self, pos_t):
|
|
475
|
+
# offset multi-row mask (spec verify/rewrite): built on demand
|
|
476
|
+
# from the positions, a few rows at a time
|
|
477
|
+
return torch.zeros(
|
|
478
|
+
1, 1, pos_t.shape[0], self._max, device=pos_t.device,
|
|
479
|
+
dtype=torch.bfloat16).masked_fill_(
|
|
480
|
+
(self._ar.view(1, -1) > pos_t.view(-1, 1)).view(
|
|
481
|
+
1, 1, -1, self._max), float("-inf"))
|
|
482
|
+
|
|
483
|
+
def _gdn_slots(self):
|
|
484
|
+
return [i for i, sl in enumerate(self.cache.conv_states)
|
|
485
|
+
if torch.is_tensor(sl)]
|
|
486
|
+
|
|
487
|
+
def _ensure_spec_graphs(self, K, dev, snaps, h_last, tok, pos):
|
|
488
|
+
"""Capture the K-step draft chain and the M=K+1 verify pass.
|
|
489
|
+
|
|
490
|
+
The warmup executions commit real state; the caller's snapshot
|
|
491
|
+
is restored before capture so the first replay starts clean.
|
|
492
|
+
"""
|
|
493
|
+
if self._kv_band is not None:
|
|
494
|
+
# every spec shape's mask exists before the compiled passes
|
|
495
|
+
# trace: a lazy dict fill flips a guard between warmup and
|
|
496
|
+
# capture, and recompiling mid-capture is illegal
|
|
497
|
+
self._kv_band.prewarm(range(1, K + 2))
|
|
498
|
+
self._dh_buf = torch.empty(1, 1, self._embed.weight.shape[1],
|
|
499
|
+
device=dev, dtype=torch.bfloat16)
|
|
500
|
+
self._dtok_buf = torch.empty(1, 1, dtype=torch.long, device=dev)
|
|
501
|
+
self._dpos_buf = torch.empty(1, dtype=torch.long, device=dev)
|
|
502
|
+
self._dtoks_out = torch.empty(K, dtype=torch.long, device=dev)
|
|
503
|
+
self._vseq_buf = torch.empty(1, K + 1, dtype=torch.long,
|
|
504
|
+
device=dev)
|
|
505
|
+
self._vpos_buf = torch.empty(K + 1, dtype=torch.long, device=dev)
|
|
506
|
+
# warmup must run on live values — an uninitialised token
|
|
507
|
+
# buffer is an out-of-bounds embedding lookup
|
|
508
|
+
self._dh_buf.copy_(h_last)
|
|
509
|
+
self._dtok_buf.copy_(tok)
|
|
510
|
+
self._dpos_buf.fill_(pos)
|
|
511
|
+
self._vpos_buf.copy_(torch.arange(pos, pos + K + 1, device=dev))
|
|
512
|
+
|
|
513
|
+
def draft_chain():
|
|
514
|
+
for k in range(K):
|
|
515
|
+
lg, dh = self._mtp(self._dh_buf, self._dtok_buf,
|
|
516
|
+
self._dpos_buf, self.cache,
|
|
517
|
+
self._row(self._dpos_buf))
|
|
518
|
+
self._dh_buf.copy_(dh)
|
|
519
|
+
nxt = lg[:, -1].float().argmax(-1)
|
|
520
|
+
self._dtoks_out[k].copy_(nxt[0])
|
|
521
|
+
self._dtok_buf.copy_(nxt.view(1, 1))
|
|
522
|
+
self._dpos_buf.add_(1)
|
|
523
|
+
|
|
524
|
+
# the verify pass runs every round: when the step is compiled,
|
|
525
|
+
# verify takes the same compile-then-capture recipe — inductor's
|
|
526
|
+
# elementwise fusion is the same third it buys the plain step
|
|
527
|
+
if self._use_compile and getattr(self, "_fwd_full_c", None) is None:
|
|
528
|
+
self._fwd_full_c = torch.compile(self._fwd_full,
|
|
529
|
+
dynamic=False)
|
|
530
|
+
fwd_v = (self._fwd_full_c if self._use_compile
|
|
531
|
+
else self._fwd_full)
|
|
532
|
+
|
|
533
|
+
def verify():
|
|
534
|
+
lg, hn = fwd_v(self._vseq_buf, self._vpos_buf)
|
|
535
|
+
return lg, hn
|
|
536
|
+
|
|
537
|
+
side = torch.cuda.Stream()
|
|
538
|
+
side.wait_stream(torch.cuda.current_stream())
|
|
539
|
+
with torch.cuda.stream(side):
|
|
540
|
+
draft_chain()
|
|
541
|
+
self._vseq_buf[:, 0].copy_(tok[0])
|
|
542
|
+
self._vseq_buf[:, 1:].copy_(self._dtoks_out)
|
|
543
|
+
verify()
|
|
544
|
+
torch.cuda.current_stream().wait_stream(side)
|
|
545
|
+
for i, cs, rs in snaps:
|
|
546
|
+
self.cache.conv_states[i].copy_(cs)
|
|
547
|
+
self.cache.recurrent_states[i].copy_(rs)
|
|
548
|
+
def snap_states():
|
|
549
|
+
# the pre-round state snapshot rides inside the draft graph:
|
|
550
|
+
# sixty host-launched little copies a round otherwise sit
|
|
551
|
+
# squarely inside the per-round sync window
|
|
552
|
+
for i, (cb, rb) in self._snap_bufs.items():
|
|
553
|
+
cb.copy_(self.cache.conv_states[i])
|
|
554
|
+
rb.copy_(self.cache.recurrent_states[i])
|
|
555
|
+
|
|
556
|
+
self._dgraph = torch.cuda.CUDAGraph()
|
|
557
|
+
with torch.cuda.graph(self._dgraph):
|
|
558
|
+
snap_states()
|
|
559
|
+
draft_chain()
|
|
560
|
+
# rejected rounds re-advance the accepted prefix; each of the K
|
|
561
|
+
# possible prefix lengths is a fixed shape, so each gets its own
|
|
562
|
+
# captured pass — the reject path replays instead of paying an
|
|
563
|
+
# eager whole-model forward
|
|
564
|
+
self._ra_seq, self._ra_pos = {}, {}
|
|
565
|
+
self._ra_lg, self._ra_hn = {}, {}
|
|
566
|
+
self._ra_graphs = {}
|
|
567
|
+
for m in range(1, K + 1):
|
|
568
|
+
self._ra_seq[m] = torch.empty(1, m, dtype=torch.long,
|
|
569
|
+
device=dev)
|
|
570
|
+
self._ra_pos[m] = torch.empty(m, dtype=torch.long, device=dev)
|
|
571
|
+
self._ra_seq[m].copy_(self._vseq_buf[:, :m])
|
|
572
|
+
self._ra_pos[m].copy_(self._vpos_buf[:m])
|
|
573
|
+
side_ra = torch.cuda.Stream()
|
|
574
|
+
side_ra.wait_stream(torch.cuda.current_stream())
|
|
575
|
+
with torch.cuda.stream(side_ra):
|
|
576
|
+
for m in range(1, K + 1):
|
|
577
|
+
self._fwd_full(self._ra_seq[m], self._ra_pos[m])
|
|
578
|
+
torch.cuda.current_stream().wait_stream(side_ra)
|
|
579
|
+
for i, cs, rs in snaps:
|
|
580
|
+
self.cache.conv_states[i].copy_(cs)
|
|
581
|
+
self.cache.recurrent_states[i].copy_(rs)
|
|
582
|
+
for m in range(1, K + 1):
|
|
583
|
+
g = torch.cuda.CUDAGraph()
|
|
584
|
+
with torch.cuda.graph(g):
|
|
585
|
+
# the rollback restore rides in-graph too: the draft
|
|
586
|
+
# graph snapshotted these buffers before the round
|
|
587
|
+
for i, (cb, rb) in self._snap_bufs.items():
|
|
588
|
+
self.cache.conv_states[i].copy_(cb)
|
|
589
|
+
self.cache.recurrent_states[i].copy_(rb)
|
|
590
|
+
self._ra_lg[m], self._ra_hn[m] = self._fwd_full(
|
|
591
|
+
self._ra_seq[m], self._ra_pos[m])
|
|
592
|
+
self._ra_graphs[m] = g
|
|
593
|
+
# the draft-KV rewrite over a cut-short accepted region is the
|
|
594
|
+
# same story: K possible shapes, each captured once. The draft
|
|
595
|
+
# layer carries no gated-delta state, so no snapshot dance.
|
|
596
|
+
hdim = self._dh_buf.shape[-1]
|
|
597
|
+
self._rwm_h, self._rwm_graphs = {}, {}
|
|
598
|
+
side_rw = torch.cuda.Stream()
|
|
599
|
+
side_rw.wait_stream(torch.cuda.current_stream())
|
|
600
|
+
with torch.cuda.stream(side_rw):
|
|
601
|
+
for m in range(1, K + 1):
|
|
602
|
+
self._rwm_h[m] = torch.zeros(1, m, hdim, device=dev,
|
|
603
|
+
dtype=torch.bfloat16)
|
|
604
|
+
self._mtp(self._rwm_h[m], self._ra_seq[m],
|
|
605
|
+
self._ra_pos[m], self.cache,
|
|
606
|
+
self._row(self._ra_pos[m]))
|
|
607
|
+
torch.cuda.current_stream().wait_stream(side_rw)
|
|
608
|
+
for m in range(1, K + 1):
|
|
609
|
+
g = torch.cuda.CUDAGraph()
|
|
610
|
+
with torch.cuda.graph(g):
|
|
611
|
+
self._mtp(self._rwm_h[m], self._ra_seq[m],
|
|
612
|
+
self._ra_pos[m], self.cache,
|
|
613
|
+
self._row(self._ra_pos[m]))
|
|
614
|
+
self._rwm_graphs[m] = g
|
|
615
|
+
if not getattr(self, "_verify_capture", True):
|
|
616
|
+
# multi-token passes stay eager (MoE hosts: the T>1 expert
|
|
617
|
+
# path routes on the host); the draft chain above is the
|
|
618
|
+
# captured piece either way
|
|
619
|
+
self._vgraph = None
|
|
620
|
+
self._rwgraph = None
|
|
621
|
+
return
|
|
622
|
+
self._vgraph = torch.cuda.CUDAGraph()
|
|
623
|
+
with torch.cuda.graph(self._vgraph):
|
|
624
|
+
self._vlg, self._vhn = verify()
|
|
625
|
+
# full-accept rewrite pass is also fixed-shape (K+1 rows)
|
|
626
|
+
self._rwh_buf = torch.empty(1, K + 1, self._dh_buf.shape[-1],
|
|
627
|
+
device=dev, dtype=torch.bfloat16)
|
|
628
|
+
side2 = torch.cuda.Stream()
|
|
629
|
+
side2.wait_stream(torch.cuda.current_stream())
|
|
630
|
+
with torch.cuda.stream(side2):
|
|
631
|
+
self._mtp(self._rwh_buf.zero_(), self._vseq_buf,
|
|
632
|
+
self._vpos_buf, self.cache,
|
|
633
|
+
self._row(self._vpos_buf))
|
|
634
|
+
torch.cuda.current_stream().wait_stream(side2)
|
|
635
|
+
self._rwgraph = torch.cuda.CUDAGraph()
|
|
636
|
+
with torch.cuda.graph(self._rwgraph):
|
|
637
|
+
self._mtp(self._rwh_buf, self._vseq_buf, self._vpos_buf,
|
|
638
|
+
self.cache, self._row(self._vpos_buf))
|
|
639
|
+
|
|
640
|
+
def _seed_spec(self, h_last, tok, pos, K, dev, snaps):
|
|
641
|
+
self._ensure_spec_graphs(K, dev, snaps, h_last, tok, pos)
|
|
642
|
+
self._dh_buf.copy_(h_last)
|
|
643
|
+
self._dtok_buf.copy_(tok)
|
|
644
|
+
self._dpos_buf.fill_(pos)
|
|
645
|
+
self._dgraph.replay()
|
|
646
|
+
|
|
647
|
+
@torch.no_grad()
|
|
648
|
+
def generate_speculative(self, input_ids, max_new_tokens, K=None):
|
|
649
|
+
"""Greedy MTP spec decode; exact vs plain greedy by verify.
|
|
650
|
+
|
|
651
|
+
``K`` (draft chain length) defaults to the value set at
|
|
652
|
+
``enable_mtp``; passing a different K rebuilds the captured
|
|
653
|
+
passes for the new shape."""
|
|
654
|
+
if getattr(self, "_mtp", None) is None:
|
|
655
|
+
raise ValueError("refused: enable_mtp first")
|
|
656
|
+
K = int(K) if K is not None else self._default_k
|
|
657
|
+
if getattr(self, "_spec_k", None) not in (None, K):
|
|
658
|
+
# the captured passes are shaped by K — rebuild, never
|
|
659
|
+
# replay a mismatched shape
|
|
660
|
+
self._dgraph = None
|
|
661
|
+
self._vgraph = None
|
|
662
|
+
self._rwgraph = None
|
|
663
|
+
self._ra_graphs = None
|
|
664
|
+
self._rwm_graphs = None
|
|
665
|
+
self._spec_k = K
|
|
666
|
+
dev = input_ids.device
|
|
667
|
+
L = int(input_ids.shape[1])
|
|
668
|
+
if L + max_new_tokens + K + 1 > self._max:
|
|
669
|
+
raise ValueError("refused: window too small for spec tail")
|
|
670
|
+
self.cache.frt_continue = False
|
|
671
|
+
logits, hn = self._fwd_full(
|
|
672
|
+
input_ids, torch.arange(L, device=dev))
|
|
673
|
+
self.cache.frt_continue = True
|
|
674
|
+
tok = logits[:, -1:].float().argmax(-1)
|
|
675
|
+
# draft KV over the prompt: position p keys on (h_{p-1}, y_p) —
|
|
676
|
+
# one multi-position pass over the whole prompt
|
|
677
|
+
if L > 1:
|
|
678
|
+
pr = torch.arange(1, L, device=dev)
|
|
679
|
+
self._mtp(hn[:, :L - 1], input_ids[:, 1:L], pr,
|
|
680
|
+
self.cache, self._row(pr))
|
|
681
|
+
produced = [tok]
|
|
682
|
+
pos = L
|
|
683
|
+
h_last = hn[:, -1:]
|
|
684
|
+
accepted_hist = []
|
|
685
|
+
self._dgraph = getattr(self, "_dgraph", None)
|
|
686
|
+
# snapshot buffers live across rounds: per-round clones would
|
|
687
|
+
# allocate sixty tensors a round for nothing
|
|
688
|
+
if getattr(self, "_snap_bufs", None) is None:
|
|
689
|
+
self._snap_bufs = {
|
|
690
|
+
i: (torch.empty_like(self.cache.conv_states[i]),
|
|
691
|
+
torch.empty_like(self.cache.recurrent_states[i]))
|
|
692
|
+
for i in self._gdn_slots()}
|
|
693
|
+
while len(produced) < max_new_tokens:
|
|
694
|
+
k_eff = min(K, max_new_tokens - len(produced))
|
|
695
|
+
if k_eff == K and getattr(self, "_dgraph", None) is not None:
|
|
696
|
+
# the draft graph snapshots in-graph on replay
|
|
697
|
+
snaps = [(i, cb, rb)
|
|
698
|
+
for i, (cb, rb) in self._snap_bufs.items()]
|
|
699
|
+
else:
|
|
700
|
+
snaps = []
|
|
701
|
+
for i, (cb, rb) in self._snap_bufs.items():
|
|
702
|
+
cb.copy_(self.cache.conv_states[i])
|
|
703
|
+
rb.copy_(self.cache.recurrent_states[i])
|
|
704
|
+
snaps.append((i, cb, rb))
|
|
705
|
+
if k_eff == K:
|
|
706
|
+
# captured fast path: draft chain replay + verify replay
|
|
707
|
+
if self._dgraph is None:
|
|
708
|
+
self._dh_buf_init = True
|
|
709
|
+
self._seed_spec(h_last, produced[-1], pos, K, dev,
|
|
710
|
+
snaps)
|
|
711
|
+
else:
|
|
712
|
+
self._dh_buf.copy_(h_last)
|
|
713
|
+
self._dtok_buf.copy_(produced[-1])
|
|
714
|
+
self._dpos_buf.fill_(pos)
|
|
715
|
+
self._dgraph.replay()
|
|
716
|
+
self._vseq_buf[:, 0].copy_(produced[-1][0])
|
|
717
|
+
self._vseq_buf[:, 1:].copy_(self._dtoks_out)
|
|
718
|
+
self._vpos_buf.copy_(torch.arange(
|
|
719
|
+
pos, pos + K + 1, device=dev))
|
|
720
|
+
if self._vgraph is not None:
|
|
721
|
+
self._vgraph.replay()
|
|
722
|
+
lg_v, hn_v = self._vlg, self._vhn
|
|
723
|
+
else:
|
|
724
|
+
lg_v, hn_v = self._fwd_full(self._vseq_buf,
|
|
725
|
+
self._vpos_buf)
|
|
726
|
+
dtoks = [self._dtoks_out[k].view(1, 1)
|
|
727
|
+
for k in range(K)]
|
|
728
|
+
seq = self._vseq_buf
|
|
729
|
+
else:
|
|
730
|
+
dtoks, dh, dtok = [], h_last, produced[-1]
|
|
731
|
+
for k in range(k_eff):
|
|
732
|
+
pt = torch.tensor([pos + k], device=dev)
|
|
733
|
+
lg, dh = self._mtp(dh, dtok, pt, self.cache,
|
|
734
|
+
self._row(pt))
|
|
735
|
+
dtok = lg[:, -1:].float().argmax(-1)
|
|
736
|
+
dtoks.append(dtok)
|
|
737
|
+
seq = torch.cat([produced[-1]] + dtoks,
|
|
738
|
+
dim=1)[:, :k_eff + 1]
|
|
739
|
+
pos_v = torch.arange(pos, pos + k_eff + 1, device=dev)
|
|
740
|
+
lg_v, hn_v = self._fwd_full(seq[:, :k_eff + 1], pos_v)
|
|
741
|
+
targets = lg_v.float().argmax(-1) # (1, k_eff+1)
|
|
742
|
+
# device-side prefix match: one sync per round, not one
|
|
743
|
+
# per accepted token
|
|
744
|
+
dstack = torch.cat([d.view(1) for d in dtoks])
|
|
745
|
+
j = int((dstack == targets[0, :k_eff]).cumprod(0)
|
|
746
|
+
.sum().item())
|
|
747
|
+
if j == k_eff:
|
|
748
|
+
# full acceptance: the verify pass committed exactly
|
|
749
|
+
# the accepted stream — no rollback, no second pass
|
|
750
|
+
bonus = targets[:, -1:].contiguous()
|
|
751
|
+
hn_x = hn_v
|
|
752
|
+
else:
|
|
753
|
+
# roll the gated-delta states back and re-advance the
|
|
754
|
+
# accepted prefix — linear states cannot unwind
|
|
755
|
+
m = j + 1
|
|
756
|
+
ra = getattr(self, "_ra_graphs", None)
|
|
757
|
+
if ra and m in ra:
|
|
758
|
+
# the rollback restore is captured at the head of
|
|
759
|
+
# the re-advance graph
|
|
760
|
+
self._ra_seq[m].copy_(seq[:, :m])
|
|
761
|
+
self._ra_pos[m].copy_(torch.arange(
|
|
762
|
+
pos, pos + m, device=dev))
|
|
763
|
+
ra[m].replay()
|
|
764
|
+
lg_a, hn_x = self._ra_lg[m], self._ra_hn[m]
|
|
765
|
+
else:
|
|
766
|
+
for i, cs, rs in snaps:
|
|
767
|
+
self.cache.conv_states[i].copy_(cs)
|
|
768
|
+
self.cache.recurrent_states[i].copy_(rs)
|
|
769
|
+
pos_a = torch.arange(pos, pos + m, device=dev)
|
|
770
|
+
lg_a, hn_x = self._fwd_full(seq[:, :m], pos_a)
|
|
771
|
+
bonus = lg_a[:, -1:].float().argmax(-1)
|
|
772
|
+
# draft KV for the accepted region keys on main hiddens —
|
|
773
|
+
# one multi-position pass; captured when the shape is the
|
|
774
|
+
# full-accept one
|
|
775
|
+
if (j == k_eff and k_eff == K
|
|
776
|
+
and getattr(self, "_rwgraph", None) is not None):
|
|
777
|
+
self._rwh_buf[:, 0].copy_(h_last[:, 0])
|
|
778
|
+
self._rwh_buf[:, 1:].copy_(hn_x[:, :K])
|
|
779
|
+
self._rwgraph.replay()
|
|
780
|
+
else:
|
|
781
|
+
m = j + 1
|
|
782
|
+
rw = getattr(self, "_rwm_graphs", None)
|
|
783
|
+
if rw and m in rw:
|
|
784
|
+
self._rwm_h[m][:, :1].copy_(h_last)
|
|
785
|
+
if j > 0:
|
|
786
|
+
self._rwm_h[m][:, 1:].copy_(hn_x[:, :j])
|
|
787
|
+
self._ra_seq[m].copy_(seq[:, :m])
|
|
788
|
+
self._ra_pos[m].copy_(torch.arange(
|
|
789
|
+
pos, pos + m, device=dev))
|
|
790
|
+
rw[m].replay()
|
|
791
|
+
else:
|
|
792
|
+
prev_rows = (torch.cat([h_last, hn_x[:, :j]], dim=1)
|
|
793
|
+
if j > 0 else h_last)
|
|
794
|
+
pos_rows = torch.arange(pos, pos + m, device=dev)
|
|
795
|
+
self._mtp(prev_rows, seq[:, :m], pos_rows,
|
|
796
|
+
self.cache, self._row(pos_rows))
|
|
797
|
+
for r in range(j):
|
|
798
|
+
produced.append(dtoks[r].clone())
|
|
799
|
+
produced.append(bonus.clone())
|
|
800
|
+
accepted_hist.append(j + 1)
|
|
801
|
+
pos += j + 1
|
|
802
|
+
h_last = hn_x[:, j:j + 1]
|
|
803
|
+
self.cache.frt_continue = False
|
|
804
|
+
out = torch.cat([input_ids] + produced, dim=1)
|
|
805
|
+
self.last_acceptance = (sum(accepted_hist) / len(accepted_hist)
|
|
806
|
+
if accepted_hist else 0.0)
|
|
807
|
+
return out[:, :L + max_new_tokens]
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
class _PrefillVehicle(torch.nn.Module):
|
|
811
|
+
"""Export vehicle for the prompt pass.
|
|
812
|
+
|
|
813
|
+
The state the pass mutates — the static KV rows, the rope delta —
|
|
814
|
+
rides as registered buffers so functionalization records the writes
|
|
815
|
+
as buffer mutations instead of refusing them. The host model hangs
|
|
816
|
+
in the module hierarchy so every parameter exports under its own
|
|
817
|
+
FQN. Loaded with the package borrowing these very tensors, the
|
|
818
|
+
packaged writes land in the loop's own cache.
|
|
819
|
+
"""
|
|
820
|
+
|
|
821
|
+
def __init__(self, loop):
|
|
822
|
+
super().__init__()
|
|
823
|
+
self.model = loop._model
|
|
824
|
+
self._frt_loop = loop
|
|
825
|
+
for i in loop._full:
|
|
826
|
+
self.register_buffer(f"frt_k{i}", loop.cache.key_cache[i])
|
|
827
|
+
self.register_buffer(f"frt_v{i}", loop.cache.value_cache[i])
|
|
828
|
+
self.register_buffer("frt_rope_delta", loop._rope_delta)
|
|
829
|
+
|
|
830
|
+
def forward(self, tok_ids, pos_t):
|
|
831
|
+
# the cache reaches its KV through a plain python object, which
|
|
832
|
+
# the exporter can only lift as an immutable constant; rerouted
|
|
833
|
+
# through this module's own attributes the same tensors carry a
|
|
834
|
+
# buffer source, and the prompt pass's index_copy_ becomes a
|
|
835
|
+
# legal buffer mutation. Outside tracing the getattr returns
|
|
836
|
+
# the very same tensors, so this is an identity rebind — but
|
|
837
|
+
# under fake tracing it leaves proxies in the cache lists,
|
|
838
|
+
# which ``aot_prefill`` restores right after export.
|
|
839
|
+
loop = self._frt_loop
|
|
840
|
+
for i in loop._full:
|
|
841
|
+
loop.cache.key_cache[i] = getattr(self, f"frt_k{i}")
|
|
842
|
+
loop.cache.value_cache[i] = getattr(self, f"frt_v{i}")
|
|
843
|
+
return loop._fwd(tok_ids, pos_t)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def build_decode_loop(model, *, max_len, compile_step=True,
|
|
847
|
+
compile_prefill=True, kv_band=None):
|
|
848
|
+
"""Build the whole-loop form over whatever is attached to ``model``."""
|
|
849
|
+
return WholeStepDecodeLoop(model, max_len=max_len,
|
|
850
|
+
compile_step=compile_step,
|
|
851
|
+
compile_prefill=compile_prefill,
|
|
852
|
+
kv_band=kv_band)
|