Skip to content

[Model] Add Inkling multi-depth MTP support [5/N] - #48768

Closed
WoosukKwon wants to merge 1 commit into
mainfrom
tml-inkling
Closed

[Model] Add Inkling multi-depth MTP support [5/N]#48768
WoosukKwon wants to merge 1 commit into
mainfrom
tml-inkling

Conversation

@WoosukKwon

@WoosukKwon WoosukKwon commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

This draft now contains the multi-depth MTP work that remains after the following model slices landed:

Additional independent changes were extracted into draft PRs:

The standalone QKVR preparation benchmark was removed.

The remaining model changes:

  • Extend the merged MTP=1 implementation to multiple checkpoint depths.
  • Add the multi-step speculative prefill, verification, and KV-cache plumbing required by MTP>1.
  • Retain the checked Rust multimodal placeholder-token conversion.

The core model, LoRA support, MTP=1 foundation, breakable CUDA graphs, and Hopper attention support are no longer introduced by this diff.

Duplicate-work check

This does not duplicate another open PR. It is rebased directly onto current main and builds on the merged slices #48799, #48822, #48858, and #48869. The changes shown here are specifically the delta from one draft depth to multi-depth MTP. LoRA is isolated in #48884, and the unrelated infrastructure changes are isolated in #48880 and #48881.

Validation

  • .venv/bin/python -m pytest tests/config/test_speculative_draft_hf_overrides.py tests/models/inkling/test_mtp_input_fusion.py -v: 38 passed
  • git diff --name-only -z origin/main...HEAD | xargs -0 .venv/bin/pre-commit run --files: passed, including Python 3.10 mypy and Rust formatting
  • git diff --check origin/main...HEAD: passed

Model evaluations

The source PR reports the following evaluations for the complete Inkling implementation. These were not rerun while extracting the multi-depth MTP delta.

Benchmark / metric vLLM NVFP4 Reference NVFP4 Delta vs Reference
MMAU overall 76.10% (761/1,000) 75.50% +0.60 pp
BFCL exact calls 78.61% (1,062/1,351) 78.16% +0.45 pp
BFCL All-Live macro 75.86% 73.54% +2.32 pp
MMMU-Pro overall micro 71.12% (3,691/5,190) 70.52% (3,660/5,190) +0.60 pp
MMMU-Pro Standard 10-option 70.23% (1,215/1,730) 70.00% (1,211/1,730) +0.23 pp
MMMU-Pro Standard 4-option 76.47% (1,323/1,730) 76.30% (1,320/1,730) +0.17 pp
MMMU-Pro Vision 66.65% (1,153/1,730) 65.26% (1,129/1,730) +1.39 pp
HLE 29.33% (633/2,158) 26.65% +2.68 pp
NIAH (2K-221K) 99.09% (436/440) 99.09% (436/440) 0.00 pp
NIAH (294K-513K) 95.68% (421/440) 96.82% (426/440) -1.14 pp
NIAH (586K-805K) 81.36% (358/440) 84.09% (370/440) -2.73 pp
NIAH (878K) 70.91% (78/110) 80.91% (89/110) -10.00 pp

AI assistance

AI assistance (OpenAI Codex) was used to split and rebase the original branch, preserve contributor attribution, isolate LoRA from multi-depth MTP, run validation, and update this draft PR. The human submitter must review every changed line, understand the change end-to-end, and confirm the model evaluations before marking this ready for review.

@mergify mergify Bot added ci/build rust new-model Requests to new models performance Performance-related issues nvidia v1 labels Jul 15, 2026
@mergify mergify Bot added the tool-calling label Jul 15, 2026
@WoosukKwon WoosukKwon changed the title [Model] Add Inkling model support [Model] Add TML Inkling model support Jul 15, 2026
self.prefill_cudagraph_manager: SpeculatorCudaGraphManager | None = None
self.decode_cudagraph_manager: SpeculatorCudaGraphManager | None = None

