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.
Files changed (122) hide show
  1. flashrt_structures/__init__.py +174 -0
  2. flashrt_structures/adapters/__init__.py +55 -0
  3. flashrt_structures/adapters/diffusers_attention.py +237 -0
  4. flashrt_structures/adapters/diffusers_rotary_attention.py +239 -0
  5. flashrt_structures/adapters/factored_qk_norm_rope.py +252 -0
  6. flashrt_structures/adapters/factored_two_way_attention.py +99 -0
  7. flashrt_structures/adapters/gemma_attention.py +226 -0
  8. flashrt_structures/adapters/packed_qkv_rope.py +342 -0
  9. flashrt_structures/adapters/packed_stream_qk_norm_rope.py +376 -0
  10. flashrt_structures/adapters/qwen_per_head_qk_norm_rope.py +231 -0
  11. flashrt_structures/adapters/sglang_engine.py +207 -0
  12. flashrt_structures/adapters/transformers_attention_interface.py +73 -0
  13. flashrt_structures/adapters/transformers_gated_delta.py +152 -0
  14. flashrt_structures/adapters/transformers_gated_delta_fused.py +96 -0
  15. flashrt_structures/adapters/vllm_engine.py +424 -0
  16. flashrt_structures/adjudicate.py +85 -0
  17. flashrt_structures/aot.py +191 -0
  18. flashrt_structures/autobuild.py +2052 -0
  19. flashrt_structures/beta/__init__.py +43 -0
  20. flashrt_structures/beta/conform.py +94 -0
  21. flashrt_structures/beta/joins.py +113 -0
  22. flashrt_structures/beta/negotiate.py +84 -0
  23. flashrt_structures/beta/ports.py +140 -0
  24. flashrt_structures/decisions.py +80 -0
  25. flashrt_structures/discover.py +623 -0
  26. flashrt_structures/explain.py +70 -0
  27. flashrt_structures/frontdoor.py +572 -0
  28. flashrt_structures/gates.py +465 -0
  29. flashrt_structures/guard.py +421 -0
  30. flashrt_structures/handle.py +189 -0
  31. flashrt_structures/impls/__init__.py +219 -0
  32. flashrt_structures/impls/adaln_producer/__init__.py +8 -0
  33. flashrt_structures/impls/adaln_producer/broker.py +116 -0
  34. flashrt_structures/impls/adaln_producer/fused.py +388 -0
  35. flashrt_structures/impls/adarms_stack/__init__.py +8 -0
  36. flashrt_structures/impls/adarms_stack/fp8_chain.py +832 -0
  37. flashrt_structures/impls/adarms_stack/region.py +102 -0
  38. flashrt_structures/impls/attention_core/__init__.py +132 -0
  39. flashrt_structures/impls/attention_core/fa2_seqused.py +458 -0
  40. flashrt_structures/impls/attention_core/fa4_cute.py +154 -0
  41. flashrt_structures/impls/attention_core/fa4_fp8.py +178 -0
  42. flashrt_structures/impls/attention_core/masked_mha.py +158 -0
  43. flashrt_structures/impls/attention_core/two_way_fa2.py +220 -0
  44. flashrt_structures/impls/cadence_static/__init__.py +18 -0
  45. flashrt_structures/impls/cadence_static/buffers.py +122 -0
  46. flashrt_structures/impls/cadence_static/cross_attention.py +187 -0
  47. flashrt_structures/impls/chain_elements.py +89 -0
  48. flashrt_structures/impls/decode_loop/__init__.py +0 -0
  49. flashrt_structures/impls/decode_loop/fp8_kv.py +206 -0
  50. flashrt_structures/impls/decode_loop/mtp_speculative.py +245 -0
  51. flashrt_structures/impls/decode_loop/whole_step.py +852 -0
  52. flashrt_structures/impls/decoder_block/__init__.py +6 -0
  53. flashrt_structures/impls/decoder_block/attn_sublayer.py +110 -0
  54. flashrt_structures/impls/decoder_block/fused.py +167 -0
  55. flashrt_structures/impls/decoder_ffn/__init__.py +0 -0
  56. flashrt_structures/impls/decoder_ffn/fp8_static.py +310 -0
  57. flashrt_structures/impls/decoder_ffn/fp8_static.yaml +22 -0
  58. flashrt_structures/impls/decoder_ffn/w4a16_static.py +221 -0
  59. flashrt_structures/impls/decoder_ffn/w8a16_static.py +183 -0
  60. flashrt_structures/impls/dit_stack/__init__.py +11 -0
  61. flashrt_structures/impls/dit_stack/fp4_chain.py +417 -0
  62. flashrt_structures/impls/dit_stack/region.py +86 -0
  63. flashrt_structures/impls/fixed_iter/__init__.py +29 -0
  64. flashrt_structures/impls/fixed_iter/openpi.py +264 -0
  65. flashrt_structures/impls/fixed_iter/protocol.py +94 -0
  66. flashrt_structures/impls/gated_delta_core/__init__.py +3 -0
  67. flashrt_structures/impls/gated_delta_core/fused_layer.py +545 -0
  68. flashrt_structures/impls/gated_delta_core/hub_v3.py +152 -0
  69. flashrt_structures/impls/graph_lowering/__init__.py +27 -0
  70. flashrt_structures/impls/graph_lowering/pi052_denoise.py +179 -0
  71. flashrt_structures/impls/graph_lowering/protocol.py +76 -0
  72. flashrt_structures/impls/graph_lowering/qwen3_vl.py +364 -0
  73. flashrt_structures/impls/linear_proj/__init__.py +0 -0
  74. flashrt_structures/impls/linear_proj/fp8_static.py +270 -0
  75. flashrt_structures/impls/linear_proj/nvfp4_balance.py +131 -0
  76. flashrt_structures/impls/linear_proj/nvfp4_dynamic.py +182 -0
  77. flashrt_structures/impls/linear_proj/w8a16_static.py +230 -0
  78. flashrt_structures/impls/modnorm_qkv_chain/__init__.py +0 -0
  79. flashrt_structures/impls/modnorm_qkv_chain/fp8_ptok_table.py +291 -0
  80. flashrt_structures/impls/moe_experts/__init__.py +9 -0
  81. flashrt_structures/impls/moe_experts/nvfp4_dynamic.py +208 -0
  82. flashrt_structures/impls/moe_experts/nvfp4_w4a16.py +129 -0
  83. flashrt_structures/impls/norm_fused/__init__.py +3 -0
  84. flashrt_structures/impls/norm_fused/bf16.py +94 -0
  85. flashrt_structures/impls/norm_fused/fp8_producer.py +84 -0
  86. flashrt_structures/impls/patch_projection/__init__.py +3 -0
  87. flashrt_structures/impls/patch_projection/bf16_flat.py +139 -0
  88. flashrt_structures/impls/prefill_tower/__init__.py +10 -0
  89. flashrt_structures/impls/prefill_tower/fp8_chain.py +955 -0
  90. flashrt_structures/impls/prefill_tower/region.py +99 -0
  91. flashrt_structures/impls/qk_norm_rope/__init__.py +12 -0
  92. flashrt_structures/impls/qk_norm_rope/per_head_gqa.py +199 -0
  93. flashrt_structures/impls/qk_norm_rope/projection_bf16.py +165 -0
  94. flashrt_structures/impls/qkv_pack/__init__.py +5 -0
  95. flashrt_structures/impls/qkv_pack/bf16.py +110 -0
  96. flashrt_structures/impls/qkv_pack/fp8_static.py +435 -0
  97. flashrt_structures/impls/qkv_pack/nvfp4_balance.py +218 -0
  98. flashrt_structures/impls/qkv_rope/__init__.py +3 -0
  99. flashrt_structures/impls/qkv_rope/packed_bias_bf16.py +143 -0
  100. flashrt_structures/impls/step_table.py +113 -0
  101. flashrt_structures/impls/vision_ffn/__init__.py +0 -0
  102. flashrt_structures/impls/vision_ffn/fp8_static.py +261 -0
  103. flashrt_structures/impls/vision_ffn/nvfp4_balance.py +211 -0
  104. flashrt_structures/impls/vision_tower/__init__.py +7 -0
  105. flashrt_structures/impls/vision_tower/fp8_chain.py +533 -0
  106. flashrt_structures/impls/vision_tower/region.py +91 -0
  107. flashrt_structures/matrix.py +126 -0
  108. flashrt_structures/points.py +368 -0
  109. flashrt_structures/prequantized.py +131 -0
  110. flashrt_structures/quantize_on_adopt.py +94 -0
  111. flashrt_structures/recipe.py +438 -0
  112. flashrt_structures/regions.py +208 -0
  113. flashrt_structures/schemes.py +490 -0
  114. flashrt_structures/stages.py +298 -0
  115. flashrt_structures/storage.py +255 -0
  116. flashrt_structures/swap.py +398 -0
  117. flashrt_structures/workspace.py +110 -0
  118. flashrt_structures-0.2.0.dist-info/METADATA +140 -0
  119. flashrt_structures-0.2.0.dist-info/RECORD +122 -0
  120. flashrt_structures-0.2.0.dist-info/WHEEL +5 -0
  121. flashrt_structures-0.2.0.dist-info/licenses/LICENSE +202 -0
  122. flashrt_structures-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,206 @@
