Skip to content

Commit a773e68

Browse files
original-el8ds41
andauthored
Prepare NVFP4 draft and Markov heads behind independent flags (#65)
DeepSeek V4 and V4.1 can prepare separate NVFP4 copies of the draft vocabulary head and the Markov transition projection. Each is selected by its own opt-in flag and recipe capability; omitting both keeps checkpoint precision, and the target verification head always keeps its checkpoint precision. Quantized heads run with BF16 activations, and the Markov kernel's BF16 output is widened to FP32 before bias addition. The rebuild record gains optional fields 8 and 9 so a rebuild reproduces the selected head precision instead of silently falling back to the checkpoint. Fields are written only when selected, so existing BF16 manifests remain byte-identical. The export path rejects unknown fields, so the schema must carry every key the manifest writes; the Python bindings are regenerated with protoc 35.1, the version that produced the checked-in file. Tests cover the four draft/Markov combinations through the export schema, the independent flag and plan validation, the Markov projection's BF16-to-FP32 path, and a record written before the fields existed. Focused suites pass; six Kimi-K3 failures pre-exist because the checkpoint fixture is absent. CUDA tests were not run in this environment; they require FLARE_RUN_CUDA_TESTS=1. A paired A/B on four DGX Sparks measured +4.0% coding throughput and flat acceptance (64.6% to 63.8%) for both heads together versus checkpoint precision, with the NVFP4 package noisier across rounds. Claude-Session: https://claude.ai/code/session_01PFTkgqizAoWiQUS1Z3uR9v Design-Signed-Off-By: ds41; design-review=6aeb Co-authored-by: ds41 <noreply@invalid>
1 parent e952c8a commit a773e68

24 files changed

Lines changed: 453 additions & 197 deletions

‎docs/model-preparation.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,16 @@ The three preceding tokens live in continuation state, including reused prefixes
452452
DSpark proposals, target verification, and accepted-state commit use the shared
453453
native speculative contract; Helios contains no DSpark model implementation.
454454

455+
The optional preparation flags `--draft-head-quantization nvfp4` and
456+
`--markov-head-quantization nvfp4` select independent NVFP4 copies of the draft
457+
vocabulary head and Markov output projection. Both use BF16 activations; the
458+
Markov kernel's BF16 output is widened to FP32 before bias addition. Target
459+
verification retains the checkpoint head. Omitting these flags preserves the
460+
checkpoint precision. Each selection requires tuning, capture, and package export;
461+
it cannot change an existing package at serving time. The same options apply to
462+
the DeepSeek V4 Flash recipe. Quantization changes proposal probabilities and can
463+
change acceptance and throughput; measure both separately and together.
464+
455465
## DeepSeek V4 Flash
456466

457467
DeepSeek V4 Flash uses `local-inference-lab/DeepSeek-V4-Flash-Vision-Exp-lossless-CSF`, including its

‎helios/proto/runtime_package.proto‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ message RebuildSpeculation {
107107
uint64 token_capacity = 5;
108108
bool packed_verification = 6;
109109
string draft_prefix_selection = 7;
110+
optional string draft_head_quantization = 8;
111+
optional string markov_head_quantization = 9;
110112
}
111113
// Views of retained prepared weight storage, with no additional payload copies.
112114
// Names are paths in the recipe's weight objects, not original checkpoint keys.

‎ignition/cli/__init__.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ def _preparation_arguments(running):
165165
" excludes mandatory width-one and ordinary graphs. Requires packed adaptive"
166166
" learned speculation (default: see MODEL --help)")
167167
advanced = running.add_argument_group("advanced options")
168+
advanced.add_argument("--draft-head-quantization", choices=("nvfp4",),
169+
help="prepare a separate NVFP4 vocabulary head for learned draft proposals;"
170+
" target verification retains its precision (DeepSeek V4/V4.1)")
171+
advanced.add_argument("--markov-head-quantization", choices=("nvfp4",),
172+
help="prepare the draft Markov transition projection as NVFP4"
173+
" (DeepSeek V4/V4.1; default: checkpoint precision)")
168174
advanced.add_argument("--mla-compute-mode", choices=("auto", "bf16"),
169175
help="constrain MLA arithmetic while retaining packed KV storage and compatible"
170176
" kernel tuning (default: see MODEL --help)")