self.num_spec_prefill_steps = (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't like the idea of folding multi-module MTP into the autoregressive speculator implementation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benchislett This is a great input. cc @TheEpicDolphin

The MTP integration might be a bit messy now. We will split this PR into smaller ones, and merge them one by one; MTP will be the last one since it involves the deepest changes and nontrivial design questions. Please share any alternative ideas that you might have.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm considering splitting it into it's own separate class that inherits from DraftModelSpeculator. The multi-module MTP implementation has become complex enough that i think it warrants separation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the draft PR: #48892. Will try to have it ready for review later today/tomorrow.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-layer MTP ready for review here: #48892

@dougbtv

dougbtv commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Hey — we're doing a 0-day build of this PR on H200s (SM90) and hit a crash in tml_fa4/interface.py during inference.

Problem: The else branch of the cute.compile() call in _flash_attn_fwd (around line 1288) passes SM100-specific positional args (logits_max_tensor, sfq_tensor, sfk_tensor, sfv_tensor, qk_sf_vec_size, v_sf_vec_size, bias_tensor, tile_count_semaphore_cute, max_seqlen_q) for all architectures. On SM90, FlashAttentionForwardSm90.__call__ has a smaller signature and cute.compile() raises:

DSLRuntimeError: Failed to bind arguments to function `__call__` ...
Caused exception: too many positional arguments

The same mismatch exists in the execution path (exec_args list) below it.

Additionally, inkling_fa4_rel_attention passes num_splits=32 but SM90 asserts not is_split_kv — split-KV is only supported on SM100+.

Workaround we're carrying downstream:

  1. Clamp num_splits=1 on major < 10 in fa4_rel_attention.py
  2. Post-fetch cmake patch on interface.py that adds an if arch // 10 in [10, 11]: guard around the SM100 compile/exec arg lists, with a fallback that matches the SM90 kernel signature

Wanted to flag in case you want to fix it upstream in the tml-fa4 interface or gate the Inkling model to SM100+ only. Happy to share the patch if useful.

@dougbtv

dougbtv commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

SM90 (H200) compatibility report

We got Inkling-NVFP4 serving on 8x H200 (SM90) using this PR merged onto our v0.25.0 base. Two issues came up that needed patching — sharing in case it's useful for the PR or for anyone else running on Hopper.

1. tml_fa4 interface.pycute.compile() arg mismatch on SM90

_flash_attn_fwd passes SM100-specific positional args (logits_max_tensor, sfq_tensor, sfk_tensor, sfv_tensor, qk_sf_vec_size, v_sf_vec_size, bias_tensor, tile_count_semaphore_cute, max_seqlen_q) to cute.compile() for ALL architectures. The SM90 kernel's __call__ has a smaller signature, so it crashes with DSLRuntimeError: too many positional arguments.

Fix: Added an arch check so only SM100/SM110 passes the extended arg list. SM90 gets a reduced compile/exec call that omits the FP4 scale-factor and split-KV args. Applied as a cmake post-fetch patch on tml_fa4/flash_attn/cute/interface.py.

2. moe.pyll_bf16_gemm CuTeDSL kernel crashes on SM90

_linear_with_fp32_out() guards the CuTeDSL ll_bf16_gemm path with has_device_capability(90), which passes on H200. But CuTeDSL kernel compilation fails on SM90 with a ThreadIdXOp MLIR binding incompatibility in _ll_bf16_dotprod.py. The torch.mm fallback works fine.

Fix: Tightened the guard from has_device_capability(90) to has_device_capability(100).

3. num_splits — split-KV assertion on SM90

inkling_fa4_rel_attention calls flash_attn_varlen_func with num_splits=32, but split-KV is only supported on SM100+. On SM90 this hits an assertion.

Fix: Clamped num_splits=1 when torch.cuda.get_device_capability()[0] < 10.


None of these are blockers for the PR (which targets Blackwell), just a heads-up if SM90/Hopper support is on the roadmap. Happy to upstream patches if helpful.

@WoosukKwon

Copy link
Copy Markdown
Collaborator Author

@dougbtv Thanks for reporting the issue. Yeah we made some last minutes change and they were not rigorously tested on Hopper GPUs. I think we should fall back to normal FA4 score_mod (instead of tml-fa4) for Hopper.

@mergify

mergify Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @WoosukKwon.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Jul 16, 2026
@WoosukKwon WoosukKwon changed the title [Model] Add TML Inkling model support [Model] Add Inkling LoRA, MTP, and piecewise CUDA graph support [2/N] Jul 16, 2026
@mergify mergify Bot removed the needs-rebase label Jul 16, 2026
@WoosukKwon WoosukKwon changed the title [Model] Add Inkling LoRA, MTP, and piecewise CUDA graph support [2/N] [Model] Add Inkling LoRA and MTP support [3/N] Jul 16, 2026
@WoosukKwon

Copy link
Copy Markdown
Collaborator Author

@dougbtv I've made a separate PR for Hopper GPU support: #48858

@WoosukKwon WoosukKwon changed the title [Model] Add Inkling LoRA and MTP support [3/N] [Model] Add Inkling LoRA and multi-depth MTP support [4/N] Jul 16, 2026
Extend the merged Inkling MTP=1 implementation to multiple checkpoint depths with multi-step speculative decoding and KV-cache plumbing.

Co-authored-by: Bugen Zhao <i@bugenzhao.com>
Co-authored-by: Giancarlo Delfin <32987265+TheEpicDolphin@users.noreply.github.com>
Co-authored-by: Isotr0py <Isotr0py@outlook.com>
Co-authored-by: Isotr0py <mozf@inferact.ai>
Co-authored-by: Jee Jee Li <jeejeelee@inferact.ai>
Co-authored-by: Roger Wang <hey@rogerw.io>
Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
@mergify

mergify Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @WoosukKwon.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify

mergify Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @WoosukKwon.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added needs-rebase mrv2 Model Runner V2 specific labels Jul 30, 2026
jundot added a commit to jundot/omlx that referenced this pull request Jul 31, 2026
Ports the upstream mlx-vlm inkling_mtp drafter semantics (one dense block per draft depth, vllm-project/vllm#48768 equivalent) onto the omlx chain cycle: the history fold runs block 0 on the persistent per-block cache list, chain step j runs block min(j, depth-1) on the head-clone copy, and the fold consumes pre-norm trunk hidden via a new per-model marker. Verify rollback reslices the short-conv states from padded-input stashes and trims the KV subs, replacing upstream's replay forward. oQ preserve_mtp maps model.mtp.layers.* through a sanitize hook (head floors at 4-bit), the bpw estimator and config normalization understand the nested mtp_config block, and prompt-priming capture is wired through the inner model. Measured on the oQ4e-mtp output the adaptive controller parks at depth 0 under prefix-cached serving (0.98-1.00x, outputs valid); a warmed head accepts 1.32 tokens/cycle at depth 4 on repetitive text with agreement decaying past depth 2, so the toggle stays opt-in.
jundot added a commit to jundot/omlx that referenced this pull request Jul 31, 2026
The first cut copied mlx-vlm's inkling_mtp drafter, which only folds block 0 and leaves depth blocks 1..7 with empty caches every round; measured per-depth agreement collapsed to 0.68/0.45/0.19/0.00 and the controller always parked. The reference semantics (vllm-project/vllm#48768) keep every block stateful over the accepted sequence: a cycle is k chained passes over one uniform token window (pass j = block j, hidden chained, token stream left-shifted), each fold trims provisional draft rows back to w0 = min_j valid_j and refolds the gap from a small ring of recent pairs, and the embedding side is double-normed through the backbone embed_norm. Rewritten that way (plus a begin-cycle hook so the pass counter lives on the cache list, no more per-cycle head clone, rolling conv-input stashes for cross-cycle rewinds, sliding-window prompt capture folded at activation, and a capture gate that skips return_hidden forwards - the v1 primed=0 bug), per-depth agreement is 0.87/0.76/0.67/0.61 with the same head weights and the server sustains 2.1-2.6 accepted tokens per cycle at 63-75% draft accept. Net decode is still 0.95-0.98x on this hardware because a fine-grained MoE verify pass costs 2.1x a plain step (per-token expert routing) plus 0.9x for the 8-pass head, so the adaptive controller correctly parks; the toggle stays opt-in. Unit oracles pin the trim/refold cycle against one-shot refolds and the runtime against the standalone reference engine.
jundot added a commit to jundot/omlx that referenced this pull request Jul 31, 2026
* fix: mixed CacheList store/restore integrity across cache subsystems

Composite CacheList layers mixing a sliceable KVCache with a non-sliceable ArraysCache were never exercised before. The prefix cache stored such layers as cumulative boundary snapshots but restored the KVCache sub by concatenating per-block data, duplicating the sequence. Restore now mirrors the store-side layer-level decision. Also: SSD compat signatures stamp CacheList sub composition (plus a per-block signature gate for hot-cache loads), TurboQuant excludes composite layers with KVCache members until a TQ sub-state serialization exists, scheduler emits sub_class_names, prefill boundary snapshots are extracted eagerly instead of aliasing live caches, and ArraysCache.extract gets the None-slot guard its siblings already had.

* feat: Inkling Small support (vendored model, serving, sliding-window slice)

Vendors the inkling package from mlx-vlm PR #1756 head plus the switch_layers/mlp/activations shims missing at the pin, with marked local changes: batched right-padded prefill correctness (conv_mask wiring, lengths-aware conv state writes, per-sequence key masking and log-tau positions, BatchKVCache-safe buffer-coordinate offsets) and an opt-out sliding-window K/V slice that keeps the 512-token layers O(window) instead of O(S). Adds a torch-free port of the transformers 5.14 Inkling processors, NVFP4 hf_quant_config translation, discovery registration, a channel output parser (content_thinking to think tags, invoke_tool_json to tool_calls, end_sampling stop), and prefill-guard pricing for the materialized banded attention bias.

* feat: oQ/oQe wiring for Inkling (streaming sanitize, sconv skip, estimator)

Name-driven rules mostly carry over; the additions are an explicit skip for the depthwise sconv conv weights (nn.Conv1d has no to_quantized), an inkling branch in the calibration layer walk, embed-norm-aware embedding for use_embed_norm checkpoints, class-method resolution on the VLM sanitize proxy plus a guard for packages without a VisionModel export, and a bpw/size estimator that prices post-sanitize names through the discovered plan (raw source names like experts.w13_weight priced 97% of the checkpoint as fp16 passthrough).

* feat: Inkling Lightning MTP runtime (single-checkpoint multi-depth head)

Ports the upstream mlx-vlm inkling_mtp drafter semantics (one dense block per draft depth, vllm-project/vllm#48768 equivalent) onto the omlx chain cycle: the history fold runs block 0 on the persistent per-block cache list, chain step j runs block min(j, depth-1) on the head-clone copy, and the fold consumes pre-norm trunk hidden via a new per-model marker. Verify rollback reslices the short-conv states from padded-input stashes and trims the KV subs, replacing upstream's replay forward. oQ preserve_mtp maps model.mtp.layers.* through a sanitize hook (head floors at 4-bit), the bpw estimator and config normalization understand the nested mtp_config block, and prompt-priming capture is wired through the inner model. Measured on the oQ4e-mtp output the adaptive controller parks at depth 0 under prefix-cached serving (0.98-1.00x, outputs valid); a warmed head accepts 1.32 tokens/cycle at depth 4 on repetitive text with agreement decaying past depth 2, so the toggle stays opt-in.

* fix: rewrite Inkling MTP cycle to vLLM per-depth-stateful semantics

The first cut copied mlx-vlm's inkling_mtp drafter, which only folds block 0 and leaves depth blocks 1..7 with empty caches every round; measured per-depth agreement collapsed to 0.68/0.45/0.19/0.00 and the controller always parked. The reference semantics (vllm-project/vllm#48768) keep every block stateful over the accepted sequence: a cycle is k chained passes over one uniform token window (pass j = block j, hidden chained, token stream left-shifted), each fold trims provisional draft rows back to w0 = min_j valid_j and refolds the gap from a small ring of recent pairs, and the embedding side is double-normed through the backbone embed_norm. Rewritten that way (plus a begin-cycle hook so the pass counter lives on the cache list, no more per-cycle head clone, rolling conv-input stashes for cross-cycle rewinds, sliding-window prompt capture folded at activation, and a capture gate that skips return_hidden forwards - the v1 primed=0 bug), per-depth agreement is 0.87/0.76/0.67/0.61 with the same head weights and the server sustains 2.1-2.6 accepted tokens per cycle at 63-75% draft accept. Net decode is still 0.95-0.98x on this hardware because a fine-grained MoE verify pass costs 2.1x a plain step (per-token expert routing) plus 0.9x for the 8-pass head, so the adaptive controller correctly parks; the toggle stays opt-in. Unit oracles pin the trim/refold cycle against one-shot refolds and the runtime against the standalone reference engine.

* perf: fix Inkling MTP cycle cost (lazy-graph accumulation, mask kernel recompiles)

Two cycle-cost defects were hiding the acceptance win. The head's bookkeeping arrays (rolling conv stashes, refold ring, per-block KV) were never on any eval path, so their lazy graphs deepened across cycles; a sync-free async_eval at fold time cuts a depth-4 head cycle from 59 ms to 8.2 ms. And the banded rel-position mask kernel templated S/LQ/Q_OFF into the Metal source, recompiling a pipeline every time the cache grew (every decode step on the seven global layers, every MTP verify and head pass); those are runtime params now, which also benefits the plain decode path. Matrix after the fix: code_4k 1.34x, code_16k 1.25x, story_4k 1.26x, story_16k 1.17x decode with the controller never parking (512-token generations sustain 1.9-2.3 accepted tokens per cycle at 58-70% draft accept).
@WoosukKwon

Copy link
Copy Markdown
Collaborator Author

Closing this as #48892 implemented this last remaining feature

@WoosukKwon WoosukKwon closed this Aug 12, 2026
@github-project-automation github-project-automation Bot moved this to Done in NVIDIA Aug 12, 2026
@WoosukKwon
WoosukKwon deleted the tml-inkling branch August 14, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/build mrv2 Model Runner V2 specific needs-rebase new-model Requests to new models nvidia performance Performance-related issues rust tool-calling v1

Projects

Status: Done
Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants