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,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)
|