Repository navigation
perf(moe): tune the Nemotron MTP decode row on S5000 with a checked-in recipe - #115
Conversation
Signed-off-by: Xiaodong Ye <xiaodong.ye@mthreads.com>
a288538 to
364b0ec
Compare
Package the measured Nemotron decode and graph buckets, gate the Triton tensor-descriptor path on the language API, and preserve SPLIT_K in the fused-MoE constexpr map. Add resolver and capability regression coverage.
13d3b47 to
37fba15
Compare
Tiny-M entry of
|
| host / driver | arm | config | 4k/128 TPOT | acceptance | output SHA |
|---|---|---|---|---|---|
| 5.2.0 host | baseline + table | old "16" entry |
8.996 ms | n/a | c214d7149093748b |
| 5.2.0 host | #241+#242 + table | old "16" entry |
7.99 ms | 4.04-4.11 | same |
| 5.2.0 host | #241+#242, no table | tiny-M default | 7.622 ms | 3.90 | same |
| 3.3.8 host | #241+#242 + table | new "16" entry |
8.473 ms | 3.84 | same |
| 3.3.8 host | #241+#242, no table | tiny-M default | 8.511 ms | 4.11 | same |
Nemotron-3.5 MTP6, 4096-in/128-out, graph path, fresh container per arm, 3 samples per arm, one
output family throughout, so the arms are subtractable. The old entry cost -4.6% versus the
default; with this commit the table matches the default (8.473 vs 8.511 ms, and the no-table arm ran
at higher acceptance, which inflates its TPOT). Larger-M entries are untouched.
The new "16" entry is the tiny-M default configuration verbatim
(BLOCK_SIZE_M=16, BLOCK_SIZE_N=64, BLOCK_SIZE_K=128, GROUP_SIZE_M=1, SPLIT_K=1, num_warps=4, num_stages=1, from get_default_config at M<=32 for bf16), so the table keeps whatever value it
has at larger M without paying for it at decode.
Note for whoever retunes further: get_configs_compute_bound() searches num_stages=1 only, so a
proper re-tune of the tiny-M rows is a separate, welcome follow-up - it may beat the default via
tiles/warps/GROUP_SIZE_M, which this change deliberately does not claim.
Edited: internal identifiers (tracker references, host names, card indices) removed; the measurements are unchanged.
Follow-up on this table: measured cost, and the right way to fix itI first pushed a hand-edit of What the measurements say (Nemotron-3.5 MTP6, 4096-in/128-out, graph path, one output family
So the stale entry costs about 4.6% once The right fix is at the generation layer, one of:
Until one of them lands, this table is not a regression - it just keeps #242 inactive - so it should Edited: internal identifiers (tracker references, host names, card indices) removed; the measurements are unchanged. |
Correction: the stale
|
|
tiny-M row corrected in place (head Why this rowKey 16 is the bucket every MTP6 verify shape lands on (real decode calls were logged at M = 1/7/14/21/28, all resolving to key 16) and it dominates the step — but it is the one row this campaign never derived: Measurementsone MTT_S5000 card, image
The last row is the mechanism check: with the table otherwise intact and only the tiny-M key missing, TPOT lands exactly on the untuned number, so the whole effect is this bucket's staging and nothing else in the map. Branch artifact re-verifiedThe "this row" numbers above swapped the row into the installed table; this run uses the file as committed:
Notes
Full nine-arm record and the shadowing mechanism: the internal campaign write-up. Edited: internal identifiers (tracker references, host names, card indices) removed; the measurements are unchanged. |
9a6562b to
c612d30
Compare
|
History note (force-push, no content change). The last three commits - |
c612d30 to
193ae5f
Compare
The table row is now derived from a checked-in recipe, not typed by handFollow-up to the row itself: the key-16 row was placed from an end-to-end measurement, and nothing in the repo could produce it. That is fixed in the same commit ( What was added
Reproduce: python src/torchada/triton/autotune/fused_moe/tune_moe.py \
--config src/torchada/triton/autotune/fused_moe/ci/shapes.json \
--materialize --merge-configs
python -m pytest tests/test_tune_moe_recipe.pyThe test seeds a copy of the shipped table, materializes the recipe into it, and asserts the result has the shipped file's content row for row and key for key. Why the row is pinned instead of sweptThe uniform-routing microbenchmark is not authoritative for this bucket, and it is not even self-consistent. Five sweeps of
Only the last two land on the row this table ships, which is also the one that wins end to end (5.5659 ms vs 6.6207 ms TPOT, the figure in the commit message). The grid's own answer for this bucket therefore cannot be the source of truth; the recipe records the measured row and the measurement.
Notes
Evidence: Edited: internal identifiers (tracker references, host names, card indices) removed; the measurements are unchanged. |
Key 16 is the bucket every MTP6 verify shape lands on (decode M = 1/7/14/21/28
all resolve here by nearest key) and it dominates the step, but it is the one row
this campaign never derived: it was carried over verbatim from the previous
five-key map when the table was repackaged. The shipped stage grid already
contains the config we want - get_configs_compute_bound iterates `num_stages = 1`
only, and its grid includes (BLOCK_M, BLOCK_N) = (16, 64) with BLOCK_K 128 - and
every other table in the tree carries s1 rows; this is the only one that does not.
Measured end to end on one MTT_S5000 card, same container and image as the
comparison arm, with the canonical Nemotron-3.5 MTP6 protocol (4096-in /
1000-out, one warmup plus five measured repeats, byte-identical output digests in
every arm):
table as shipped 6.6207 ms/token ttft 1516.5
this row 6.2626 ms/token ttft 1520.5 -5.41%
this row + vllm-musa #241 and #242 5.5649 ms/token ttft 1513.1 -15.95%
(control: same stack, previous row) 5.9338 ms/token
TTFT is unchanged because the large-M rows still serve prefill - only the decode
bucket moves. Key 512 keeps the config it was measured with; the two buckets used
to share a row and are now independent, and the routing test now pins which M
lands on which key.
The row is not hand-typed. ci/shapes.json carries the kernel shape and the
bucket, and records the row as `pinned_rows` together with the measurement behind
it, so
tune_moe.py --config .../ci/shapes.json --materialize --merge-configs
writes the table from the recipe, and tests/test_tune_moe_recipe.py asserts the
result has the shipped file's content row for row and key for key. It has to be
pinned rather than swept: the uniform-routing microbenchmark does not agree with
itself on this bucket - five sweeps produced three distinct winners
(BM64/BN128/BK64/GSM1/w8/s1, BM16/BN64/BK64/GSM1/w4/s1 and
BM16/BN64/BK128/GSM1/w4/s1) and only two of them landed on the row this table
ships - so nothing in the grid is authoritative for it here.
The tooling a recipe needs comes with it: an entry may pin `batch_sizes` or
describe a shape without a model checkout, `--merge-configs` keeps the rows a run
did not measure, and a candidate the kernel cannot launch is logged and skipped
instead of killing the tuning worker and leaving the parent waiting for a result
that never arrives.
342df3e to
440265c
Compare
The English and Chinese READMEs have not kept up with the May-October work. This documents what merged, moves the version-gated shims into one table, and refreshes the measured numbers that had gone stale. Feature table - CUDA memory-pool APIs, `torch.cuda.streams`, CUDA-graph executable rotation, `torch.cuda._get_device_index`, `get_memory_info()`, and the FlashAttention provider shims (#61, #98, #103, #106, #108, #115) - the "What Works" table goes back to one line per feature; the paragraph-sized `log_` / `isfinite` / `out_dtype` cells move into the new section below New "torch_musa Compatibility" section - one table of every version-gated shim with the release it is installed on: the four `< 2.11.0.post2` patches (#106, #113, #124), the `< 2.13.0` `mm`/`bmm` `out_dtype=` backport (#116), the stable-ABI header backport (#86, #96), asynchronous `isfinite` (#120), and `torch.cuda.streams` (#98) New "Environment Variables" section - the graph-rotation knobs (#72), `TORCHADA_PLATFORM`, the C++ operator-override switches (#61, #128), and the two variables that were already documented Corrected and extended details - torch.compile: FX `device` builtin (#124), Dynamo's device-index helper (#108), `MUSA_VISIBLE_DEVICES` mirroring (#106) - C++ extensions: nested `<torch/cuda.h>` porting (#95), stable-ABI `STABLE_TORCH_LIBRARY_IMPL` rekeying and stream helpers (#100), torch 2.6+ `include_paths`/`library_paths` signatures (#121), stale JIT build locks (#128) - MoE tables are generated from checked-in recipes (#115) - unsupported CUDA runtime APIs as no-ops (#65) - Performance: replace the 0.1.94 / torch_musa 2.7.1 numbers with the checked-in 0.1.95 / 2.11.0.post2 entry, and stop claiming every fast path is under 200ns
Tune the Nemotron MTP decode row on S5000 with a checked-in recipe
Status: draft, pending review. Six commits on top of
main: the packaged Nemotron shape map and thedecode/graph work, then the decode table row plus the autotune tooling that derives it (
440265c, on topof
37fba15). Pre-squash history for the older lineup is preserved onbackup/pr115-before-squash-20260919.What it does
1. The decoded table row.
E=128,N=1856,device_name=MTT_S5000.jsongave the decode bucket(
M = 1/7/14/21/28all resolve to nearest key 16) a row carried over from the previousfive-key map:
BLOCK_M=32, BLOCK_N=64, BLOCK_K=64, GROUP_SIZE_M=16, num_warps=8, num_stages=2.It now carries the tiny-M tile this hardware actually wants:
Key 512 keeps the config it was measured with - the two buckets used to share a row and are now
independent. Packaged keys stay
16, 49, 80, 113, 144, 511, 512, 513; the serving routes areM≈115/120 -> 113andM=512 -> 512, and largerMfalls back to the generic heuristic.Packaged table SHA256:
3e39677d2dbf5c984325f94255dbba3fa5eb2ac331fde3a86a79f75105fc531b.2. The recipe that produces the row.
src/torchada/triton/autotune/fused_moe/ci/shapes.jsonrecords the kernel shape, the bucket and the row itself (
pinned_rows, together with themeasurement behind it), so the table is generated rather than typed:
writes it without a GPU, and
tests/test_tune_moe_recipe.pyasserts the result matches the shippedfile row for row and key for key.
--tune --merge-configsreruns the search and keeps the rows arun did not measure, so a disagreement shows up as a reviewable diff.
3. Why this row is pinned rather than swept. The uniform-routing microbenchmark is not
authoritative for this bucket and not self-consistent: five sweeps produced three distinct winners
and only two of them landed on the row this table ships.
4. Runtime and kernel changes the decode path depends on (squashed earlier work):
SPLIT_Kstays in the fused-MoE constexpr map - popping that key changed the generated launch variant and
caused a TPOT regression; the tensor-descriptor capability check now requires the Triton language
API
triton.language.make_tensor_descriptor, so Triton 3.2 takes the fallback path while newerTriton can use descriptors; the resolver regression test pins which
Mlands on which key.Measurements
End-to-end on one MTT_S5000 card, same container and image per comparison, canonical Nemotron-3.5
MTP6 protocol (4096-in / 1000-out, one warmup plus five measured repeats, byte-identical output
digests in every arm):
TTFT is unchanged: prefill still runs the large-
Mrows. MTP-depth ladder for the same stack,median of five measured requests:
A rerun of the same final wheel and source reproduced MTP6 within 0.117% of the historical manual
result (
6.616876over ten requests vs6.609144ms/token) with matching output SHA256. Absolutenumbers are host- and image-specific.
Validation
tests/test_tune_moe_recipe.py+tests/test_nemotron_moe_config.py:22 passedon the branchartifact.
10 passed.509 passed, 19 skipped; the only reported failure is the source-onlyproject-version check without project metadata.
2cf0a6915ce544dc493a0990f2ea38d81601128a, post1/Triton3.2 image, TP1 BF16, Triton Mamba, piecewise + full graphs, 4096 input / 1000 output.
Scope and known limits
E,N, dtype, block shape, device) plus the Triton versiondirectory, not by model. This row affects the
M <= 28bucket of any model that resolves to thatshape on this device; the other seven rows are untouched.
hidden_sizeandtopkare not part of the table key (upstream naming convention); the recipedoes record them.
num_stages=1only, so the seven rows carryings2/s3/s4cannot beregenerated by the current grid - a full-table re-tune would be a deliberate re-baseline.