Skip to content

Remove the empty graph nodes of the MTP draft phase: +0.33% decode on Qwen3.6-35B-A3B - #226

Open
MichaelDementii wants to merge 5 commits into
Neroued:masterfrom
MichaelDementii:perf/mtp-draft-phase-nodes
Open

Remove the empty graph nodes of the MTP draft phase: +0.33% decode on Qwen3.6-35B-A3B#226
MichaelDementii wants to merge 5 commits into
Neroued:masterfrom
MichaelDementii:perf/mtp-draft-phase-nodes

Conversation

@MichaelDementii

Copy link
Copy Markdown
Contributor

Third of three node-count reductions measured in one session; #222 and #225 are the others. All
three are independent mechanisms but all three touch text_context_impl.h, so whichever lands first
leaves the other two a trivial rebase.

Why one PR and not three. Separately these are +0.090 %, +0.095 % and +0.040 % of decode against
a zero-control band of ±0.15 % on this hardware — none is resolvable alone. Together they are.

Change

was is
ops::argmax opened its atomic contest with a 4-byte cudaMemsetAsync per slice memset node initializer kernel
the MTP stem normalized embedding and hidden only to lay them side by side 3 nodes 1
the MTP tail added the residual only for the next norm to read it back 2 nodes 1
the draft chain copied the new hidden state over the old one every step copy per step two buffers

Both new Ops carry a *_supported() predicate with the three separate calls as the fallback, so an
unregistered shape keeps working.

Nodes

One traced run — 8192-token prefill, 64 generated tokens, two repetitions, nsys --cuda-graph-trace=node:

master this
[CUDA memset] 676 503 (now kernel nodes)
rmsnorm_cta_bf16x2 3132 2735
mtp_pack_fc_input 134 0
residual_add_bf16x8 132 3
memcpy Device-to-Device 100 18

End to end

ninfer_bench -pg 8192,512 --prefill-chunk 4096 --max-ctx 9216 --spec mtp --draft-tokens 3 --lm-head-draft -r 2 --warmup 1, metric decode_output_tok_s_mean. Twelve passes, arm order
mirrored, third arm = master rebuilt under a second label as a zero control.

median range passes positive
this change +0.334 % decode −0.34 … +0.40 10 / 12
zero control −0.027 % −0.79 … +0.22

Ten of the twelve sit in a tight band of +0.27…+0.40 %. The two that do not, and the one outlier of
the control, coincide with a build running on the neighbouring container, which shares the host CPU
quota — so separation is not full and I am not claiming it. Prefill is unchanged: −0.009 %
median against a control of −0.029 %.

Correctness

Output byte-identical on the same prompt, greedy, int8 KV, mtp3. ctest 114/114. The stem Op test
checks the fused result against the three-Op path it replaces, in the same process on the same
inputs.

Scope: 10 files, +756 / −18, of which 288 are the new Op test.
RTX 5090 sm_120a, CUDA 13.1, Release, qwen3_6_35b_a3b.ninfer.

🤖 Generated with Claude Code

The draft phase of an MTP decode round spends nodes that carry no work of their
own. Three of them, in one pass because separately none is above what an
end-to-end benchmark on this hardware can resolve:

  - ops::argmax opened its atomic contest with a 4-byte cudaMemsetAsync per
    slice; an initializer kernel writes the same bytes as a kernel node;
  - the MTP stem normalized the embedding and the hidden state only to lay them
    side by side, and the MTP tail added the residual only for the next norm to
    read it back: two new Ops write each result into place directly;
  - the autoregressive draft chain copied the new hidden state over the old one
    after every step, where alternating between two buffers costs nothing.

Both new Ops carry a *_supported() predicate and the three separate calls remain
as the fallback, so an unregistered shape keeps working.

Over one traced run - 8192-token prefill, 64 generated tokens, two repetitions:
173 memset nodes become kernel nodes, 397 rmsnorm launches disappear along with
134 pack and 129 residual-add launches, and 82 of 100 device-to-device copies go
away. End to end on Qwen3.6-35B-A3B, twelve mirrored passes, decode +0.334%
median with ten of twelve positive; the two negative passes and one outlier of
the zero-control arm coincide with a build running on the neighbouring container,
which shares the host CPU quota.

