Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions python/tokenspeed/runtime/models/deepseek_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
_is_blackwell = _platform.is_blackwell
_is_hopper_plus = _platform.is_hopper_plus
_device_sm = _platform.arch_version.major * 10 + _platform.arch_version.minor
_FUSED_A_MAX_M = 16 # measured cliff: wins to M=16, flat ~1.35x loss from 18 to 64


from tokenspeed.runtime.distributed import Mapping
Expand Down Expand Up @@ -463,6 +464,7 @@ def forward(
if (
self.use_min_latency
and x.size(0) > 0
and x.size(0) <= _FUSED_A_MAX_M
and block_scale is None
and (output_dtype is None or output_dtype == torch.bfloat16)
):
Expand Down
17 changes: 14 additions & 3 deletions python/tokenspeed/runtime/models/kimi_k3.py
Original file line number Diff line number Diff line change
Expand Up @@ -1874,12 +1874,23 @@ def forward(

# Router runs uncontended on main (3us; on aux it starves to 14us
# under concurrent GEMMs). When the selected experts need precomputed
# TopK, its single small CTA overlaps down_proj from the aux stream,
# followed by the shared chain. Kernel routing bypasses that CTA.
# TopK runs on the fork branch beside down_proj; routing bypasses it.
router_logits = self.gate(hidden_states)
routing_output_format = self._routing_output_format(ctx)
precompute_topk = routing_output_format.is_standard()
plan = self.comm.plan(num_tokens, hidden_states)
plan = self.comm.plan(
num_tokens,
hidden_states,
# Rank-uniform by construction: DP-EP gather replicates the phase.
is_decode=(
ctx is not None
and (
ctx.all_decode_or_idle
if self._gather_dp_tokens_for_moe
else ctx.forward_mode.is_decode()
)
),
)
if plan.lane is not None:
self.experts._situ_output_buffer = plan.lane[:, : self.routed_hidden]
else:
Expand Down
19 changes: 17 additions & 2 deletions python/tokenspeed/runtime/models/kimi_k3_comm.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def select_k3_moe_tail_tier(
tail_fusion_max_tokens: int,
fused_moe_ar: bool,
multimem_ok: bool,
is_decode: bool = False,
) -> K3MoETailTier:
"""Pick the tail tier; every input must be rank-uniform.

Expand All @@ -124,6 +125,8 @@ def select_k3_moe_tail_tier(
able and worth running at, 0 when absent.
fused_moe_ar: Whether the fused-AR execution plan is armed.
multimem_ok: Collectively-agreed multimem availability.
is_decode: Whether this forward is a decode (spec-verify included);
rank-uniform and stable between graph capture and replay.

Returns:
The best applicable ``K3MoETailTier``.
Expand All @@ -134,7 +137,12 @@ def select_k3_moe_tail_tier(
return K3MoETailTier.TAIL_FUSION
if not fused_moe_ar:
return K3MoETailTier.SEPARATE_REDUCE
if multimem_ok and MULTIMEM_AR_MIN_TOKENS <= num_tokens <= MULTIMEM_AR_MAX_TOKENS:
if (
multimem_ok
# Decode buckets skip multimem: same bytes, but it leaves the GPU idle there.
and not is_decode
and MULTIMEM_AR_MIN_TOKENS <= num_tokens <= MULTIMEM_AR_MAX_TOKENS
):
return K3MoETailTier.MULTIMEM_AR
return K3MoETailTier.FUSED_LANE_AR

Expand Down Expand Up @@ -587,7 +595,13 @@ def __init__(
# ------------------------------------------------------------------
# Routing
# ------------------------------------------------------------------
def plan(self, num_tokens: int, hidden_states: torch.Tensor) -> TailPlan:
def plan(
self,
num_tokens: int,
hidden_states: torch.Tensor,
*,
is_decode: bool = False,
) -> TailPlan:
"""Pick the tail tier and its forward-side obligations.

Every input must be rank-uniform (token count, graph phase and the
Expand All @@ -604,6 +618,7 @@ def plan(self, num_tokens: int, hidden_states: torch.Tensor) -> TailPlan:
),
fused_moe_ar=self.execution_plan.fused_moe_ar,
multimem_ok=self.state.multimem_ar_ok,
is_decode=is_decode,
)
if tier is K3MoETailTier.TAIL_FUSION:
# Full fusion: with the trtllm fused-AR plan armed and a
Expand Down
16 changes: 15 additions & 1 deletion test/gemm_tuning/tune_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,22 @@
(7168, 1536, 69, "kda_o_proj_shard"),
(1536, 7168, 92, "shared_gate_up_shard"),
(7168, 768, 92, "shared_down_shard"),
(1536, 1536, 12, "dspark_q_b_tp8"),
(7168, 1024, 12, "dspark_o_proj_tp8"),
(7168, 1792, 12, "dspark_down_tp8"),
(768, 1536, 0, "qb_tp16"),
(1792, 7168, 0, "dspark_gate_up_tp16"),
(2112, 14336, 0, "eagle3_fused_qkv_a_tp16"),
(2304, 7168, 0, "eagle3_gate_up_tp16"),
(7168, 512, 0, "o_proj_tp16"),
(7168, 896, 0, "dspark_down_tp16"),
(7168, 1152, 0, "eagle3_down_tp16"),
(2304, 1536, 0, "mla_q_b"),
(6288, 7168, 0, "kda_in_proj"),
(3648, 7168, 0, "mla_fused_qkv_a_gate"),
],
[1, 2, 3, 4, 5, 6, 7, 8], # observed range: the gates admit M <= 8
# Table keys on exact M; sweep the routed range with no holes.
list(range(1, 33)),
),
"qwen38_next_tp4": (
[
Expand Down
3 changes: 2 additions & 1 deletion test/runtime/test_kimi_k3_moe_fork_warmup.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ def _make_moe(fork: _SpyFork) -> SimpleNamespace:
defer_finalize=False,
)
comm = SimpleNamespace(
plan=lambda num_tokens, hs: plan,
# Absorb keyword axes so the stub does not pin plan's signature.
plan=lambda num_tokens, hs, **_: plan,
run=lambda *a, **k: hidden,
reduce_scatter_shared=lambda x: x,
reduce_project_routed=lambda x: x,
Expand Down
139 changes: 138 additions & 1 deletion tokenspeed-kernel/python/tokenspeed_kernel/ops/gemm/routed_gemv.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,106 @@
# wrappers' per-call view cannot diverge. Measured; see module docstring.
MEASURED_ROUTE: MappingProxyType[tuple[int, int, int], str] = MappingProxyType(
{
# K3 drafters at TP16; shapes read from the model code, not configs.
# both q_b N=768 K=1536
(2, 768, 1536): "skinny",
(3, 768, 1536): "skinny",
(4, 768, 1536): "skinny",
(5, 768, 1536): "skinny",
(6, 768, 1536): "skinny",
(7, 768, 1536): "skinny",
(8, 768, 1536): "skinny",
(9, 768, 1536): "skinny",
(10, 768, 1536): "skinny",
(11, 768, 1536): "skinny",
(12, 768, 1536): "skinny", # re-swept: 1.49x vs cuBLAS; edges tgv by ~1%
(13, 768, 1536): "tgv",
(14, 768, 1536): "tgv",
(15, 768, 1536): "tgv",
(16, 768, 1536): "tgv",
(17, 768, 1536): "tgv",
(18, 768, 1536): "tgv",
(19, 768, 1536): "tgv",
(20, 768, 1536): "tgv",
(21, 768, 1536): "tgv",
(22, 768, 1536): "tgv",
(23, 768, 1536): "tgv",
(24, 768, 1536): "tgv",
(25, 768, 1536): "tgv",
(26, 768, 1536): "tgv",
(27, 768, 1536): "tgv",
(28, 768, 1536): "tgv",
(17, 2304, 7168): "tgv",
(18, 2304, 7168): "tgv",
(19, 2304, 7168): "tgv",
(21, 2304, 7168): "tgv",
(22, 2304, 7168): "tgv",
(24, 2304, 7168): "tgv",
(32, 2304, 7168): "tgv",
(29, 768, 1536): "tgv",
(30, 768, 1536): "tgv",
(31, 768, 1536): "tgv",
(32, 768, 1536): "tgv",
# dspark gate_up N=1792 K=7168
(2, 1792, 7168): "skinny",
(3, 1792, 7168): "skinny",
(4, 1792, 7168): "skinny",
# dspark fused_qkv_a (2112x7168): handled by dsv3_fused_a_gemm upstream.
# eagle3 fused_qkv_a N=2112 K=14336
(1, 2112, 14336): "skinny",
(2, 2112, 14336): "skinny",
(3, 2112, 14336): "skinny",
# eagle3 gate_up N=2304 K=7168
(2, 2304, 7168): "skinny",
(3, 2304, 7168): "skinny",
# both o_proj N=7168 K=512
(1, 7168, 512): "tgv",
# dspark down N=7168 K=896
(1, 7168, 896): "tgv",
(2, 7168, 896): "tgv",
(3, 7168, 896): "tgv",
(4, 7168, 896): "tgv",
(5, 7168, 896): "tgv",
(6, 7168, 896): "tgv",
(7, 7168, 896): "tgv",
(8, 7168, 896): "tgv",
# eagle3 down N=7168 K=1152
(1, 7168, 1152): "tgv",
(2, 7168, 1152): "tgv",
# eagle3 fc: (7168, 21504) was the unsharded width; TP16 shard unmeasured.
# M values the 1/2/4/8 sweep skipped; GB200 reproduced 39/40 verdicts.
# n1152_k1536 N=1152 K=1536
(9, 1152, 1536): "skinny",
(10, 1152, 1536): "tgv",
(11, 1152, 1536): "tgv",
(12, 1152, 1536): "tgv",
(13, 1152, 1536): "tgv",
(14, 1152, 1536): "tgv",
(15, 1152, 1536): "tgv",
(16, 1152, 1536): "tgv",
# shared gate_up shard N=1536 K=7168
(3, 1536, 7168): "skinny",
# MLA q_b N=2304 K=1536
(5, 2304, 1536): "tgv",
(6, 2304, 1536): "tgv",
(7, 2304, 1536): "tgv",
# n2880_k7168 N=2880 K=7168
(12, 2880, 7168): "tgv", # 1.095-1.102x, three runs
(13, 2880, 7168): "tgv",
(15, 2880, 7168): "tgv",
# MLA fused qkv_a + gate N=3648 K=7168
(3, 3648, 7168): "skinny",
# KDA in_proj N=6288 K=7168
(3, 6288, 7168): "tgv", # re-swept: 1.12x vs cuBLAS; edges skinny by ~1%
(5, 6288, 7168): "tgv",
(6, 6288, 7168): "tgv",
# shared down shard N=7168 K=768
(3, 7168, 768): "tgv",
(5, 7168, 768): "tgv",
(6, 7168, 768): "tgv",
(7, 7168, 768): "tgv",
(1, 3584, 7168): "skinny", # MoE latent down-proj, 92 calls/step
# KDA o_proj shard N=7168 K=1536
(1, 7168, 1536): "tgv",
(2, 7168, 1536): "tgv",
(4, 7168, 1536): "tgv",
Expand All @@ -78,6 +177,28 @@
# (1, 2304, 1536) mla_q_b stays on rowcta: 2.48 vs skinny 2.53.
# M > 1 (small batches, speculative verify) vs the cublas incumbent.
(2, 3584, 7168): "skinny", # 9.20 vs 11.67 (1.27x)
# TP8 DSpark drafter, shapes observed at the launch point on GB300.
# eagle3 drafter TP8 widths; nothing past M=4 clears the margin.
(2, 4608, 7168): "tgv", # 1.10x
(1, 7168, 2304): "tgv", # 1.10x
(2, 7168, 2304): "tgv", # 1.10x
(2, 1536, 1536): "skinny", # 2.18x
(3, 1536, 1536): "skinny", # 1.95x
(4, 1536, 1536): "skinny", # 1.83x
(5, 1536, 1536): "skinny", # 1.68x
(6, 1536, 1536): "skinny", # 1.63x
(7, 1536, 1536): "skinny", # 1.55x
(8, 1536, 1536): "tgv", # 1.54x
(1, 7168, 1024): "tgv", # 1.24x
(2, 7168, 1024): "tgv", # 1.16x
(3, 7168, 1024): "tgv", # 1.15x
(4, 7168, 1024): "tgv", # 1.16x
(5, 7168, 1024): "tgv", # 1.15x
(6, 7168, 1024): "tgv", # 1.16x
(7, 7168, 1024): "tgv", # 1.15x
(8, 7168, 1024): "tgv", # 1.15x
(1, 7168, 1792): "tgv", # 1.13x
(2, 7168, 1792): "tgv", # 1.10x
(4, 3584, 7168): "skinny", # 10.38 vs 11.21 (1.08x)
(2, 6288, 7168): "tgv", # 15.29 vs 17.07 (1.12x)
(4, 6288, 7168): "tgv", # 15.25 vs 17.26 (1.13x)
Expand All @@ -92,13 +213,29 @@
# dispatch during a live bs=1 run rather than scaled from TP8: only
# 1152x1536 halves, 3584x7168 is not TP-sharded at all, 2880x7168 has
# no TP8 analogue, and TP8's largest entry (6288x7168) never reaches
(17, 1152, 1536): "tgv",
(18, 1152, 1536): "tgv",
(19, 1152, 1536): "tgv",
(20, 1152, 1536): "tgv",
(21, 1152, 1536): "tgv",
(22, 1152, 1536): "tgv",
(23, 1152, 1536): "tgv",
(24, 1152, 1536): "tgv",
(25, 1152, 1536): "tgv",
(26, 1152, 1536): "tgv",
(27, 1152, 1536): "tgv",
(28, 1152, 1536): "tgv",
(29, 1152, 1536): "tgv",
(30, 1152, 1536): "tgv",
(31, 1152, 1536): "tgv",
(32, 1152, 1536): "tgv",
# decode_gemv here. Same >= 4% margin, same cold-L2 tuner.
(3, 3584, 7168): "skinny", # 9.54 vs 11.23 (1.18x)
(1, 2880, 7168): "skinny", # 7.13 vs rowcta 8.61 (1.21x)
(2, 2880, 7168): "skinny", # 7.95 vs 9.80 (1.23x)
(3, 2880, 7168): "skinny", # 8.24 vs 9.77 (1.19x)
(4, 2880, 7168): "skinny", # 9.32 vs 9.78 (1.05x)
# 3584 and 2880 stay on the incumbent at M >= 5: skinny inverts there
# 3584 and 2880 leave skinny at M >= 5: skinny inverts there
# (15.85 vs cublas 11.06 at M=8, 3584x7168), so the win is not simply
# "skinny for small M" and the boundary has to be measured per width.
(2, 1152, 1536): "skinny", # 1.99 vs 4.85 (2.44x)
Expand Down
Loading
Loading