In a graph-off attribution trace, TokenSpeed spends less time in non-collective device kernels than SGLang. In a separate graph-on serving run, its batch-1 TPOT is slower. That is a measured paradox, not yet a measured location: host scheduling, synchronization and exposed communication are the next things to trace.
The kernel trace and the serving benchmark point in opposite directions. The useful result is not “the kernels explain the engine”; it is that they do not.
TokenSpeed is LightSeek's MIT-licensed inference engine. Its AMD path is concrete: an AMD image, a gfx950 Gluon MLA kernel, a Gluon SiTU MoE path, and per-commit MI35x CI. We already had SGLang traces for the same checkpoint on the same node, so both stacks could be measured on one day with one attribution method.
Four operation-level matched groups are faster on TokenSpeed; routed-expert GEMM is the one loss, and it is confounded by EP8/a16w4 versus TP8/a8w4. The aggregate non-collective device time is still lower and the total launch count is still smaller. Yet graph-on batch-1 serving is slower.
four measurements · two execution modes四个测量 · 两种执行模式
Graph-off attribution: 17,108 vs 20,769 µs of non-collective device work; 2,375 vs 3,372 total launches. TokenSpeed has 187 Iris layer all-reduces plus 2 RCCL dispatches; SGLang has 187 measured collectives. Separate graph-on serving: 22.19 vs 19.39 ms TPOT at batch 1. The underlying rows are in block-comparison.csv, launches-per-step.csv and the attributed kernel table.
What this rules out is a deficit in summed kernel work or an excess launch count. It does not rule out higher per-launch host cost, synchronization, different graph replay behavior, collective latency or poor overlap. Because the two time totals come from different execution modes, they cannot be subtracted into a measured “gap between kernels.” The host/runtime path is the leading hypothesis and the next profiling target.
One node, 8× AMD Instinct MI355X (gfx950, 288 GiB each), ROCm 7.2. The model is moonshotai/Kimi-K3: 2.78 T total parameters, 105.4 B active, 93 layers, 896 routed experts at top-16 plus 2 shared, situ activation, routed experts in compressed-tensors MXFP4 group-32 and everything else BF16. TokenSpeed's pinned config defines the layer and expert counts and the 69 KDA / 24 NoPE-MLA split.
Each stack runs its own recommended configuration. That makes this best-config against best-config rather than a controlled A/B, which is the honest way to answer "which should I deploy" but means some differences are structural rather than incidental.
Each stack's own recommendation for this chip.各自对这块芯片的推荐值。
MoE backendMoE 后端
auto → Gluon SiTUauto → Gluon SiTU
AITER situv2 a8w4
The comparison this experiment exists for.这个实验的核心对比对象。
KV cache
fp8
bf16
Favours TokenSpeed — cannot explain a deficit.对 TokenSpeed 有利—— 解释不了它的劣势。
Scheduler调度器
FlatKV (C++ FSM)FlatKV(C++ FSM)
radix, disabled hereradix, 本次关闭
Both run without prefix caching.两边都不开前缀缓存。
Speculative decoding投机解码
none无
off (nospec) / DSpark关(nospec)/ DSpark
nospec is the like-for-like row.nospec 才是同类对比那一行。
Three server boots, not one universal configuration三次 server boot, 不是一条万能配置
The serving table combines three deliberate profiles. Increasing --max-num-seqs reserves more graph state and reduced the reported token pool from 4.47 M to 3.56 M; the 8K-input workload also needs a 16K model limit because input plus output is 9,216 tokens. The exact invocations are parameterized in ts-bench.sh.
The fp8 KV cache deserves a note because it cuts the wrong way for the conclusion: fp8 halves the bytes moved per cached token relative to bf16, so it is a handicap given to TokenSpeed. Any deficit we find survives despite that advantage.
the flag that would have silently ruined it一个会静默毁掉对比的 flag
Both harnesses print similarly named metrics, but --random-range-ratio has opposite semantics. SGLang samples from [full_len × ratio, full_len], so ratio 1 means exact length. TokenSpeed samples symmetrically around the target and requires a ratio in [0, 1), so ratio 0 is exact. Every number here uses exact lengths on both sides.
None of these are exotic, all four are load-bearing, and the documented recipe walks into three of them. They are worth recording because each one is the kind of failure that costs an afternoon and leaves no trace in a benchmark table.
2.1 · A pinned dependency that was never published2.1 · 一个从未发布过的依赖被硬钉住
The pinned runtime dependency list requires tokenspeed-mooncake>=0.3.12.post20260725. That package had no resolvable release in this environment, so a normal install stopped at from versions: none. Mooncake serves P/D KV transfer and is not used by this aggregated single-node run with --disable-kvstore. We installed the remaining dependencies, omitted that one entry, then installed the runtime with --no-deps; the exact workaround is in ts-build-step6.sh.
This is the subtle one, and it fails silently. torch 2.11.0+rocm7.2 requires triton-rocm==3.6.0. xgrammar declares a bare triton dependency. Both distributions install into the sametriton/ directory, so pip installs stock triton 3.7.1 from PyPI on top of the ROCm build and whichever landed last wins:
$ python -c "import triton; print(triton.__version__)"
3.7.1# stock PyPI build — not the ROCm one torch asked for
Nothing errors. You simply run a Triton that was not built for the platform. The fix is to uninstall triton and reinstall triton-rocm==3.6.0 after the dependency set settles. The Gluon gfx950 code is isolated from that collision because every compiler symbol enters through one namespaced indirection module.
# tokenspeed_kernel_amd/_triton.pyimport tokenspeed_triton as triton
import tokenspeed_triton.experimental.gluon.language as gl
from tokenspeed_triton.experimental.gluon.language.amd.cdna4 import (
async_copy as cdna4_async_copy,
)
Because tokenspeed_triton is a distinct top-level module name (version 3.8.10), it can never collide with whatever occupies triton/. Vendoring a compiler under its own namespace costs one file and buys immunity from every dependency resolver that will ever run against this environment.
2.3 · The documented AMD recipe OOMs on boot2.3 · 文档里的 AMD recipe 一启动就 OOM
Following the pinned AMD K3 recipe exactly, our server loaded all 96 shards, captured decode graphs, then OOMed during prefill graph capture at 751 seconds. The timing and free-memory values below are observations from our server log, not source constants:
After weights and the KV pool there were roughly 20 GB left per GPU, not enough for all 40 prefill buckets. We added --disable-prefill-graph. This is not an invented tuning flag: TokenSpeed's own MI35x perf server command already passes it.
2.4 · It cannot share a Python environment with SGLang2.4 · 它没法跟 SGLang 共用一个 Python 环境
The same dependency file pins torch==2.11.0 and transformers==5.12.0; the SGLang container uses a custom torch 2.9.1+rocm7.2.0. We therefore built TokenSpeed in its own venv, following the recorded installation order. This means the engine comparison is necessarily cross-environment, even though hardware, checkpoint and measurement day are held fixed.
After those fixes, each boot took about 11 minutes, dominated by reading 1.5 TB of weights. Profile A reported max_total_num_tokens=4466304; the two max-num-seqs=128 profiles reported 3556992, as listed in §01.
Exact-length random workloads, temperature 0, ignore_eos, prefix caching off; output tokens/s is aggregated over all 8 GPUs. SGLang appears twice: nospec is the engine-level like-for-like row because TokenSpeed ran without speculation, while DSpark is the best tuned deployment result on this node. TokenSpeed's raw responses are archived as serving JSON.
Plate I · Output throughput vs concurrency · ISL 1024 / OSL 1024图版 I · 输出吞吐 vs 并发 · ISL 1024 / OSL 1024tok/s · 8 GPU
TokenSpeed's curve is flat where the other two climb. Its best point anywhere — 487 tok/s at concurrency 128 — still sits below SGLang's 848 at concurrency 32. SGLang has no measurement at 64 or 128 on this workload, so those lines stop.
Single stream is respectable. TokenSpeed's TPOT holds at 21.5–22.2 ms from 1 K to 8 K input, essentially flat, while DSpark's advantage decays as its accept length falls from 2.51 to 2.34. Concurrency is where it comes apart: 3.7x behind at both 8 and 32.
this validates the single-stream build这能验证单流 build
TokenSpeed's own CI reference for 4K/1K at concurrency 1 is 43.05 tok/s per user on 8× MI350X, gated at 42. We measured 43.98 on MI355X, 2% above that reference. This validates the single-stream build; it does not, by itself, validate the high-concurrency scheduler path.
The more useful detail is what that CI file gates: --max-num-seqs 1, --cudagraph-capture-sizes 1, and one reference row, 1: [42, 5.2]. The AMD K3 gate therefore cannot detect a batched-throughput regression. That missing coverage does not explain the gap, but it explains why the gap can remain invisible to CI.
At concurrency 128 with 8K prompts, SGLang delivered 890 output tok/s and 8,012 total tok/s. TokenSpeed's archived benchmark result records 1 completion and 127 failures after 3,600 seconds. This is not ordinary saturation: useful progress nearly stops.
Plate II · FlatKV page pool under 128 × 9,216-token requests图版 II · 128 个 9,216-token 请求下的 FlatKV 页池live
Runtime log, Profile C: max_total_num_tokens=3556992; scheduler config: 3,298 device pages at block size 128. K3 publishes one full-attention group plus three linear-attention state groups, as defined by its cache spec.
The server log contains 9,256 flat retract ... to unwedge the pool events; the exact message comes from resolveFlatStarvation. The observed loop repeatedly retracts a holder, admits more work, and returns to starvation while instantaneous decode drops as low as 0.02 tok/s. The public JSON proves the end result; the retract count and instantaneous floor are server-log observations.
The proposed fix is a hypothesis: make admission account for all four cache-group demands before accepting a batch, then queue requests that cannot fit. Success means bounded or zero retractions and monotonic progress under 8K × 128—not merely a higher average throughput number.
The documented --max-model-len 8192 is not the explanation: Profile C raised it to 16,384, booted with max_req_input_len=16383, and completed the same 8K/1K shape at concurrency 1. The failure appears only under concurrency in this experiment.
Both sides use batch 1, ISL 4096, TP8, no speculative decoding, graphs off, device-kernel duration summed per decode step, and collective durations excluded. The complete capture sequence—not just a prose description—is in ts-profile.sh.
Why graph off. Inside a replayed HIP graph the individual kernels are not separately attributable — you get one opaque replay. The SGLang baseline was captured graph-off for the same reason, so the two remain comparable. The cost is that eager wall-clock time is meaningless here; only the summed device time is.
Why collective durations are excluded. In eager decode the small all-reduce kernels busy-wait for peers, so recorded duration is dominated by rank launch skew. TokenSpeed's collective rows sum to 67.6 ms/step against 17.1 ms of non-collective work—physically impossible as a wall-time decomposition. SGLang's graph-off baseline has the same issue. Dispatch identity and count remain evidence: TokenSpeed has 187 Iris layer all-reduces plus 2 RCCL calls; SGLang has 187 measured collectives.
Attaching Proton to the already-initialized ROCm server failed with error 16 on every rank in our run. We therefore used TokenSpeed's runtime torch/roctracer endpoint with GPU activities, which produced the same chrome-trace format as the SGLang baseline. This is a recorded runtime workaround in the capture script, not a general claim that Proton cannot profile ROCm when initialized earlier.
One kernel genuinely spans blocks. _kimi3_projection_gemv_kernel backs four MoE call sites per layer plus kimi3_qkvfab_projection in each KDA layer. That predicts 4 × 92 + 69 = 437 dispatches, exactly matching the trace; its time is split 368/69. The 93-dispatch vendor GEMM is the once-per-layer attention output projection and is split 69/24.
Plate III · One decode step, laid end to end · bs 1 · ISL 4096 · graph off图版 III · 一个 decode step 首尾展开 · bs 1 · ISL 4096 · 关 graphµs / step
In graph-off attribution, every named model block is shorter on TokenSpeed; glue is the one aggregate segment that grows. The graph-on TPOT annotation is a separate measurement and must not be subtracted from this stack.
The two stacks bucket their per-block "misc" slightly differently, so read the block rows as indicative and the total as solid. §07 compares only the kernel groups whose mapping is unambiguous.
Glue is the one aggregate block TokenSpeed loses: 2.786 vs 1.290 ms. Its trace rows include RMSNorm, _add3, fp8 copies, Memcpy DtoD and a generic GEMV. SGLang's ROCm path calls AITER's fused residual-add + RMSNorm + fp8 quantization; add_rmsnorm_quant_kernel is the runtime-emitted AITER kernel name, not a symbol defined in the SGLang repository. At 93 layers, avoiding two extra tensor passes is material even when each individual launch is small.
Four rows are matched by output role and layer count; they are not instruction-for-instruction identical. The fifth—routed-expert GEMM—is intentionally shown because it is important, but it also changes parallel layout and activation precision. Read the first four as operation-level implementation comparisons and the fifth as a strategy comparison.
Plate IV · Matched kernel groups · decode, bs 1, ISL 4096图版 IV · 精确匹配的核函数组 · decode, bs 1, ISL 4096µs / step
Four wins and one loss. The single largest absolute gap is routing, and the one place AITER wins is the expert GEMM itself — which is also the one row confounded by a parallelism difference.
7.1 · Gluon MLA decode — 2.77x, and the cleanest win7.1 · Gluon MLA decode—— 2.77 倍, 最干净的一场胜利
SGLang runs MLA decode as a two-stage split-KV Triton attention: _fwd_grouped_kernel_stage1 scans the KV cache in splits and _fwd_kernel_stage2 reduces them. The trace measures 50.5 µs per MLA layer. TokenSpeed's _mla_decode_gluon plus softmax/reduce-V measures 18.3 µs per layer.
Why the gap is real and not a tuning artifact: at batch 1 with a 4 K context, decode attention has an arithmetic intensity near one — it is a pure streaming read of the KV cache, and the only thing that matters is how close to peak bandwidth the load instructions get. A split-K template must choose its split count without knowing the access pattern, materialise partial results, and reduce them in a second pass over memory. A kernel written against one architecture can size its tiles to the cache line and the LDS budget and skip the second pass. That is a structural advantage of hand-writing for a fixed target, and it is exactly the regime where it pays.
7.2 · The router — 6.51x, and the largest prize on the board7.2 · Router—— 6.51 倍, 桌上最大的一笔
Selecting top-16 of 896 experts and preparing grouped-GEMM routing costs SGLang 2.96 ms per decode step across trace-emitted AITER kernels: grouped_topk, multi-phase opus_moe_sorting, fused_mx_quant_moe_sort and moe_reduction. TokenSpeed's measured routing row is one 0.45 ms _kimi3_sigmoid_bias_topk_kernel, which performs sigmoid, bias, top-k, normalization and optional logical-to-physical mapping.
Plate V · Routing 896 experts: pipeline vs one pass图版 V · 给 896 个 expert 做路由: 流水线 vs 一趟per layer × 92
With 896 experts the routing metadata is large enough that writing it out and reading it back between kernels costs more than computing it. Fusing sigmoid, bias and top-k into one pass deletes those round trips.
That single kernel is worth 2.5 ms per decode step, which is more than the entire end-to-end deficit TokenSpeed carries. Nothing about it is AMD-specific or Gluon-specific: it is the observation that when intermediate state is proportional to a large constant — here 896 — the cost of materialising it dominates the cost of producing it, so the boundary between kernels should not fall there. Delete the proper nouns and the idea still stands.
7.3 · Where AITER wins, and why the row is confounded7.3 · AITER 赢在哪里, 以及这一行为什么被污染
SGLang's trace-emitted mfma_moe1/mfma_moe2 MXFP4 kernels run the routed experts in 1.44 ms, against 2.39 ms for TokenSpeed's Gluon stage 1 and stage 2. Two caveats make this the weakest row in the table:
Different parallelism, different shapes. TokenSpeed runs EP8, so each rank owns 112 whole experts and at batch 1 touches only the few its tokens route to. SGLang runs pure TP8, where every rank runs all 16 experts at one-eighth width. Those are not the same GEMM, so this compares two strategies as much as two kernels.
Different activation precision. AITER's path is a8w4 — 8-bit activations — against Gluon's a16w4. AITER is moving half the activation bytes, which at batch 1 is most of what the kernel does.
The honest reading is not "AITER's GEMM is better" but "8-bit activations plus TP sharding beat 16-bit activations plus EP sharding at batch 1 on this shape". That is a useful finding, and it points at quantising the activations rather than rewriting the kernel.
7.4 · KDA is a wash, and both are Triton7.4 · KDA 打平, 而且两边都是 Triton
TokenSpeed's batch-1 KDA recurrence dispatches to a Triton implementation, like SGLang's path. The measured recurrence + short-conv rows land within 14%. Our occupancy estimate is 48 active workgroups on 256 CUs for this fixed-size batch-1 update; that is an analysis of the launch shape, not a source constant. Batching is the direct experiment for testing whether exposed parallelism—not kernel language—is the limit.
What the evidence does—and does not—locate证据定位了什么、 没定位什么
Plate VI · Four measurements, two execution modes图版 VI · 四个测量, 两种执行模式bs 1 · ISL 4096
The first three rows come from graph-off attribution; the fourth comes from graph-on serving. Together they prioritize the host/runtime path, but they do not measure its duration or prove one component is responsible.
Concurrency strengthens the prioritization but not the causal claim. At concurrency 8 and 32, TokenSpeed is about 3.7× behind SGLang nospec. That shape is consistent with scheduler, batching, graph-shape or overlap inefficiency; it is not evidence that one fixed per-step tax simply grew by 3.7×. The FlatKV failure at 8K × 128 provides a concrete scheduler bug, while the smaller-workload gap still needs localization.
Capture a synchronized host + GPU timeline with graph replay enabled: scheduler decision time, Python/C++ handoff, launch enqueue, stream waits, collective start/end and idle gaps. Only that trace can partition the 22.19 ms step and turn “host/runtime” from a category into a component-level diagnosis.
Localize the host and scheduler path定位 host 与 scheduler 路径
Highest uncertainty · broadest possible payoff不确定性最高 · 潜在收益范围最大
Evidence证据
Lower graph-off kernel sum and fewer launches coexist with 14% slower graph-on TPOT; the gap widens under concurrency.关 graph 的 kernel 合计更低、 launch 更少, 却与开 graph TPOT 慢 14% 同时存在; 并发下差距继续扩大。
Change动作
Instrument scheduler decision time, Python/C++ handoff, graph enqueue, stream waits and collective overlap around overlap scheduling. Capture one synchronized host + GPU timeline with graphs on.围绕 overlap scheduling 记录 scheduler 决策、 Python/C++ 交接、 graph enqueue、 stream wait 与 collective overlap; 在开 graph 下抓一份同步 host + GPU timeline。
Pass通过
At least 95% of the 22.19 ms step is assigned to named intervals; then an A/B change reduces the identified idle or serialized interval and improves conc-8/32 throughput without regressing batch-1 TPOT.至少 95% 的 22.19 ms step 能归入具名区间; 随后 A/B 改动缩短已定位的 idle 或串行区间, 提高并发 8/32 吞吐且不回退 batch-1 TPOT。
02
Make FlatKV admission page-aware让 FlatKV admission 感知 page 预算
Confirmed failure mode · correctness before speed已确认失败模式 · 先保证进度再谈速度
Evidence证据
At 8K × 128, only 1 request completes; the log records 9,256 retractions across one full-attention and three linear-state cache groups.8K × 128 仅完成 1 个请求; log 在一组 full-attention 与三组 linear-state cache 上记录 9,256 次 retract。
Change动作
Before admitting a batch, compute all group demands and queue work that cannot fit. Start at flatPoolWedged / flatAdmit; keep retraction as bounded recovery, not normal flow control.接纳 batch 前计算所有 cache group 的需求, 装不下的请求排队。 从 flatPoolWedged / flatAdmit 入手; retract 只作为有界恢复, 不再承担正常流控。
Pass通过
All 128 requests make monotonic progress, retractions are zero or strictly bounded, no OOM occurs, and queue latency degrades smoothly rather than collapsing.128 个请求持续单调完成, retract 为零或严格有界, 不发生 OOM, 排队延迟平滑劣化而非停滞。
03
Fuse the repeated glue epilogue融合重复的 glue epilogue
Measured 1.50 ms opportunity · narrow kernel change实测 1.50 ms 机会 · 改动边界较窄
Evidence证据
TokenSpeed glue is 2.786 ms vs 1.290 ms; repeated RMSNorm, residual add, fp8 casts and copies account for the difference.TokenSpeed glue 为 2.786 ms, SGLang 为 1.290 ms; 重复 RMSNorm、 residual add、 fp8 cast 与 copy 构成主要差距。
Change动作
Add an AMD fused residual-add + RMSNorm + fp8-output path at TokenSpeed's RMSNorm boundary, eliminating intermediate casts/copies where the consumer already needs fp8.在 TokenSpeed 的 RMSNorm 边界加入 AMD residual-add + RMSNorm + fp8-output 融合路径; 下游本来就需要 fp8 时不再物化中间 cast/copy。
Pass通过
Numerics match the unfused path, the relevant launch count falls, and graph-off glue time approaches the 1.29 ms SGLang reference without moving cost into another bucket.数值与未融合路径一致, 相关 launch 数下降, graph-off glue 时间接近 SGLang 的 1.29 ms 参考, 且成本没有转移到其它 bucket。
Shared guardrail. Extend AMD K3 CI beyond concurrency 1: gate at least conc 1, 8 and 32 for 4K/1K, plus a bounded-progress 8K/1K admission test. CI coverage is not an optimization; it is what keeps each optimization from silently regressing.
Deployment decision for this measured setup对这组实测配置的部署判断
Choose SGLang for concurrent serving on this exact 8× MI355X setup and these workloads. TokenSpeed is close enough at single stream to be worth fixing, but its measured batched throughput and 8K admission behavior are not production-ready. This conclusion is scoped to the pinned commits, flags and workloads above.
A tokens/s benchmark alone would have sent optimization effort toward TokenSpeed's AMD kernels. The graph-off attribution says the opposite: its non-collective kernel sum is already lower. The general lesson is narrower and more useful—separate device work, launch structure, communication and host scheduling, and do not turn measurements from different execution modes into a causal subtraction.
Exactly how we served Kimi-K3 with TokenSpeed我们实际如何用 TokenSpeed 起 Kimi-K3
This is the operational recipe used for every TokenSpeed number above: pinned source, isolated ROCm environment, one base serve command, and three explicit capacity profiles.
10.1 · Build the pinned source in an isolated venv10.1 · 在独立 venv 中构建固定版本
We used TokenSpeed d50bb481. Package order matters: the in-tree AMD package precedes the generic kernel build; the scheduler is compiled with FlatKV; runtime dependencies omit unavailable Mooncake; ROCm Triton is restored last. The full executable records are ts-build.sh and ts-build-step6.sh.
MAXLEN and MAXSEQS are the only profile variables. Everything else stays fixed. The model's K3 notes explain FlatKV, fp8 KV and MoonViT data mode; the pinned AMD recipe supplies the TP8/EP8 backend choices. Our two deviations are highlighted.
Text model uses TP8; each rank runs the 12-head vision encoder at TP1 on whole images, as required by K3 parallelism.文本模型 TP8; 每个 rank 以 TP1 处理完整图像, 满足 K3 并行约束。
expert parallel
Distributes 896 routed experts across 8 ranks; the measured path is TP8 + EP8.把 896 个 routed expert 分布到 8 个 rank; 实测路径为 TP8 + EP8。
Matches the pinned AMD recipe. The checkpoint has no KV scales, so the loader's default scale is part of this result.匹配固定版本的 AMD recipe。 Checkpoint 没有 KV scale, 因而 loader 默认 scale 也是本结果的一部分。
disable kvstore
Runs one aggregated node without P/D disaggregation or Mooncake transfer.运行单节点聚合服务, 不启用 P/D 分离或 Mooncake 传输。
no prefix cache
Matches the SGLang --disable-radix-cache baseline.匹配 SGLang --disable-radix-cache baseline。
10.4 · Run the three profiles actually measured10.4 · 运行实际测量的三个 profile
The harness takes the shared-node lock, starts the server above, waits on /readiness, sends an OpenAI-compatible chat smoke test, runs the listed workloads, archives JSON, then stops the server. These are the exact profile inputs:
Serving results use normal graph replay. Exact lengths require TokenSpeed ratio 0. Kernel attribution is a separate restart with --enforce-eager; it is not the serving configuration.