Output is byte-identical, ctest 114/114.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T01:40:13.631873Z d121079 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@MichaelDementii

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 697606edea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +190 to +192
ops::rmsnorm(t_embedding, t_weight_e, kStemEps, true, t_norm_e, nullptr);
ops::rmsnorm(t_hidden, t_weight_h, kStemEps, true, t_norm_h, nullptr);
ops::mtp_pack_fc_input(t_norm_e, t_norm_h, t_reference, nullptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Test the fused Ops against an independent numerical oracle

These production rmsnorm and pack calls are used as the sole reference, and the residual test below likewise composes residual_add with rmsnorm; consequently, an arithmetic, reduction, or rounding defect shared with the copied fused implementation can pass every new comparison. Add a test-owned naive FP32/FP64 oracle for each complete fused formula, including the BF16 residual state boundary, and retain implementation-parity checks only as supplementary evidence.

AGENTS.md reference: AGENTS.md:L83-L88

Useful? React with 👍 / 👎.

Every check on the fused Ops used the Ops they replace as the reference, so a
defect the two share passed all of them. Add the naive FP64 oracle and the
criterion tests/ops/test_rmsnorm.cpp judges ops::rmsnorm by, evaluated from the
represented BF16 inputs; the residual half gets an exact oracle, because a BF16
sum of two BF16 values is exact in double. The parity checks stay: they are the
bit-exactness these Ops claim.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MichaelDementii

Copy link
Copy Markdown
Contributor Author

Taken, in 2a9dc0fa. Both fused Ops are now judged against an independent oracle as well:

  • the stem — the naive FP64 RMSNorm oracle and the criterion tests/ops/test_rmsnorm.cpp judges
    ops::rmsnorm by, evaluated from the represented BF16 inputs and compared against both halves of
    the packed output;
  • the tail — the same for the normalised output, and an exact oracle for the residual, because
    a BF16 sum of two BF16 values is exact in double.

The parity checks stay where they were. They are not weak evidence, they are the claim these Ops
make: the fused route has to produce the bytes the three-Op path produced, and anything that shifts
a bit shows up as a different draft token.

Strength control, and it answers your point directly. The same defect placed in both the fused
kernel and ops::rmsnorm (+ eps+ 0.05f in mtp_pack.cuh and rmsnorm.cuh) leaves every
parity check passing and fails only the new ones — 12 of 12 oracle cells, no other failure in the
run:

mtp_norm_pack_fc_input D=5120 T=1 against the oracle: reduction criterion failed
  at index 4581 actual=2.4375 reference=2.45659

With the kernels restored, ninfer_mtp_pack_test is OK. clang-format clean.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a9dc0faa7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/ops/launcher/mtp_pack.cu Outdated
Comment on lines +43 to +44
if (d >= 512 && d <= 3072 && d % 512 == 0) { return MtpRowRoute::Cta256x6; }
if (d > 3072 && d <= 8192 && d % 1024 == 0) { return MtpRowRoute::Cta512x8; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Limit the fused route to registered MTP widths

Restrict this dispatch to the registered MTP widths, 2048 and 5120. As written, widths such as 512, 1536, 4096, and 8192 are treated as supported—and the test explicitly canonizes 1536—even though no product target requires them. This creates placeholder execution paths coupled to the current generic RMSNorm ladder; unsupported future widths should remain on the composed fallback until a target explicitly requires and qualifies them.

AGENTS.md reference: AGENTS.md:L57-L61

Useful? React with 👍 / 👎.

The route mirrored the whole generic RMSNorm ladder, so 512, 1536, 4096 and 8192
were admitted although no target asks for them -- a path kept alive for a model
that does not exist. Admit 2048 and 5120, drop the instantiation nothing reaches
any more, and pin both directions in the test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MichaelDementii

Copy link
Copy Markdown
Contributor Author

Taken, in 03e67bc3. The route now admits 2048 and 5120 and nothing else; 1536, 4096 and
8192 go back to the composed three-Op path, and the 512x8 instantiation, which no admitted
width reaches any more, is gone rather than left dangling.

The same predicate serves the residual-and-norm tail, so that route narrowed with it. The test
pins both directions on both routes — 1536 and 4096 now assert declined, 384 still does,
2048 and 5120 still assert admitted. ninfer_mtp_pack_test is OK, clang-format clean.

Why the ladder was mirrored in the first place is unchanged and still in the comment: an admitted
width has to land on the instantiation src/ops/launcher/rmsnorm.cu would have picked for it, or
the sum of squares splits differently and the fused row stops being bit-exact. Narrowing does not
weaken that — it only stops offering it to widths nothing asks for.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03e67bc369

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

void mtp_norm_pack_fc_input(const Tensor& embedding, const Tensor& embedding_weight,
const Tensor& hidden, const Tensor& hidden_weight, Tensor& out,
float eps, cudaStream_t stream) {
require_stem_operands(embedding, embedding_weight, hidden, hidden_weight, out);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject invalid epsilon before launching fused norms

When either fused Op is called directly with eps <= 0 or a non-finite value, these entry points launch and can produce NaNs instead of rejecting the input as the rmsnorm() operation they replace does (src/ops/wrapper/rmsnorm.cpp validates this at lines 56–58). Apply the same positive-and-finite check in both mtp_norm_pack_fc_input() and mtp_residual_norm() so their advertised composition preserves the normalization contract.

Useful? React with 👍 / 👎.

Both fused entry points stand in for a composition containing ops::rmsnorm, which
refuses such an eps; they launched instead and came back with NaN. Apply the same
check and pin it in the test, on both Ops and on both registered widths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MichaelDementii

Copy link
Copy Markdown
Contributor Author

Right, and fixed in 59f80df2 — both entry points now apply the same check ops::rmsnorm applies,
with the same message shape.

Pinned in the test rather than only written: eps_contract calls both Ops at both registered
widths with 0, -1e-6, NaN and infinity and requires each to throw. Removing the two checks and
nothing else turns that into 16 failures, one per call; restoring them returns the suite to
OK. clang-format clean.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59f80df250

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread include/ninfer/ops/mtp_pack.h Outdated
Comment on lines +43 to +47
* Numeric:
* Identical to rmsnorm() applied to each half followed by mtp_pack_fc_input(): the same block
* width, pair decomposition, reduction and epilogue produce the same bytes. Returns false from
* mtp_norm_pack_fc_input_supported() for shapes it does not cover, and the caller keeps the
* three-Op path there.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove private reduction topology from the Op contract

The Numeric contract makes the current RMSNorm block width, pair decomposition, reduction order, and byte-for-byte parity semantic requirements. This conflicts with docs/maintainer/op-development.md §3, which explicitly excludes reduction association, launch decomposition, and bitwise equality for floating-point Ops; future RMSNorm tuning would now either silently violate this contract or require the fused kernels and exact-parity tests to change in lockstep. Specify the FP64-oracle criterion and observable BF16 boundaries here instead, keeping implementation parity only as supplementary regression evidence.

AGENTS.md reference: AGENTS.md:L83-L89

Useful? React with 👍 / 👎.

Comment thread src/ops/wrapper/mtp_pack.cpp Outdated
Comment on lines +66 to +67
if (embedding_weight.numel() != rows || hidden_weight.numel() != rows) {
throw std::invalid_argument("mtp_norm_pack_fc_input: weight length must be D");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject non-vector weights in both fused Ops

When a caller supplies a contiguous weight with numel() == D but a shape such as [1,D], this check accepts it and both the support predicate and fused execution proceed, whereas the documented [D] contract and the composed ops::rmsnorm route require ne[0] == D with all remaining dimensions equal to one. Validate the complete weight shape for both stem weights and the residual-norm weight so these fused entry points preserve the operation they replace.

AGENTS.md reference: AGENTS.md:L83-L89

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 59f80df250

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread include/ninfer/ops/mtp_pack.h Outdated
Comment on lines +43 to +47
* Numeric:
* Identical to rmsnorm() applied to each half followed by mtp_pack_fc_input(): the same block
* width, pair decomposition, reduction and epilogue produce the same bytes. Returns false from
* mtp_norm_pack_fc_input_supported() for shapes it does not cover, and the caller keeps the
* three-Op path there.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Define semantic RMSNorm behavior instead of a private route

Replace this implementation-parity clause with the complete RMSNorm formula, the positive-and-finite eps domain now enforced by the wrappers, the observable BF16/state boundaries, and the numerical acceptance criterion. Making the current block width, pair decomposition, reduction tree, and byte parity contractual prevents an independent implementation and turns a valid future rmsnorm kernel tuning into a contract break even when its mathematical output remains conformant.

AGENTS.md reference: AGENTS.md:L83-L89

Useful? React with 👍 / 👎.

Comment thread src/ops/wrapper/mtp_pack.cpp Outdated
Comment on lines +66 to +68
if (embedding_weight.numel() != rows || hidden_weight.numel() != rows) {
throw std::invalid_argument("mtp_norm_pack_fc_input: weight length must be D");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce the declared one-dimensional weight shapes

When a caller supplies a contiguous BF16 weight shaped, for example, {D/2, 2}, this numel() check accepts it and the support predicate admits the fused route even though the contract requires [D] and ops::rmsnorm rejects the same operand. The residual helper repeats this check for its weight; validate ne[0] == D and ne[1..3] == 1 in both helpers so malformed tensor views cannot silently cross the public Op boundary.

Useful? React with 👍 / 👎.

…shape

The Numeric fields made the current RMSNorm block width, pair decomposition and
byte parity contractual, which docs/maintainer/op-development.md forbids: a valid
retuning of ops::rmsnorm would have broken the contract without changing what the
Op computes. State the formula, the eps domain, the observable BF16 roundings and
the FP64-oracle criterion instead; parity stays where it belongs, in the tests.

The weight check counted elements, so [1,D] and [D/2,2] reached the fused route
although ops::rmsnorm refuses them. Check the whole shape in both Ops.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MichaelDementii

Copy link
Copy Markdown
Contributor Author

Both taken, in d121079d.

The contract. You are right and the rule is explicit — op-development.md §3 forbids freezing
reduction association, launch decomposition and bitwise equality in a contract, and my Numeric
fields froze all three. A legitimate retuning of ops::rmsnorm would have broken the contract
without changing what the Op computes. Both fields now state the formula, the positive-and-finite
eps domain, the BF16 roundings that are the observable boundaries, and the FP64-oracle criterion
— enough to write an independent implementation, which is what §3 asks for. Bit-parity with the
composed path is still checked, in the tests, as regression evidence rather than as the contract.

The weight shape. Also right: numel() let [1,D] and [D/2,2] take the fused route although
ops::rmsnorm refuses them. Both Ops now check the whole shape. Pinned: weight_shape_contract
feeds those two views to all three weight parameters at both widths; reverting to the element count
and nothing else turns that into 12 failures, and the suite is OK with it restored.

clang-format clean on all three files.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: d121079154

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants