Skip to content

Fold the sigmoid gate into the causal-attention reduce epilogue: operator -8.3% where it folds, +0.31% decode - #225

Open
MichaelDementii wants to merge 3 commits into
Neroued:masterfrom
MichaelDementii:perf/attention-fused-sigmoid-gate
Open

Fold the sigmoid gate into the causal-attention reduce epilogue: operator -8.3% where it folds, +0.31% decode#225
MichaelDementii wants to merge 3 commits into
Neroued:masterfrom
MichaelDementii:perf/attention-fused-sigmoid-gate

Conversation

@MichaelDementii

Copy link
Copy Markdown
Contributor

All three call sites in TextContext issue causal_softmax_attention and then ops::sigmoid_mul
over the whole output — one more graph node per full-attention layer and per MTP tail.

Change

causal_softmax_attention takes an optional gate. Where the route reaches the shared BF16/INT8
reducer the multiply is folded into its store; every other route — FP8, NVFP4, K8V4, the prompt
kernel, chunked small-T — applies the standalone elementwise kernel inside the Op. One contract
either way, so no caller has to know which route it landed on.

The bytes are the bytes the caller's own sigmoid_mul produced. That is a rounding question, not a
formality: the standalone kernel reads what attention stored, so it sees the reduce result already
rounded to BF16. The fused epilogue therefore rounds first, widens back, multiplies in FP32 and
rounds to nearest. Keeping the FP32 accumulator would be more accurate and would move tokens.

causal_softmax_attention_cached is untouched and keeps the standalone multiply; extending it is a
separate decision.

Operator

The bench gains --gate off|standalone|fused. Across 320 append cells:

cells median kernel time vs standalone
route folds the gate (BF16, INT8) 65 −8.33 %, 58 of 65 improved
route does not fold (control) 255 +0.00 %

Per storage and geometry: bf16 16/2 −8.33 %, bf16 24/4 −6.67 %, int8 16/2 −8.33 %, int8 24/4
−8.78 %. The 255 unfolded cells are the control that the standalone path is untouched. Timings
quantise to 2.048 µs at these widths, so read the percentages as coarse.

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. Ten passes, arm order mirrored
between passes, third arm = master rebuilt under a second label as a zero control.

median vs master range passes positive
this change +0.311 % decode +0.134 … +0.391 10 / 10
zero control +0.003 % −0.097 … +0.088

The worst pass of the change is above the best pass of the control.

Prefill is not claimed: −0.06 % median against a zero control of +0.06 % on the same runs.

Correctness

Each case in causal_cache.cpp now runs a second time with a gate and is judged against a third,
ungated run — which separates "the Op repeats itself" from "the gate is exact" instead of conflating
the two. Both checks are exact, on every storage, both geometries, batched and single, graph and
eager.

Strength control: dropping the rounding before the multiply — the one subtlety in the epilogue —
makes that check fail. Without it "the test passes" would say nothing.

ctest 114/114 on both sides; clang-format clean on every touched file.

Scope: 8 files, +224 / −99, of which the bench switch and the test are 129 lines.
RTX 5090 sm_120a, CUDA 13.1, Release, qwen3_6_35b_a3b.ninfer.

🤖 Generated with Claude Code

The three call sites in TextContext all issued causal_softmax_attention and then
ops::sigmoid_mul over the whole output, which is one more graph node per
full-attention layer and per MTP tail.

causal_softmax_attention now takes an optional gate. Where the route reaches the
shared BF16/INT8 reducer the multiply is folded into its store; every other route
- FP8, NVFP4, K8V4, the prompt kernel, chunked small-T - applies the standalone
elementwise kernel inside the Op. One contract either way, and the bytes are the
bytes the caller's own sigmoid_mul produced: the reduce result is rounded to BF16
first, widened back and multiplied in FP32, because the standalone kernel reads
what attention stored rather than the accumulator.

The operator bench gains --gate off|standalone|fused. Over the 65 cells where the
fold applies, kernel time is -8.33% median with 58 of 65 improved; over the 255
cells where the route does not fold, the median is exactly zero, which is the
control that the standalone path is untouched.

End to end on Qwen3.6-35B-A3B, ten mirrored passes with a zero-control arm:
decode +0.311% median, all ten passes positive, fully separated from a control
spanning -0.097 to +0.088%. Prefill is unchanged within that same control.

The Op test now runs each case a second time with a gate and judges it against a
third, ungated run, which separates "the Op repeats itself" from "the gate is
exact" instead of conflating the two. Dropping the rounding before the multiply
makes that check fail, so it is not vacuous.

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-09T22:59:41.705456Z d63848e 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.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Something went wrong. Try again later by commenting “@codex review”.

Provided git ref 9fe228a391f4d9aeabcd38738ba40cc4d1958957 does not exist
ℹ️ 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".

@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: 9fe228a391

ℹ️ 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".

failures +=
verify_exact((label + " ungated repeat").c_str(),
copy_from_guarded<std::uint16_t>(dexpected, output.size()), output);
ops::sigmoid_mul(tgate, texpected, device.stream);

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 Qualify the gated output against an independent oracle

The expected result is produced by another call to causal_softmax_attention followed by the production sigmoid_mul, so this verifies only parity between two production paths. A shared error in the attention result, sigmoid computation, or BF16 boundary can therefore pass this check; derive the gated expectation from the existing FP64 attention oracle and an independent host sigmoid/cast instead.

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

Useful? React with 👍 / 👎.

Execution execution = Execution::Graph;
CacheMode cache = CacheMode::Cold;
PageMapping mapping = PageMapping::Identity;
GateMode gate = GateMode::Off;

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 Persist the gate mode in benchmark results

When --gate standalone or --gate fused is used with --csv-out, the selected mode changes the measured workload but is not stored in Result or emitted by write_csv; the resulting rows are indistinguishable from --gate off and from each other once separated from the console header. Add the gate mode to each result and its CSV identity so saved measurements remain interpretable.

AGENTS.md reference: AGENTS.md:L109-L113

Useful? React with 👍 / 👎.

…d record the gate mode

The gated checks compared production against production, so a defect shared by the
attention result, the sigmoid or the BF16 boundary would have passed them. Judge the
gated output against the same FP64 attention oracle the ungated output is judged by,
multiplied by a host sigmoid, with the criterion widened by the one extra BF16
rounding the gated route performs.

The bench now carries the gate mode each row was measured in -- the effective one, so
a cached row reads standalone -- on the console line and in the CSV.

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

Copy link
Copy Markdown
Contributor Author

Both taken, in 2b7fb9e7.

P1, the oracle. You are right that the two gated checks compared production against
production. The gated output is now judged against the same FP64 attention oracle the ungated
output is judged by, multiplied by a host sigmoid of the gate — an independent path from the
inputs, not a second run of the Op. The parity checks stay as the contract they express: the
folded route must produce the bytes the standalone one produced.

The criterion is the ungated one widened by exactly the second BF16 rounding the gated route adds
(2^-9 on the relative-L2 bound and on the bound relative to the largest reference; the absolute
floor is unchanged). That widening is not cosmetic — with only the L2 bound widened, the oracle
check failed on real cells, e.g. actual=-0.664062 reference=-0.660504, which is the per-element
rounding and nothing else. ninfer_softmax_attention_test passes on all three suites.

P2, the CSV. Each row now carries the gate mode it was measured in, on the console line and in
the CSV, as the effective mode rather than the flag: the cached entry takes no gate parameter,
so with --gate fused it records standalone. From one run:

append,d256-h16-kv2,bf16,identity,graph,cold,fused,1,4,...
cached,d256-h16-kv2,bf16,identity,graph,cold,standalone,1,4,...

clang-format clean on both files.

@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: 2b7fb9e76d

ℹ️ 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 914 to +915
Case data(geometry, storage, width, rows.contexts, rows.valid_columns, rows.table_rows,
options.mapping);
options.mapping, options.gate);

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 Include the effective gate mode in profile metadata

Fresh evidence after the follow-up is that --profile now passes options.gate into the measured Case, but profile() still prints a PROFILE header without any gate field, and the new gate=... console line is below this branch's early return. Consequently, profiles captured with --gate off, standalone, or fused remain indistinguishable from their emitted metadata; for cached --gate fused, the header should also report the effective standalone mode, as the CSV path does.

AGENTS.md reference: AGENTS.md:L109-L113

Useful? React with 👍 / 👎.

The --profile path returns before the console line that named the mode, so profiles
captured with --gate off, standalone and fused carried no way to tell them apart.
The PROFILE header now names the effective mode, through the same helper the CSV and
the result line use.

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

Copy link
Copy Markdown
Contributor Author

Right — the --profile branch returns above that line. d63848ec puts the gate in the PROFILE
header through the same helper the CSV and the result line now use, so all three name the effective
mode. Four runs, two entries by two modes:

PROFILE entry=append ... cache=cold gate=off        B=1 W=4 ...
PROFILE entry=cached ... cache=cold gate=off        B=1 W=4 ...
PROFILE entry=append ... cache=cold gate=fused      B=1 W=4 ...
PROFILE entry=cached ... cache=cold gate=standalone B=1 W=4 ...

clang-format clean.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: d63848ece0

ℹ️ 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