File size: 3,119 Bytes
c0696d0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | # Detailed results
Hardware: 2× RTX PRO 6000 Blackwell Max-Q (sm_120, 96 GB, 300 W cap), PCIe, TP2. A side process held 2.4–2.9 GB per
GPU throughout. Raw outputs are in `raw/` and Nsight Compute summaries in `ncu/`.
## Dense MXFP8 backend
Server A/B with CUDA graphs and Engram in pinned RAM, before any MoE kernel changes (`raw/ab-marlin.txt`,
`raw/ab-humming.txt`).
| Dense backend | Decode, 2K, 1 stream | Decode, 2K, 4 streams | Prefill, 46K, 1 stream | Decode, 46K, 1 stream | Prefill, 46K, 4 streams | GSM8K-200 |
|---|---:|---:|---:|---:|---:|---:|
| FlashInfer W8A8 (vLLM default) | | | 4,026 | 68.0 | | 97.5% |
| Marlin W8A16 | 101.6 | 210.2 | 3,804 | 90.5 | 7,617 | 97.0% |
| Humming 0.1.12 | 87.2 | 227.6 | 3,937 | 87.6 | 7,940 | 97.5% |
## Kernel profiles in the server
One 33,863-token real-code prompt plus 64 decode tokens under the torch profiler (`bench/profile-run.sh`,
`raw/prof-*.txt`). Times are summed GPU kernel time.
| Prefill, 33.9K tokens | Marlin, stock MoE | + multi-row prefill | + flat scheduler |
|---|---:|---:|---:|
| Total | 9,938 ms | 8,731 ms | 8,719 ms |
| MoE | 5,771 ms | 4,455 ms | 4,470 ms |
| Dense GEMM | 2,002 ms | 2,003 ms | 2,038 ms |
| All-reduce | 749 ms | 857 ms | 783 ms |
| Sparse attention | 555 ms | 555 ms | 568 ms |
| Decode, per step | Multi-row prefill kernel | + flat scheduler |
|---|---:|---:|
| Total | 28.9 ms | 23.5 ms |
| MoE | 16.1 ms | 8.9 ms |
| Dense GEMM | 8.0 ms | 8.2 ms |
| NCCL all-reduce | 0.6 ms | 1.6 ms |
All-reduce time per call grew with the flat scheduler, most likely one rank waiting for the other. Dense GEMMs now
cost as much as the MoE per decode step.
## Nsight Compute on the stock kernel
`kernels/ncu` runs came from `ncu --set full` on single `exl3_moe` launches (`ncu/*.summary.txt`).
| Stock `exl3_moe` | 6 tokens | 4,096 tokens |
|---|---:|---:|
| DRAM throughput | 18% | 10% |
| Issue slots busy | 51% | 56% |
| Registers per thread | 128 | 128 |
| Achieved occupancy | 33% | 33% |
| Integer ALU pipe | 31% | 34% |
| Tensor pipe | 11% | 13% |
| Top stall, cycles per issued instruction | barrier 1.61 | wait 1.38 |
Barrier stalls were 36–38% of stall samples in both cases. In decode, a serial scan over 384 expert counts was the
single hottest spot, about 8% of samples.
## Things that did not help
| Change | Effect |
|---|---|
| Shared-memory pipeline depth 3 → 5, 8, 12 stages | At most 4% faster (`raw/stage-sweep.txt`) |
| Expert group count 4–23 instead of 23 | Between 13% faster and 61% slower depending on batch size (`raw/bench-na.txt`) |
| 4× multi-row prefill (64 rows per tile decode) | Slower than 2× at every size: register spills at the 128-register limit |
| Wider N tiles on the decode path | Deadlocked in split-K |
## Hybrid dense GEMM, controlled A/B
Both runs use the flat scheduler (`raw/ab-flat.txt`, `raw/ab-flathyb.txt`).
| | Default | `DENSE_HYBRID_M=128`, 6 GiB KV |
|---|---:|---:|
| Prefill, 46K, 1 / 4 streams | 4,044 / 8,085 | 4,260 / 8,503 |
| Decode, 46K, 1 stream | 116.6 | 114.9 |
| DSpark mean acceptance length | 3.66 | 3.59 |
| GSM8K-200 | 98.5% | 95.5% |
|