‎ignition/cli/display.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ def speculation(settings, *, brief: bool = False) -> str:
2929
drafts = settings.draft_tokens
3030
depth = f"{'up to' if settings.adaptive else 'exactly'} {drafts} draft token{'' if drafts == 1 else 's'}"
3131
selection = " · GPU prefix selection" if settings.draft_prefix_selection == "gpu" else ""
32+
heads = [f"{head} head {getattr(settings, head + '_head_quantization').upper()}"
33+
for head in ("draft", "markov") if getattr(settings, head + '_head_quantization') is not None]
34+
precision = " · " + ", ".join(heads) if heads else ""
3235
if brief:
33-
return depth + selection
34-
return f"{depth} per round · MTP module" + selection
36+
return depth + selection + precision
37+
return f"{depth} per round · MTP module" + selection + precision

‎ignition/cli/preparation.py‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ def speculation(args, model: ResolvedModel, configuration: Configuration | None
140140
given = [flag for flag, value in (
141141
("--mtp-draft-tokens", args.mtp_draft_tokens),
142142
("--speculative-token-capacity", args.speculative_token_capacity),
143-
("--no-mtp-adaptive-verification", args.no_mtp_adaptive_verification or None)) if value is not None]
143+
("--no-mtp-adaptive-verification", args.no_mtp_adaptive_verification or None),
144+
("--draft-head-quantization", getattr(args, "draft_head_quantization", None)),
145+
("--markov-head-quantization", getattr(args, "markov_head_quantization", None))) if value is not None]
144146
if explicit_graph_budget:
145147
given.append("--speculative-graph-budget")
146148
if default is None:
@@ -172,8 +174,13 @@ def speculation(args, model: ResolvedModel, configuration: Configuration | None
172174
budget = graph_budget.value if packed else None
173175
if budget is not None and (type(budget) is not int or budget < 1):
174176
raise UsageError("--speculative-graph-budget must be a positive integer")
177+
draft_head = getattr(args, "draft_head_quantization", None)
178+
markov_head = getattr(args, "markov_head_quantization", None)
179+
if (draft_head or markov_head) and drafts < 1:
180+
raise UsageError("draft head quantization requires positive learned draft depth")
175181
return Speculation(drafts, adaptive, token_capacity=capacity, packed_verification=packed,
176-
draft_prefix_selection=prefix_selection, graph_budget=budget)
182+
draft_prefix_selection=prefix_selection, graph_budget=budget,
183+
draft_head_quantization=draft_head, markov_head_quantization=markov_head)
177184

178185

179186
def fetch(model: ResolvedModel, hub_cache, *, progress_bars: bool = True) -> str:

‎ignition/components/dspark.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,25 +225,30 @@ class PreparedMarkov:
225225
226226
A replicated head runs independently on each rank without a collective.
227227
A sharded head uses the supplied runtime to gather its FP32 bits unchanged.
228+
Quantized A16 projections produce BF16 logits, widened before bias addition.
228229
"""
229230

230231
def __init__(self, weights, tokens, *, runtime, coordinator, session, name):
231232
rows, device = tokens.numel(), tokens.device
232233
self.embedding = torch.zeros(
233234
(rows, weights.config.draft.markov_rank), dtype=torch.bfloat16, device=device
234235
)
235-
columns = weights.draft.markov_head.shape[0]
236+
head = weights.draft.markov_head
237+
columns = head.shape[0] if isinstance(head, torch.Tensor) else head.out_features
236238
expected_columns = 129280 if runtime is None else 129280 // weights.shard.world_size
237239
if columns != expected_columns:
238240
raise ValueError("Markov head vocabulary partition must match its gather runtime")
239241
self.local = torch.zeros((rows, columns), dtype=torch.float32, device=device)
242+
self.projected = self.local if isinstance(head, torch.Tensor) else torch.empty(
243+
(rows, columns), dtype=torch.bfloat16, device=device)
240244
self.logits = self.local if runtime is None else torch.zeros((rows, 129280), dtype=torch.float32, device=device)
241245
self.embed = PreparedEmbedding(
242246
weights.draft.markov_embedding, tokens, self.embedding, session=session, name=name + ".embed"
243247
)
244-
self.projection = UnquantizedProjection(
245-
self.embedding, weights.draft.markov_head, self.local, session=session, name=name + ".head"
246-
)
248+
projection = UnquantizedProjection if isinstance(head, torch.Tensor) else QuantizedProjection
249+
options = {} if isinstance(head, torch.Tensor) else {"activation_mode": "a16"}
250+
self.projection = projection(self.embedding, head, self.projected,
251+
session=session, name=name + ".head", **options)
247252
self.gather = None if runtime is None else PreparedAllGather(
248253
self.local.view(torch.bfloat16),
249254
self.logits.view(torch.bfloat16),
@@ -255,6 +260,8 @@ def __init__(self, weights, tokens, *, runtime, coordinator, session, name):
255260

256261
def __call__(self):
257262
self.projection(self.embed())
263+
if self.projected is not self.local:
264+
self.local.copy_(self.projected)
258265
if self.gather is not None:
259266
self.gather(self.local.view(torch.bfloat16))
260267
return self.logits

‎ignition/planning.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ def to_dict(self) -> dict:
302302
"draft_tokens": self.speculation.draft_tokens, "adaptive": self.speculation.adaptive,
303303
"packed_verification": self.speculation.packed_verification,
304304
"draft_prefix_selection": self.speculation.draft_prefix_selection,
305+
"draft_head_quantization": self.speculation.draft_head_quantization,
306+
"markov_head_quantization": self.speculation.markov_head_quantization,
305307
**({"graph_budget": self.speculation.graph_budget}
306308
if self.speculation.graph_budget is not None else {})},
307309
"context": self.context, "checkpoint_context": self.recipe.context,
@@ -341,6 +343,12 @@ def fit(recipe: Recipe, checkpoint: Checkpoint, gpus: tuple[Gpu, ...], *, concur
341343
if mla_compute_mode not in recipe.mla_compute_modes:
342344
raise UsageError(f"--mla-compute-mode {mla_compute_mode}: supported values for this recipe are "
343345
+ ", ".join(recipe.mla_compute_modes))
346+
if speculation is not None:
347+
for head in ("draft", "markov"):
348+
value = getattr(speculation, f"{head}_head_quantization")
349+
if value is not None and value not in getattr(recipe, f"{head}_head_quantizations"):
350+
raise UsageError(f"--{head}-head-quantization {value}: {checkpoint.alias}"
351+
" does not prepare this proposal head format")
344352
if expert_scale_compression is not None and expert_scale_compression not in checkpoint.scale_compressed_device_bytes:
345353
raise UsageError(f"--expert-scale-compression {expert_scale_compression}: {checkpoint.alias}"
346354
" does not prepare this scale encoding")

‎ignition/preparation/rebuild_manifest.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ def from_plan(model, plan, *, compiled_forwards):
3131
adaptive=s.adaptive,
3232
token_capacity=plan.speculative_token_capacity, packed_verification=s.packed_verification,
3333
draft_prefix_selection=s.draft_prefix_selection)
34+
# Record proposal-head precision only when it differs from the checkpoint
35+
# default, so existing BF16 manifests stay byte-identical.
36+
for field in ('draft_head_quantization', 'markov_head_quantization'):
37+
if (value := getattr(s, field)) is not None:
38+
result['speculation'][field] = value
3439
return result
3540

3641

‎ignition/preparation/runtime_package_pb2.py‎

Lines changed: 170 additions & 170 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ignition/recipes/declaration.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,15 @@ class Speculation:
108108
packed_verification: bool = False
109109
draft_prefix_selection: str = "cpu"
110110
graph_budget: int | None = None
111+
draft_head_quantization: str | None = None
112+
markov_head_quantization: str | None = None
111113

112114
def __post_init__(self):
115+
for value in (self.draft_head_quantization, self.markov_head_quantization):
116+
if value not in (None, "nvfp4"):
117+
raise ValueError("draft head quantization must be omitted or nvfp4")
118+
if value is not None and self.draft_tokens < 1:
119+
raise ValueError("draft head quantization requires positive learned draft depth")
113120
if self.graph_budget is not None:
114121
if type(self.graph_budget) is not int or self.graph_budget < 1:
115122
raise ValueError("speculative graph budget must be a positive integer")
@@ -209,6 +216,9 @@ class Recipe:
209216
mla_compute_mode: str = "auto"
210217
expert_scale_compression: str | None = None
211218
export_repository: str = ""
219+
# Preparation overrides for vocabulary and transition projections used only by proposals.
220+
draft_head_quantizations: tuple[str, ...] = ()
221+
markov_head_quantizations: tuple[str, ...] = ()
212222

213223
def mixed_shapes(self, concurrency, chunk, speculation):
214224
from ignition.components.capacity import mixed_shapes

0 commit comments

Comments
 (0)