[Bugfix] Support non-power-of-2 top_k in legacy triton_kernels routing (#46406)

Signed-off-by: Woosuk Kwon <woosuk@inferact.ai> Co-authored-by: Claude <noreply@anthropic.com>

Woosuk Kwon committed Jun 24, 2026 at 13:52 UTC 6a1570711c9ab6dd356f68f5728b845711901e45
1 file changed +238
vllm/model_executor/layers/fused_moe/experts/gpt_oss_triton_kernels_moe.py
+238
@@ -220,6 +220,241 @@ def _patch_make_bitmatrix_metadata() -> None:
220 _bm.make_bitmatrix_metadata = _make_bitmatrix_metadata_pow2_safe
221
222
223 +def _patch_legacy_routing_for_nonpow2_topk() -> None:
224 + """Monkey-patch the legacy (v3.5.1) triton_kernels routing path to support
225 + non-power-of-2 top_k (e.g. DeepSeek-V4 top_k=6).
226 +
227 + The bundled ``_routing_compute_indx`` does ``tl.arange(0, N_EXPTS_ACT *
228 + BLOCK_M)``, which fails to compile when ``N_EXPTS_ACT`` (top_k) is not a
229 + power of 2 (6 * 32 = 192). This installs a pow2-safe variant that pads the
230 + ``tl.arange`` to the next power of 2, strides by the real per-block size,
231 + and masks the padded tail so it neither loads the next block's gates nor
232 + writes any output. For power-of-2 top_k it is identical to the original.
233 +
234 + A matching ``sort_tokens`` is installed that threads the padded size into
235 + the patched kernel. Only needed on the legacy path; the v3.6+ SparseMatrix
236 + path is handled by ``_patch_make_bitmatrix_metadata``.
237 + """
238 + import triton
239 + import triton.language as tl
240 +
241 + # Import via the `triton_kernels` alias (set up by has_triton_kernels) so
242 + # we patch the SAME module object that `make_routing_data` consumes. The
243 + # `vllm.third_party.triton_kernels.routing` path is a *different* module
244 + # object under the import alias, so patching it would have no effect.
245 + try:
246 + import triton_kernels.routing as _routing
247 + from triton_kernels.routing_details import _routing_compute as _rc
248 + except ImportError:
249 + return
250 +
251 + _keyed_add = _rc._keyed_add
252 + _expt_data_compute = _rc._expt_data_compute
253 +
254 + @triton.jit
255 + def _routing_compute_indx_pow2(
256 + pid_m,
257 + GatherIndx,
258 + ScatterIndx,
259 + GateScal,
260 + ExptScal,
261 + ExptIndx,
262 + PartialOffs,
263 + stride_pm,
264 + stride_pn,
265 + TokensStart,
266 + n_tokens,
267 + BLOCK_M: tl.constexpr,
268 + N_EXPTS_ACT: tl.constexpr,
269 + BLOCK_SIZE_PADDED: tl.constexpr,
270 + ):
271 + if isinstance(n_tokens, tl.tensor) and n_tokens.dtype.is_ptr():
272 + n_tokens = tl.load(n_tokens)
273 + n_gates = n_tokens * N_EXPTS_ACT
274 + BLOCK_SIZE: tl.constexpr = N_EXPTS_ACT * BLOCK_M
275 + tl.static_assert(BLOCK_SIZE_PADDED <= 32768)
276 + local_offs = tl.arange(0, BLOCK_SIZE_PADDED)
277 + offs = pid_m * BLOCK_SIZE + local_offs
278 + expert = tl.load(
279 + ExptIndx + offs,
280 + mask=(local_offs < BLOCK_SIZE) & (offs < n_gates),
281 + other=-1,
282 + ).to(tl.uint32)
283 + kv_pairs = ((expert << 16) | local_offs).to(tl.uint32)
284 + kv_pairs = tl.sort(kv_pairs, 0)
285 + expert = kv_pairs >> 16
286 + offs = pid_m * BLOCK_SIZE + (kv_pairs & 0xFFFF)
287 + mask = expert != 0xFFFF
288 + gate_scal = tl.load(ExptScal + offs, mask=mask)
289 + x = kv_pairs & 0xFFFF0000 | 0x00000001
290 + run_lengths = tl.associative_scan(x, 0, _keyed_add)
291 + exclusive_run_lengths = (run_lengths - 1) & 0xFFFF
292 + gates = tl.load(PartialOffs + pid_m * stride_pm + expert * stride_pn, mask=mask)
293 + gates += tl.load(TokensStart + expert, mask=mask)
294 + gates += exclusive_run_lengths
295 + tl.store(ScatterIndx + offs, gates, mask=mask)
296 + tl.store(GatherIndx + gates, offs, mask=mask)
297 + tl.store(GateScal + gates, gate_scal, mask=mask)
298 +
299 + @triton.jit
300 + def _combined_routing_compute_pow2(
301 + GatherIndx,
302 + ScatterIndx,
303 + GateScal,
304 + ExptScal,
305 + ExptIndx,
306 + PartialOffs,
307 + stride_pm,
308 + stride_pn,
309 + TokensStart,
310 + n_tokens,
311 + BLOCK_M: tl.constexpr,
312 + N_EXPTS_ACT: tl.constexpr,
313 + Hist,
314 + MDTileStarts,
315 + tile_starts_stridem,
316 + MDTileInfo,
317 + tile_info_stridem,
318 + first_tile_dim_log2,
319 + SIZES: tl.constexpr,
320 + BLOCK: tl.constexpr,
321 + blocks2a,
322 + BLOCK_SIZE_PADDED: tl.constexpr,
323 + ):
324 + pid = tl.program_id(0)
325 + if pid < blocks2a:
326 + _expt_data_compute(
327 + Hist,
328 + MDTileStarts,
329 + tile_starts_stridem,
330 + MDTileInfo,
331 + tile_info_stridem,
332 + first_tile_dim_log2,
333 + SIZES,
334 + BLOCK,
335 + )
336 + else:
337 + pid -= blocks2a
338 + _routing_compute_indx_pow2(
339 + pid,
340 + GatherIndx,
341 + ScatterIndx,
342 + GateScal,
343 + ExptScal,
344 + ExptIndx,
345 + PartialOffs,
346 + stride_pm,
347 + stride_pn,
348 + TokensStart,
349 + n_tokens,
350 + BLOCK_M,
351 + N_EXPTS_ACT,
352 + BLOCK_SIZE_PADDED,
353 + )
354 +
355 + def _sort_tokens_pow2(expt_scal, expt_indx, n_expts_tot, bitmatrix):
356 + import torch
357 +
358 + HIST_BLOCK_M = 32
359 + INDX_OFFS_BLOCK_M = 512
360 + MEMSET_BLOCK = 1024
361 + cdiv = triton.cdiv
362 + device = expt_scal.device
363 + dtype = expt_scal.dtype
364 + n_tokens_raw, _ = bitmatrix.shape
365 + n_tokens_pad, n_expts_act = expt_scal.shape
366 + n_gates_pad = n_tokens_pad * n_expts_act
367 + # pad per-block gate count (HIST_BLOCK_M * top_k) up to a pow2.
368 + block_size_padded = triton.next_power_of_2(HIST_BLOCK_M * n_expts_act)
369 +
370 + hist, partial_hist = bitmatrix.sum(partials_block_size=HIST_BLOCK_M)
371 + hist = hist[:n_expts_tot]
372 + expt_offs = torch.empty(n_expts_tot, dtype=torch.int32, device=device)
373 + combined_indx = torch.empty(n_gates_pad * 2, dtype=torch.int32, device=device)
374 + topk_indx = combined_indx[:n_gates_pad]
375 + gate_indx = combined_indx[n_gates_pad:]
376 + gate_scal = torch.empty(n_gates_pad, dtype=dtype, device=device)
377 +
378 + (
379 + token_offs_combined,
380 + token_offs_raw,
381 + token_offs_pad,
382 + block_pid_map,
383 + blocks1a,
384 + blocks2a,
385 + MEMSET_BLOCK_A,
386 + HIST2_BLOCK_M,
387 + block_m_log2_start,
388 + block_m_num,
389 + ) = _routing._compute_expt_data_internal(hist, n_expts_tot, n_gates_pad)
390 +
391 + blocks1b = cdiv(n_gates_pad * 2, MEMSET_BLOCK) + n_expts_tot + 1
392 + blocks2b = cdiv(n_tokens_pad, HIST_BLOCK_M)
393 +
394 + _rc._combined_routing_memset[(blocks1a + blocks1b,)](
395 + combined_indx,
396 + n_gates_pad * 2,
397 + -1,
398 + MEMSET_BLOCK,
399 + hist,
400 + expt_offs,
401 + hist.shape[0],
402 + n_expts_tot,
403 + partial_hist,
404 + partial_hist.shape[0],
405 + partial_hist.stride(0),
406 + partial_hist.stride(1),
407 + token_offs_combined,
408 + token_offs_combined.stride(0),
409 + blocks1a,
410 + block_pid_map,
411 + block_m_log2_start,
412 + SIZES=block_m_num,
413 + BLOCK_A=MEMSET_BLOCK_A,
414 + BLOCK_N=512,
415 + BLOCK_M=INDX_OFFS_BLOCK_M,
416 + )
417 +
418 + indx_offs = partial_hist
419 + _combined_routing_compute_pow2[(blocks2a + blocks2b,)](
420 + topk_indx,
421 + gate_indx,
422 + gate_scal,
423 + expt_scal,
424 + expt_indx,
425 + indx_offs,
426 + indx_offs.stride(0),
427 + indx_offs.stride(1),
428 + expt_offs,
429 + n_tokens_raw,
430 + HIST_BLOCK_M,
431 + n_expts_act,
432 + hist,
433 + token_offs_pad,
434 + token_offs_pad.stride(0),
435 + block_pid_map,
436 + block_pid_map.stride(0),
437 + block_m_log2_start,
438 + block_m_num,
439 + HIST2_BLOCK_M,
440 + blocks2a,
441 + block_size_padded,
442 + )
443 + return (
444 + hist,
445 + topk_indx,
446 + gate_indx,
447 + gate_scal,
448 + token_offs_raw,
449 + token_offs_pad,
450 + block_pid_map,
451 + )
452 +
453 + # `routing_from_bitmatrix` looks up `sort_tokens` via the routing module
454 + # global, so replacing it here redirects the legacy path to the pow2 kernel.
455 + _routing.sort_tokens = _sort_tokens_pow2
456 +
457 +
458 # Two API generations of triton_kernels are supported:
459 # - v3.5.1 (the version bundled with vLLM): exposes `routing()` and
460 # `routing_from_bitmatrix()` in triton_kernels.routing; the `Bitmatrix`
@@ -260,6 +495,9 @@ if has_triton_kernels():
495 use_legacy_triton_kernels = True
496 if not use_legacy_triton_kernels:
497 _patch_make_bitmatrix_metadata()
498 + else:
499 + # Legacy routing fails to compile for non-pow2 top_k (DeepSeek-V4).
500 + _patch_legacy_routing_for_nonpow2_topk()
501 except (AttributeError, ImportError) as e:
502 logger.error(
503 "Failed to import Triton kernels. Please make sure your triton "