1
+ """FP8-KV decode band for the whole-step loop.
2
+
3
+ Attention reads dominate a deep-context decode step, and the XQA entry
4
+ reads its KV as FP8 pages — half the bytes of the BF16 cache the host's
5
+ SDPA walks. The loop owns both ends of the attention data path (its
6
+ static cache writes the KV, its registered attention interface consumes
7
+ it), so the band slots in without touching the host layer: ``update``
8
+ keeps returning the BF16 tensors the prefill path needs and *also*
9
+ quantises the same rows into the paged FP8 store; the interface routes
10
+ short query batches (the decode row, a spec verify) through XQA and
11
+ everything else back to SDPA on the BF16 arm.
12
+
13
+ v1 is dual-store: the BF16 cache stays for prefill and fallback, so
14
+ the win is attention read bandwidth at depth, not memory — dropping
15
+ the BF16 store rides on an FP8 prefill attention, recorded follow-up.
16
+ The page shape is the kernel's v1 contract (24 query heads, 4 KV
17
+ heads, head dim 256); other profiles refuse cleanly at bind.
18
+
19
+ Graph discipline: seq_lens is a device buffer written in-graph, the
20
+ per-shape spec masks and the workspace are allocated at first use
21
+ (warmup) and never repointed, and the FP8 row writes are index_copy_
22
+ into stable pages — everything a captured replay requires.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from functools import lru_cache
28
+
29
+ import torch
30
+
31
+ KERNEL_DEP = {
32
+ "provider": "huggingface_kernels",
33
+ "repo": "flashrt/fp8-kv-attention",
34
+ "version": ">=1",
35
+ }
36
+
37
+ #: the kernel's v1 fixed profile
38
+ _QH, _KVH, _HD, _PAGE = 24, 4, 256, 128
39
+
40
+ #: query batches at or under this route through XQA; longer batches
41
+ #: (prompts) keep the BF16 SDPA arm
42
+ _XQA_MAX_Q = 32
43
+
44
+ _INTERFACE_NAME = "frt_fp8kv"
45
+
46
+
47
+ @lru_cache(maxsize=1)
48
+ def _kernel():
49
+ from flashrt_structures.impls import hub_kernel
50
+
51
+ return hub_kernel(KERNEL_DEP["repo"], KERNEL_DEP["version"])
52
+
53
+
54
+ class Fp8KvBand:
55
+ """Paged FP8 KV store plus the XQA call state for one loop."""
56
+
57
+ def __init__(self, attn_layers, max_len, device):
58
+ kern = _kernel()
59
+ self._kern = kern
60
+ self._max = int(max_len)
61
+ pages = (self._max + _PAGE - 1) // _PAGE
62
+ # the kernel's max_seq_len speaks in whole pages
63
+ self._max_paged = pages * _PAGE
64
+ f8 = torch.float8_e4m3fn
65
+ self.k_pages = {i: torch.zeros(pages, _PAGE, _KVH, _HD,
66
+ device=device, dtype=f8)
67
+ for i in attn_layers}
68
+ self.v_pages = {i: torch.zeros_like(self.k_pages[i])
69
+ for i in attn_layers}
70
+ self._seq = torch.zeros(1, 1, device=device, dtype=torch.int32)
71
+ s_mb = 64 # the default 256MB scratch tips a full band
72
+ # form over the card rim; 64MB serves the v1 shapes
73
+ self._sem, self._scratch = kern.allocate_workspace(
74
+ q_seq=_XQA_MAX_Q, device=device, scratch_mb=s_mb)
75
+ self._masks: dict[int, torch.Tensor] = {}
76
+ self._table = kern.default_page_table(pages, device=device)
77
+
78
+ def write(self, layer_idx, k, v, pos):
79
+ """Quantise post-rope rows into the pages at ``pos``."""
80
+ kp = self.k_pages.get(layer_idx)
81
+ if kp is None:
82
+ return
83
+ # eager ATen has no fp8 index_copy kernel (the compiled path
84
+ # codegens around it); the byte view is the same write
85
+ rows_k = k[0].transpose(0, 1).to(kp.dtype).view(torch.uint8)
86
+ rows_v = v[0].transpose(0, 1).to(kp.dtype).view(torch.uint8)
87
+ kp.view(torch.uint8).view(-1, _KVH, _HD).index_copy_(
88
+ 0, pos, rows_k)
89
+ self.v_pages[layer_idx].view(torch.uint8).view(
90
+ -1, _KVH, _HD).index_copy_(0, pos, rows_v)
91
+
92
+ def reset(self):
93
+ """Zero the pages: a fresh prompt must not see the previous
94
+ stream's rows. The read path is page-granular in the kernel,
95
+ so rows beyond seq_len are reachable garbage unless cleared —
96
+ the repeat gate caught exactly that leak."""
97
+ for kp in self.k_pages.values():
98
+ kp.view(torch.uint8).zero_()
99
+ for vp in self.v_pages.values():
100
+ vp.view(torch.uint8).zero_()
101
+
102
+ def prewarm(self, shapes):
103
+ """Materialise the per-shape masks ahead of compiled use: a
104
+ lazy build inside a compiled region flips a dynamo guard
105
+ between warmup and capture, and recompiling while a stream is
106
+ capturing is illegal."""
107
+ dev = self._table.device
108
+ for s in shapes:
109
+ if s not in self._masks:
110
+ self._masks[s] = self._kern.causal_spec_mask(
111
+ int(s), device=dev)
112
+
113
+ def clear_rows(self, pos):
114
+ """Zero the page rows at ``pos`` across every layer.
115
+
116
+ The warmup steps before capture write real rows past the
117
+ prompt; the read path is page-granular, so a later replay
118
+ from the rolled-back position can still reach them. Clearing
119
+ restores the exact page state a fresh call would see."""
120
+ for kp in self.k_pages.values():
121
+ kp.view(torch.uint8).view(-1, _KVH, _HD).index_fill_(
122
+ 0, pos, 0)
123
+ for vp in self.v_pages.values():
124
+ vp.view(torch.uint8).view(-1, _KVH, _HD).index_fill_(
125
+ 0, pos, 0)
126
+
127
+ def set_len(self, total):
128
+ """Total sequence length (device tensor or int), in-graph safe."""
129
+ if torch.is_tensor(total):
130
+ self._seq.view(-1).copy_(total.view(-1).to(torch.int32))
131
+ else:
132
+ self._seq.fill_(int(total))
133
+
134
+ def attend(self, layer_idx, q):
135
+ """``q`` is [1, qh, S, hd] post-rope; returns [1, S, qh, hd]."""
136
+ s = q.shape[2]
137
+ # the kernel wrapper self-builds a host-side mask when none is
138
+ # passed - illegal inside a capture - so every shape's mask is
139
+ # built once here (warmup) and replayed as a device constant
140
+ mask = self._masks.get(s)
141
+ if mask is None:
142
+ mask = self._kern.causal_spec_mask(s, device=q.device)
143
+ self._masks[s] = mask
144
+ out = self._kern.xqa_bf16_fp8kv(
145
+ q[0].transpose(0, 1).contiguous(),
146
+ self.k_pages[layer_idx], self.v_pages[layer_idx],
147
+ page_table=self._table, seq_lens=self._seq, mask=mask,
148
+ semaphores=self._sem, scratch=self._scratch,
149
+ max_seq_len=self._max_paged)
150
+ return out.view(1, s, _QH, _HD)
151
+
152
+
153
+ def _interface(module, q, k, v, attention_mask, scaling=None, **kwargs):
154
+ band = getattr(module, "_frt_fp8_band", None)
155
+ # the band serves exactly the loop whose cache filled its pages;
156
+ # the loop's static cache hands attention a full-window K (its
157
+ # second-to-last dim is the window), a host-side DynamicCache hands
158
+ # a growing one — that shape is the ownership signature, and host
159
+ # forwards fall through to plain SDPA untouched
160
+ if band is not None and q.shape[0] == 1 \
161
+ and q.shape[2] <= _XQA_MAX_Q \
162
+ and k.shape[2] == band._max:
163
+ return band.attend(module.layer_idx, q), None
164
+ if attention_mask is None and q.shape[2] > 1 \
165
+ and k.shape[2] > q.shape[2]:
166
+ # maskless prompt rows over the full static window: causal
167
+ # rows 0..S-1 never see columns past S, and the square slice
168
+ # keeps SDPA on its fused causal path — the rectangular case
169
+ # falls to the math backend, which materialises an [S, window]
170
+ # score matrix (gigabytes at deep windows)
171
+ k = k[:, :, :q.shape[2]]
172
+ v = v[:, :, :q.shape[2]]
173
+ # the fall-through must be the host's own sdpa interface, bit for
174
+ # bit — a lookalike SDPA call differs in repeat/contiguity details
175
+ # and a detached model would stop reproducing its own baseline
176
+ from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
177
+
178
+ return ALL_ATTENTION_FUNCTIONS["sdpa"](
179
+ module, q, k, v, attention_mask, scaling=scaling, **kwargs)
180
+
181
+
182
+ def install(model, lm, cache, max_len):
183
+ """Attach the band to a loop: pages on the cache, interface on the
184
+ host's attention dispatch, handles on the attention modules.
185
+
186
+ Refuses (returns ``None``) when the host profile is not the
187
+ kernel's v1 contract — the loop simply keeps its BF16 attention.
188
+ """
189
+ cfg = getattr(model.config, "text_config", model.config)
190
+ if (int(cfg.num_attention_heads), int(cfg.num_key_value_heads),
191
+ int(getattr(cfg, "head_dim", 0))) != (_QH, _KVH, _HD):
192
+ return None
193
+ from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS
194
+
195
+ if _INTERFACE_NAME not in ALL_ATTENTION_FUNCTIONS:
196
+ ALL_ATTENTION_FUNCTIONS.register(_INTERFACE_NAME, _interface)
197
+ attn_layers = [i for i, lyr in enumerate(lm.layers)
198
+ if hasattr(lyr, "self_attn")]
199
+ dev = lm.embed_tokens.weight.device
200
+ band = Fp8KvBand(attn_layers, max_len, dev)
201
+ for i in attn_layers:
202
+ lm.layers[i].self_attn._frt_fp8_band = band
203
+ lm.layers[i].self_attn.config._attn_implementation = \
204
+ _INTERFACE_NAME
205
+ cache.frt_fp8_band = band
206
+ return band
@@ -0,0 +1,245 @@
1
+ """MTP speculative decode — the decode_loop family's second member.
2
+
3
+ Checkpoints in this family ship a one-layer DeepSeek-style draft head
4
+ (``mtp.safetensors``) that transformers hosts never use. This member
5
+ loads it, assembles the draft from the host's own module classes, and
6
+ runs the draft/verify loop around the whole-step form. Greedy spec
7
+ decode is exact by construction: the verify pass recomputes every
8
+ draft token with the main model, so the accepted stream is identical
9
+ to plain greedy decode — the gate checks token identity, not a band.
10
+
11
+ The draft head is carried in BF16 (its FP8 blocks are dequantised at
12
+ load); its attention layer gets one extra slot in the static cache.
13
+ The gated-delta states cannot roll back through a rejected suffix, so
14
+ the loop snapshots them before each verify and re-advances the
15
+ accepted prefix from the snapshot when a draft is cut short.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import torch
21
+
22
+
23
+ def _load_mtp_tensors(ckpt_dir):
24
+ """The draft's tensors, ``mtp.`` prefix stripped.
25
+
26
+ Two shipping forms: a sidecar ``mtp.safetensors``, or ``mtp.*`` keys
27
+ inside the main sharded checkpoint (the MoE hosts ship this way) —
28
+ the index says which shards carry them.
29
+ """
30
+ import json
31
+ import pathlib
32
+
33
+ from safetensors import safe_open
34
+
35
+ d = pathlib.Path(str(ckpt_dir))
36
+ side = d / "mtp.safetensors"
37
+ if side.is_file():
38
+ f = safe_open(str(side), "pt")
39
+ return {k[len("mtp."):]: f.get_tensor(k) for k in f.keys()}
40
+ idx_path = d / "model.safetensors.index.json"
41
+ if not idx_path.is_file():
42
+ raise ValueError(
43
+ f"refused: {d} carries neither mtp.safetensors nor a "
44
+ "sharded index with mtp.* keys")
45
+ wmap = json.loads(idx_path.read_text())["weight_map"]
46
+ by_shard: dict[str, list[str]] = {}
47
+ for key, shard in wmap.items():
48
+ if key.startswith("mtp."):
49
+ by_shard.setdefault(shard, []).append(key)
50
+ if not by_shard:
51
+ raise ValueError(
52
+ f"refused: this checkpoint's index carries no mtp.* keys "
53
+ "(the host ships no draft head)")
54
+ t = {}
55
+ for shard, keys in by_shard.items():
56
+ f = safe_open(str(d / shard), "pt")
57
+ for k in keys:
58
+ t[k[len("mtp."):]] = f.get_tensor(k)
59
+ return t
60
+
61
+
62
+ def _dequant_block_fp8(w, scale_inv):
63
+ n, k = w.shape
64
+ bn, bk = n // scale_inv.shape[0], k // scale_inv.shape[1]
65
+ wf = w.float().view(scale_inv.shape[0], bn, scale_inv.shape[1], bk)
66
+ wf = wf * scale_inv.float().view(scale_inv.shape[0], 1,
67
+ scale_inv.shape[1], 1)
68
+ return wf.view(n, k).to(torch.bfloat16)
69
+
70
+
71
+ #: the draft's precision axes and their measured arms. The draft answers
72
+ #: to acceptance length alone — greedy spec output is anchored by the
73
+ #: verify pass either way — so both axes may trade precision for cost,
74
+ #: and the defaults are the measured sweet spot on the record: a private
75
+ #: W8 view of the shared head (the model's own head must not change —
76
+ #: step and verify share its numeric family) and the BF16 expert bank
77
+ #: (the FP4 bank measured AL-equal; BF16 is the conservative default).
78
+ DRAFT_FORMATS = {
79
+ "head": ("w8a16_static", "host"),
80
+ "experts": ("bf16", "nvfp4_dynamic"),
81
+ }
82
+
83
+
84
+ def check_draft_formats(head_format: str, experts_format: str) -> None:
85
+ if head_format not in DRAFT_FORMATS["head"]:
86
+ raise ValueError(
87
+ f"refused: unknown draft head format {head_format!r}; "
88
+ f"measured arms: {', '.join(DRAFT_FORMATS['head'])}")
89
+ if experts_format not in DRAFT_FORMATS["experts"]:
90
+ raise ValueError(
91
+ f"refused: unknown draft experts format {experts_format!r}; "
92
+ f"measured arms: {', '.join(DRAFT_FORMATS['experts'])}")
93
+
94
+
95
+ class _GatherExpertsBf16(torch.nn.Module):
96
+ """BF16 expert bank behind the host contract, sync-free.
97
+
98
+ Routed slots gather device-side and run as one batched matmul per
99
+ projection — the same fixed-shape, host-silent step the packed form
100
+ takes, at the bank's own precision. Sized for a draft layer: one
101
+ layer's bank, a few MB per routed slot.
102
+ """
103
+
104
+ def __init__(self, gate_up, down, act_fn):
105
+ super().__init__()
106
+ self.register_buffer("_gu", gate_up.contiguous())
107
+ self.register_buffer("_dn", down.contiguous())
108
+ self._act = act_fn
109
+
110
+ def forward(self, hidden_states, top_k_index, top_k_weights):
111
+ t, h = hidden_states.shape
112
+ k = top_k_index.shape[1]
113
+ flat = top_k_index.reshape(-1)
114
+ gu = self._gu.index_select(0, flat) # [T*k, 2I, H]
115
+ dn = self._dn.index_select(0, flat) # [T*k, H, I]
116
+ x = hidden_states.unsqueeze(1).expand(t, k, h).reshape(t * k, 1, h)
117
+ y = torch.bmm(x, gu.transpose(1, 2)) # [T*k, 1, 2I]
118
+ gate, up = y.chunk(2, dim=-1)
119
+ inter = self._act(gate) * up
120
+ d = torch.bmm(inter, dn.transpose(1, 2)) # [T*k, 1, H]
121
+ out = (d.view(t, k, h).float()
122
+ * top_k_weights[..., None].float()).sum(dim=1)
123
+ return out.to(hidden_states.dtype)
124
+
125
+
126
+ class MtpDraftHead(torch.nn.Module):
127
+ """fc + one host-class decoder layer + norms; embed/head shared."""
128
+
129
+ def __init__(self, model, lm, ckpt_dir, layer_slot: int, *,
130
+ head_format: str = "w8a16_static",
131
+ experts_format: str = "bf16"):
132
+ super().__init__()
133
+ check_draft_formats(head_format, experts_format)
134
+
135
+ cfg = getattr(model.config, "text_config", model.config)
136
+ full_idx = next(i for i, t in enumerate(cfg.layer_types)
137
+ if t == "full_attention")
138
+ layer_cls = type(lm.layers[full_idx])
139
+ norm_cls = type(lm.norm)
140
+ hidden = int(cfg.hidden_size)
141
+ dev = lm.norm.weight.device
142
+
143
+ t = _load_mtp_tensors(ckpt_dir)
144
+
145
+ # assemble on CPU, move once — the draft loads while the host
146
+ # still has headroom, and never doubles on the device
147
+ self.layer = layer_cls(cfg, full_idx).to(torch.bfloat16)
148
+ pre = "layers.0."
149
+ with torch.no_grad():
150
+ for name, p in self.layer.named_parameters():
151
+ w = t.get(pre + name)
152
+ if w is None:
153
+ continue
154
+ s = t.get(pre + name + "_scale_inv")
155
+ w = (_dequant_block_fp8(w, s) if s is not None
156
+ else w.to(torch.bfloat16))
157
+ p.copy_(w)
158
+ self.layer = self.layer.to(dev)
159
+ # a draft layer whose MLP is an expert bank must run a
160
+ # gather-then-fixed-shape form: the draft chain is captured,
161
+ # and the host bank's routed loop syncs the host. The bank
162
+ # stays BF16 — the scheme's draft default — because the
163
+ # draft's whole value is its acceptance length, and BF16
164
+ # gathers capture just as well as packed ones (the draft is
165
+ # one layer; a routed slot is a handful of MB).
166
+ mlp = getattr(self.layer, "mlp", None)
167
+ bank = getattr(mlp, "experts", None) if mlp is not None else None
168
+ self.formats = {"head": "host", "experts": None}
169
+ if bank is not None and hasattr(bank, "gate_up_proj") \
170
+ and torch.is_tensor(bank.gate_up_proj) \
171
+ and bank.gate_up_proj.dim() == 3:
172
+ if experts_format == "nvfp4_dynamic":
173
+ from ..moe_experts.nvfp4_dynamic import (
174
+ bind_experts_seam)
175
+ mlp.experts, self.experts_conversion = \
176
+ bind_experts_seam(
177
+ {"gate_up_proj": bank.gate_up_proj.detach(),
178
+ "down_proj": bank.down_proj.detach()},
179
+ bank.act_fn)
180
+ else:
181
+ mlp.experts = _GatherExpertsBf16(
182
+ bank.gate_up_proj.detach().to(dev),
183
+ bank.down_proj.detach().to(dev), bank.act_fn)
184
+ self.formats["experts"] = experts_format
185
+ self.fc = torch.nn.Linear(2 * hidden, hidden, bias=False,
186
+ device=dev, dtype=torch.bfloat16)
187
+ self.fc.weight.copy_(t["fc.weight"].to(torch.bfloat16))
188
+ self.norm_h = norm_cls(hidden).to(dev, torch.bfloat16)
189
+ self.norm_h.weight.copy_(
190
+ t["pre_fc_norm_hidden.weight"].to(torch.bfloat16))
191
+ self.norm_e = norm_cls(hidden).to(dev, torch.bfloat16)
192
+ self.norm_e.weight.copy_(
193
+ t["pre_fc_norm_embedding.weight"].to(torch.bfloat16))
194
+ self.norm_out = norm_cls(hidden).to(dev, torch.bfloat16)
195
+ self.norm_out.weight.copy_(t["norm.weight"].to(torch.bfloat16))
196
+ self.slot = int(layer_slot)
197
+ self._embed = lm.embed_tokens
198
+ self._rotary = lm.rotary_emb
199
+ # the draft carries its own W8 view of the shared head: a draft
200
+ # step pays the full-vocab projection every token, its precision
201
+ # is judged by acceptance length alone, and the model's own head
202
+ # must NOT change — the step and the verify pass share that one,
203
+ # and splitting their numeric family at the logits is what a
204
+ # W8-swapped model head was measured to do
205
+ self._head = model.lm_head
206
+ if head_format == "w8a16_static" \
207
+ and isinstance(model.lm_head, torch.nn.Linear):
208
+ try:
209
+ from ..linear_proj import w8a16_static
210
+ self._head = w8a16_static.bind_proj_seam(
211
+ {"w": model.lm_head.weight.detach()},
212
+ original=model.lm_head)
213
+ self.formats["head"] = "w8a16_static"
214
+ except (ValueError, RuntimeError):
215
+ self._head = model.lm_head
216
+ self.eval()
217
+
218
+ @torch.no_grad()
219
+ def forward(self, prev_h, tok_ids, pos_t, cache, mask_row):
220
+ """(logits, h_out) at ``pos_t``; writes the draft's KV slot."""
221
+ e = self._embed(tok_ids)
222
+ h = self.fc(torch.cat([self.norm_e(e), self.norm_h(prev_h)],
223
+ dim=-1))
224
+ cache._cp = pos_t
225
+ h = self.layer(h, position_embeddings=self._rotary(
226
+ h, pos_t.view(1, -1)),
227
+ attention_mask=mask_row,
228
+ past_key_values=_SlotView(cache, self.slot),
229
+ use_cache=True, cache_position=pos_t)
230
+ h = self.norm_out(h)
231
+ return self._head(h), h
232
+
233
+
234
+ class _SlotView:
235
+ """Route the draft layer's cache traffic to its private slot."""
236
+
237
+ def __init__(self, cache, slot):
238
+ self._c = cache
239
+ self._s = slot
240
+
241
+ def update(self, k, v, layer_idx, cache_kwargs=None):
242
+ return self._c.update(k, v, self._s, cache_kwargs)
243
+
244
+ def __getattr__(self, name):
245
+ return getattr(self._c, name)