From 2d06973be6b32d59de5fc14d176fb5c44f76ad93 Mon Sep 17 00:00:00 2001 From: Xiao Wang <24860335+xwang233@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:10:37 -0700 Subject: [PATCH 01/19] [None][feat] PyTorch executor: runtime hooks for model-owned decode work Add RuntimeHooks (pyexecutor/runtime_hooks.py), a per-model seam through which a modeling_v2 target can take over parts of the decode step without changing the engine's behaviour for other models: - step-input staging, so a target can write the next step's inputs from its own buffers; - the speculative sampler's store writer; - executor events and an execution-stream loop; - a decode prologue that runs before the model forward. The executor, CUDA graph runner, decoder runner, KV cache manager V2, attention backends and the speculative modules read the hooks at their call sites. Every hook is off unless a model installs it, and the default behaviour is unchanged. Signed-off-by: Xiao Wang <24860335+xwang233@users.noreply.github.com> --- .../_torch/attention/backends/fmha/manager.py | 5 +- .../attention/backends/sparse/dsa/indexer.py | 136 ++- .../attention/backends/sparse/dsa/metadata.py | 98 +- .../_torch/attention/backends/trtllm.py | 61 +- tensorrt_llm/_torch/pyexecutor/_util.py | 8 +- .../_torch/pyexecutor/cuda_graph_runner.py | 14 +- .../engine/runners/decoder/runner.py | 189 +++- .../kv_cache/kv_cache_manager_v2.py | 13 + tensorrt_llm/_torch/pyexecutor/py_executor.py | 65 +- .../_torch/pyexecutor/runtime_hooks.py | 216 +++++ tensorrt_llm/_torch/speculative/eagle3.py | 9 +- tensorrt_llm/_torch/speculative/interface.py | 10 +- .../_torch/speculative/spec_sampler_base.py | 82 +- .../attention/sparse/dsa/test_dsa_indexer.py | 100 ++ .../_torch/attention/test_fmha_manager.py | 57 ++ .../_torch/executor/test_gpu_keepalive.py | 1 + .../_torch/executor/test_py_executor.py | 3 + .../_torch/executor/test_runtime_hooks.py | 918 ++++++++++++++++++ 18 files changed, 1876 insertions(+), 109 deletions(-) create mode 100644 tensorrt_llm/_torch/pyexecutor/runtime_hooks.py create mode 100644 tests/unittest/_torch/executor/test_runtime_hooks.py diff --git a/tensorrt_llm/_torch/attention/backends/fmha/manager.py b/tensorrt_llm/_torch/attention/backends/fmha/manager.py index c9afff66e1d8..a4770ad3dbba 100644 --- a/tensorrt_llm/_torch/attention/backends/fmha/manager.py +++ b/tensorrt_llm/_torch/attention/backends/fmha/manager.py @@ -308,7 +308,10 @@ def __init__(self, attn: TrtllmAttention) -> None: # normal forwarding never pays for an environment lookup. self._cache_sanity_check_enabled = _is_fmha_cache_sanity_check_enabled() self.fmha_libs: list[Fmha] = [] - for fmha_cls in get_enabled_fmha_lib_classes(): + for fmha_cls in ( + *getattr(attn, "prepended_fmha_lib_classes", ()), + *get_enabled_fmha_lib_classes(), + ): if fmha_cls.is_available(attn): self.fmha_libs.append(fmha_cls(attn)) diff --git a/tensorrt_llm/_torch/attention/backends/sparse/dsa/indexer.py b/tensorrt_llm/_torch/attention/backends/sparse/dsa/indexer.py index abe993e31438..61884b628af4 100644 --- a/tensorrt_llm/_torch/attention/backends/sparse/dsa/indexer.py +++ b/tensorrt_llm/_torch/attention/backends/sparse/dsa/indexer.py @@ -1050,6 +1050,18 @@ def recompute_slot_mappings( metadata.host_slot_mapping_fp8[:total_new_kv_tokens] = fp8_flat_indices metadata.host_slot_mapping_scale[:total_new_kv_tokens] = scale_flat_indices + stager = getattr(metadata, "step_input_stager", None) + if stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy( + metadata.slot_mapping_fp8[:total_new_kv_tokens], + metadata.host_slot_mapping_fp8[:total_new_kv_tokens], + ) + stager.copy( + metadata.slot_mapping_scale[:total_new_kv_tokens], + metadata.host_slot_mapping_scale[:total_new_kv_tokens], + ) + return metadata.slot_mapping_fp8[:total_new_kv_tokens].copy_( metadata.host_slot_mapping_fp8[:total_new_kv_tokens], non_blocking=True ) @@ -1308,7 +1320,13 @@ def prepare(metadata: DSAtrtllmAttentionMetadata) -> None: if num_generations > 0: # Prepare schedule metadata for fp8_paged_mqa_logits # This is a preprocessing step that computes scheduling information for the kernel - Indexer.prepare_scheduler_metadata(metadata) + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager = getattr(metadata, "step_input_stager", None) + if stager is None: + Indexer.prepare_scheduler_metadata(metadata) + elif not stager.scheduler_metadata(metadata): + # Reads staged KV lengths: run right after the staged launch. + stager.defer(lambda: Indexer.prepare_scheduler_metadata(metadata)) def _update_k_cache( self, k_fp8: torch.Tensor, k_scale: torch.Tensor, metadata: DSAtrtllmAttentionMetadata @@ -1771,9 +1789,24 @@ def sparse_attn_indexer( ) if has_decode and not metadata.skip_indexer_for_gen_reqs and reuse_topk: - topk_indices_buffer[token_offset : token_offset + num_gen_tokens, :] = ( - metadata.shared_topk_indices[:num_generations, :] + self._copy_reused_topk( + metadata, + topk_indices_buffer, + token_offset, + num_gen_tokens, + num_generations, + has_prefill, + gvr_prior_indices, ) + elif ( + has_decode + and not metadata.skip_indexer_for_gen_reqs + and self._fill_decode_topk( + metadata, q_fp8, weights, topk_indices_buffer, token_offset, num_gen_tokens + ) + ): + # A subclass filled the generation rows. + pass elif has_decode and not metadata.skip_indexer_for_gen_reqs: # Get decode lengths per request (from seq_lens) for validation gen_seq_lens = metadata.seq_lens[num_contexts : num_contexts + num_generations] @@ -2034,30 +2067,83 @@ def sparse_attn_indexer( prev_topk_dst.copy_(last_mtp_topk) if self.mtp_index_share and metadata.in_mtp_draft_loop and not reuse_topk: - rows = None - if has_decode: - next_n = num_gen_tokens // num_generations - gen_topk = topk_indices_buffer[token_offset : token_offset + num_gen_tokens] - rows = self._mtp_last_accepted_rows( - gen_topk, metadata, num_contexts, num_generations, next_n - ) - if has_prefill: - ctx_last = ( - torch.cumsum(metadata.seq_lens_cuda[:num_contexts].to(torch.long), dim=0) - 1 - ) - ctx_rows = topk_indices_buffer[ctx_last] - rows = ctx_rows if rows is None else torch.cat([ctx_rows, rows]) - if rows is not None: - shared_topk_indices = metadata.shared_topk_indices - if shared_topk_indices is None: - metadata.shared_topk_indices = rows.contiguous() - else: - row_start = num_contexts if is_generation else 0 - shared_topk_indices[ - row_start : row_start + rows.shape[0], : rows.shape[1] - ].copy_(rows) + self._store_mtp_shared_rows( + topk_indices_buffer, + metadata, + num_contexts, + num_generations, + num_gen_tokens, + token_offset, + has_prefill, + has_decode, + is_generation, + ) return topk_indices_buffer + def _copy_reused_topk( + self, + metadata: DSAtrtllmAttentionMetadata, + topk_indices_buffer: torch.Tensor, + token_offset: int, + num_gen_tokens: int, + num_generations: int, + has_prefill: bool, + gvr_prior_indices: Optional[torch.Tensor], + ) -> None: + """Copy the shared TopK rows an MTP draft step reuses into its generation rows.""" + topk_indices_buffer[token_offset : token_offset + num_gen_tokens, :] = ( + metadata.shared_topk_indices[:num_generations, :] + ) + + def _fill_decode_topk( + self, + metadata: DSAtrtllmAttentionMetadata, + q_fp8: torch.Tensor, + weights: torch.Tensor, + topk_indices_buffer: torch.Tensor, + token_offset: int, + num_gen_tokens: int, + ) -> bool: + """Fill the generation TopK rows ahead of the decode branch; True if it filled them. + + This class leaves them to the decode branch of ``sparse_attn_indexer``. + """ + return False + + def _store_mtp_shared_rows( + self, + topk_indices_buffer: torch.Tensor, + metadata: DSAtrtllmAttentionMetadata, + num_contexts: int, + num_generations: int, + num_gen_tokens: int, + token_offset: int, + has_prefill: bool, + has_decode: bool, + is_generation: Optional[bool], + ) -> None: + """Store each request's last accepted TopK row for the next MTP draft step to reuse.""" + rows = None + if has_decode: + next_n = num_gen_tokens // num_generations + gen_topk = topk_indices_buffer[token_offset : token_offset + num_gen_tokens] + rows = self._mtp_last_accepted_rows( + gen_topk, metadata, num_contexts, num_generations, next_n + ) + if has_prefill: + ctx_last = torch.cumsum(metadata.seq_lens_cuda[:num_contexts].to(torch.long), dim=0) - 1 + ctx_rows = topk_indices_buffer[ctx_last] + rows = ctx_rows if rows is None else torch.cat([ctx_rows, rows]) + if rows is not None: + shared_topk_indices = metadata.shared_topk_indices + if shared_topk_indices is None: + metadata.shared_topk_indices = rows.contiguous() + else: + row_start = num_contexts if is_generation else 0 + shared_topk_indices[row_start : row_start + rows.shape[0], : rows.shape[1]].copy_( + rows + ) + def _mtp_last_accepted_rows( self, gen_topk: torch.Tensor, diff --git a/tensorrt_llm/_torch/attention/backends/sparse/dsa/metadata.py b/tensorrt_llm/_torch/attention/backends/sparse/dsa/metadata.py index c6af4efd7dba..610a996f7e5c 100644 --- a/tensorrt_llm/_torch/attention/backends/sparse/dsa/metadata.py +++ b/tensorrt_llm/_torch/attention/backends/sparse/dsa/metadata.py @@ -1454,6 +1454,15 @@ def _update_indexer_k_cache_block_offsets(self) -> torch.Tensor: page_indices = pool_indices * cache_manager.indexer_k_cache_page_scale num_blocks = page_indices.shape[1] self.host_indexer_k_cache_block_offsets[: self.num_seqs, :num_blocks].copy_(page_indices) + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy( + self.indexer_k_cache_block_offsets[: self.num_seqs], + self.host_indexer_k_cache_block_offsets[: self.num_seqs], + clamp_min=0, + ) + self.step_input_stager.clamp_min(self.indexer_k_cache_block_offsets[self.num_seqs :], 0) + return pool_indices self.indexer_k_cache_block_offsets[: self.num_seqs].copy_( self.host_indexer_k_cache_block_offsets[: self.num_seqs], non_blocking=True ) @@ -1670,9 +1679,16 @@ def prepare_dense_topk_indices(self, kv_lens, device=False): # device=False mea kv_lens[self.num_contexts : self.num_seqs], self.num_tokens - self.num_ctx_tokens, ) - self.topk_indices_buffer[gen_range, :].copy_( - self.host_topk_indices_buffer[gen_range, :], non_blocking=True - ) + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy( + self.topk_indices_buffer[gen_range, :], + self.host_topk_indices_buffer[gen_range, :], + ) + else: + self.topk_indices_buffer[gen_range, :].copy_( + self.host_topk_indices_buffer[gen_range, :], non_blocking=True + ) def prepare_for_spec_decode(self, kv_lens: torch.Tensor): # The DeepGEMM paged-MQA kernel (fp8_paged_mqa_logits) runs a native @@ -1753,9 +1769,16 @@ def prepare_for_spec_decode(self, kv_lens: torch.Tensor): num_tokens = self.num_generations * expand_factor gen_kv_lens_expanded = gen_kv_lens.repeat_interleave(expand_factor) self.kv_lens_expanded_host[:num_tokens].copy_(gen_kv_lens_expanded) - self.kv_lens_expanded_cuda[:num_tokens].copy_( - self.kv_lens_expanded_host[:num_tokens], non_blocking=True - ) + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy( + self.kv_lens_expanded_cuda[:num_tokens], + self.kv_lens_expanded_host[:num_tokens], + ) + else: + self.kv_lens_expanded_cuda[:num_tokens].copy_( + self.kv_lens_expanded_host[:num_tokens], non_blocking=True + ) self._refresh_expanded_block_table(expand_factor) else: self.dsl_expand_factor = 1 @@ -1984,6 +2007,15 @@ def _refresh_expanded_block_table(self, repeat_factor: Optional[int] = None): self.host_block_table_expanded[:num_tokens, :max_len].copy_( expanded_blocks, non_blocking=True ) + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy( + self.block_table_expanded[:num_tokens], + self.host_block_table_expanded[:num_tokens], + clamp_min=0, + ) + self.step_input_stager.clamp_min(self.block_table_expanded[num_tokens:], 0) + return self.block_table_expanded[:num_tokens].copy_( self.host_block_table_expanded[:num_tokens], non_blocking=True ) @@ -2024,9 +2056,15 @@ def prepare_for_indexer_k_cache(self): pad_cols.unsqueeze(0) >= num_blocks_per_seq[: self.num_seqs].unsqueeze(1), -1 ) # Copy to GPU - self.block_table[: self.num_seqs, :max_blocks_used].copy_( - host_block_table, non_blocking=True - ) + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy( + self.block_table[: self.num_seqs, :max_blocks_used], host_block_table + ) + else: + self.block_table[: self.num_seqs, :max_blocks_used].copy_( + host_block_table, non_blocking=True + ) def prepare_for_skip_indexer(self, kv_lens: torch.Tensor): num_extra_kv_tokens = self.kv_cache_params.num_extra_kv_tokens @@ -2092,9 +2130,17 @@ def prepare_for_mla_rope_append(self, cached_token_lens: torch.Tensor, kv_lens: dtype=torch.int64, out=self.host_gen_cached_token_indptr[1 : self.num_generations + 1], ) - self.gen_cached_token_indptr[: self.num_generations + 1].copy_( - self.host_gen_cached_token_indptr[: self.num_generations + 1], non_blocking=True - ) + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy( + self.gen_cached_token_indptr[: self.num_generations + 1], + self.host_gen_cached_token_indptr[: self.num_generations + 1], + ) + else: + self.gen_cached_token_indptr[: self.num_generations + 1].copy_( + self.host_gen_cached_token_indptr[: self.num_generations + 1], + non_blocking=True, + ) # generation kv indptr torch.cumsum( kv_lens[self.num_contexts : self.num_seqs], @@ -2102,9 +2148,16 @@ def prepare_for_mla_rope_append(self, cached_token_lens: torch.Tensor, kv_lens: dtype=torch.int64, out=self.host_gen_kv_indptr[1 : self.num_generations + 1], ) - self.gen_kv_indptr[: self.num_generations + 1].copy_( - self.host_gen_kv_indptr[: self.num_generations + 1], non_blocking=True - ) + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy( + self.gen_kv_indptr[: self.num_generations + 1], + self.host_gen_kv_indptr[: self.num_generations + 1], + ) + else: + self.gen_kv_indptr[: self.num_generations + 1].copy_( + self.host_gen_kv_indptr[: self.num_generations + 1], non_blocking=True + ) else: self.max_gen_seq_len = 0 @@ -2116,7 +2169,14 @@ def prepare_for_indices_conversion(self): self.seq_lens, dim=0, ) - self.req_idx_per_token[: self.num_tokens].copy_( - self.host_req_idx_per_token[: self.num_tokens], - non_blocking=True, - ) + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy( + self.req_idx_per_token[: self.num_tokens], + self.host_req_idx_per_token[: self.num_tokens], + ) + else: + self.req_idx_per_token[: self.num_tokens].copy_( + self.host_req_idx_per_token[: self.num_tokens], + non_blocking=True, + ) diff --git a/tensorrt_llm/_torch/attention/backends/trtllm.py b/tensorrt_llm/_torch/attention/backends/trtllm.py index 048138a48582..9a6634434d90 100644 --- a/tensorrt_llm/_torch/attention/backends/trtllm.py +++ b/tensorrt_llm/_torch/attention/backends/trtllm.py @@ -28,6 +28,7 @@ from tensorrt_llm.mapping import Mapping from ...model_config import ModelConfig + from ...pyexecutor.runtime_hooks import StepInputStager from ...speculative.interface import SpecMetadata from ...speculative.spec_tree_manager import SpecTreeManager from .sparse.dsa.metadata import TokenMajorGenView @@ -123,6 +124,8 @@ def generate_spec_decoding_packed_mask(max_num_requests: int, @dataclass(kw_only=True) class TrtllmAttentionMetadata(AttentionMetadata): + # A step-input stager while it records this metadata's writes, else None. + step_input_stager: ClassVar[Optional["StepInputStager"]] = None workspace: Optional[torch.Tensor] = None cuda_graph_workspace: Optional[torch.Tensor] = None workspace_reclaimable: bool = field(default=True, init=False) @@ -997,8 +1000,14 @@ def prepare(self) -> None: device='cpu', ) self.prompt_lens_cpu[:self.num_seqs].copy_(prompt_lens) - self.prompt_lens_cuda[:self.num_seqs].copy_( - self.prompt_lens_cpu[:self.num_seqs], non_blocking=True) + stager = self.step_input_stager + if stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy(self.prompt_lens_cuda[:self.num_seqs], + self.prompt_lens_cpu[:self.num_seqs]) + else: + self.prompt_lens_cuda[:self.num_seqs].copy_( + self.prompt_lens_cpu[:self.num_seqs], non_blocking=True) # number of tokens in the kv cache for each sequence in the batch cached_token_lens = torch.tensor( @@ -1051,9 +1060,14 @@ def prepare(self) -> None: # the sequence length including the cached tokens and the input tokens. self.kv_lens[:self.num_seqs].copy_( kv_lens + self.kv_cache_params.num_extra_kv_tokens) - self.kv_lens_cuda[:self.num_seqs].copy_(maybe_pin_memory( - kv_lens[:self.num_seqs]), - non_blocking=True) + if stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy(self.kv_lens_cuda[:self.num_seqs], + kv_lens[:self.num_seqs]) + else: + self.kv_lens_cuda[:self.num_seqs].copy_(maybe_pin_memory( + kv_lens[:self.num_seqs]), + non_blocking=True) # total kv lens for context requests and generation requests, without extra tokens self.host_total_kv_lens[0] = kv_lens[:self.num_contexts].sum().item() self.host_total_kv_lens[1] = kv_lens[self.num_contexts:self. @@ -1420,23 +1434,42 @@ def prepare_context_mla_with_cached_kv(self, dtype=torch.int64, out=self.host_ctx_cached_token_indptr[1:self.num_contexts + 1]) - self.ctx_cached_token_indptr[:self.num_contexts + 1].copy_( - self.host_ctx_cached_token_indptr[:self.num_contexts + 1], - non_blocking=True) + stager = self.step_input_stager + if stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy( + self.ctx_cached_token_indptr[:self.num_contexts + 1], + self.host_ctx_cached_token_indptr[:self.num_contexts + 1]) + else: + self.ctx_cached_token_indptr[:self.num_contexts + 1].copy_( + self.host_ctx_cached_token_indptr[:self.num_contexts + 1], + non_blocking=True) torch.cumsum( self.seq_lens[:self.num_contexts], dim=0, dtype=torch.int64, out=self.host_ctx_uncached_token_indptr[1:self.num_contexts + 1]) - self.ctx_uncached_token_indptr[:self.num_contexts + 1].copy_( - self.host_ctx_uncached_token_indptr[:self.num_contexts + 1], - non_blocking=True) + if stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy( + self.ctx_uncached_token_indptr[:self.num_contexts + 1], + self.host_ctx_uncached_token_indptr[:self.num_contexts + 1]) + else: + self.ctx_uncached_token_indptr[:self.num_contexts + 1].copy_( + self.host_ctx_uncached_token_indptr[:self.num_contexts + 1], + non_blocking=True) torch.cumsum(kv_lens[:self.num_contexts], dim=0, dtype=torch.int64, out=self.host_ctx_kv_indptr[1:self.num_contexts + 1]) - self.ctx_kv_indptr[:self.num_contexts + 1].copy_( - self.host_ctx_kv_indptr[:self.num_contexts + 1], non_blocking=True) + if stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy(self.ctx_kv_indptr[:self.num_contexts + 1], + self.host_ctx_kv_indptr[:self.num_contexts + 1]) + else: + self.ctx_kv_indptr[:self.num_contexts + 1].copy_( + self.host_ctx_kv_indptr[:self.num_contexts + 1], + non_blocking=True) def compute_max_num_custom_mask_tiles_kv_upper_bound( self, max_seq_len_kv, min_first_sparse_mask_offset_kv, @@ -1678,6 +1711,8 @@ def is_sm_version_trtllm_gen_kernel(self, sm): class TrtllmAttention(AttentionBackend[TrtllmAttentionMetadata]): Metadata = TrtllmAttentionMetadata + # FMHA library classes this instance tries before the registry's libraries. + prepended_fmha_lib_classes: tuple = () @staticmethod def is_sm_version_trtllm_gen_kernel(sm): diff --git a/tensorrt_llm/_torch/pyexecutor/_util.py b/tensorrt_llm/_torch/pyexecutor/_util.py index bcfb7529311b..26940ca8bb50 100644 --- a/tensorrt_llm/_torch/pyexecutor/_util.py +++ b/tensorrt_llm/_torch/pyexecutor/_util.py @@ -53,6 +53,7 @@ from ..speculative import (draft_prompt_lookahead, get_num_extra_kv_tokens, get_num_spec_layers, get_spec_decoder, should_use_separate_draft_kv_cache) +from ..speculative.spec_sampler_base import SpecSampler from ..utils import is_gdn_replay_enabled from . import config_utils from .config_utils import (MambaKVCacheParams, _is_sliding_attention_layer, @@ -77,6 +78,7 @@ from .resource_manager import (KVCacheCompressionManager, KVCacheManager, PeftCacheManager, ResourceManager, ResourceManagerType) +from .runtime_hooks import runtime_hooks_of from .sampler import EarlyStopSampler, EarlyStopWithMMResult, TorchSampler from .scheduler import (BindCapacityScheduler, BindMicroBatchScheduler, KVCacheV2Scheduler, MultimodalEagerEncoderScheduler, @@ -4451,7 +4453,11 @@ def instantiate_sampler( ) if engine.spec_config is not None and engine.spec_config.spec_dec_mode.has_spec_decoder( ): - return get_spec_decoder(sampler_args, engine.spec_config) + sampler = get_spec_decoder(sampler_args, engine.spec_config) + store_writer = runtime_hooks_of(engine.model).sampler_store_writer + if store_writer is not None and isinstance(sampler, SpecSampler): + sampler.store_writer = store_writer + return sampler if mm_encoder_only: # NOTE: handle model outputs specially for mm encoder executor/engine. diff --git a/tensorrt_llm/_torch/pyexecutor/cuda_graph_runner.py b/tensorrt_llm/_torch/pyexecutor/cuda_graph_runner.py index 19ed990f33a1..73aa638f740d 100644 --- a/tensorrt_llm/_torch/pyexecutor/cuda_graph_runner.py +++ b/tensorrt_llm/_torch/pyexecutor/cuda_graph_runner.py @@ -162,6 +162,10 @@ class CUDAGraphRunner: multiple graphs, keyed by batch shape and execution-path specializations. """ WARMUP_STEPS = 1 + # Tokens of input_ids / position_ids that a step-input stager already + # wrote into the static inputs for the next replay (0: the replay copies + # them). + staged_static_input_tokens = 0 def __init__(self, config: CUDAGraphRunnerConfig): self.config = config @@ -816,7 +820,12 @@ def replay( f"replay() got {seqlen} tokens for key {key}, but the graph " f"was captured for {expected_num_tokens} tokens. A shorter " "input_ids leaves the tail of the static input buffer stale.") - static_tensors["input_ids"][:seqlen].copy_(input_ids) + # A step-input stager already wrote input_ids / position_ids[:seqlen] + # into the static buffers. + staged = self.staged_static_input_tokens == seqlen + self.staged_static_input_tokens = 0 + if not staged: + static_tensors["input_ids"][:seqlen].copy_(input_ids) position_ids = current_inputs["position_ids"] if self.config.use_mrope: @@ -863,7 +872,8 @@ def replay( f"for key {key}, but expected {expected_position_ids_shape}. " "torch.Tensor.copy_() silently broadcasts mismatched shapes, " "which would corrupt the static input buffer.") - static_tensors["position_ids"][:, :seqlen].copy_(position_ids) + if not staged: + static_tensors["position_ids"][:, :seqlen].copy_(position_ids) num_encoder_tokens = key.num_encoder_tokens if num_encoder_tokens: diff --git a/tensorrt_llm/_torch/pyexecutor/engine/runners/decoder/runner.py b/tensorrt_llm/_torch/pyexecutor/engine/runners/decoder/runner.py index c74a0a8d9e7e..986d05081590 100644 --- a/tensorrt_llm/_torch/pyexecutor/engine/runners/decoder/runner.py +++ b/tensorrt_llm/_torch/pyexecutor/engine/runners/decoder/runner.py @@ -54,6 +54,12 @@ ResourceManager, ResourceManagerType, ) +from tensorrt_llm._torch.pyexecutor.runtime_hooks import ( + DEFAULT_RUNTIME_HOOKS, + RuntimeHooks, + StepInputStager, + runtime_hooks_of, +) from tensorrt_llm._torch.pyexecutor.sampler import SampleStateTensors from tensorrt_llm._torch.pyexecutor.sampler.ops.flashinfer import warmup_sampling_module from tensorrt_llm._torch.pyexecutor.sampler.sampler_common import SampleType @@ -315,6 +321,11 @@ def __getattr__(self, name: str) -> Any: class DecoderRunner(ScheduledModelRunner): """Run scheduled decoder generation with its own buffers, graphs and metadata.""" + # Step-loop behaviour the model opted into (RuntimeHooks); all off unless it declares otherwise. + runtime_hooks: RuntimeHooks = DEFAULT_RUNTIME_HOOKS + # The model's step-input stager while it records this runner's writes, else None. + step_input_stager: StepInputStager | None = None + # Optional execution paths that a subclass can turn off. _context_warmups_supported = True _context_graph_promotion_supported = True @@ -535,6 +546,7 @@ def _init_decoder_state(self) -> None: cuda_graph_manager=None, ) self._prepare_inputs_event: torch.cuda.Event | None = None + self.runtime_hooks = runtime_hooks_of(self.model) # Let the first CUDA graph capture create its private pool. Piecewise # CUDA graphs use a separate pool owned by their runners, so sharing a # pre-created pool handle with the outer graph runner is unnecessary. @@ -2856,6 +2868,16 @@ def _preprocess_inputs( Make some changes to the device inputs and avoid blocking the async data transfer """ attn_meta = inputs.get("attn_metadata") + # A model's decode prologue may replace the device work below with one + # launch; it decides on the host before any device work and returns + # False to keep the code below. + prologue = self.runtime_hooks.decode_prologue + if prologue is not None and prologue( + self, inputs, enable_spec_decode=enable_spec_decode, runtime_draft_len=runtime_draft_len + ): + if self.guided_decoder is not None: + self.guided_decoder.token_event.record() + return inputs # Invalidate per-forward-pass caches so they are recomputed (and captured) on every _forward_step. if attn_meta is not None: attn_meta.on_update_kv_lens() @@ -4076,21 +4098,51 @@ def previous_seq_slots_device(): f"total_num_tokens ({total_num_tokens}) should be less than or equal to max_num_tokens ({self._config.max_num_tokens})" # noqa: E501 ) # if exist requests that do not have previous batch, copy input_ids and draft_tokens + stager = self.step_input_stager if num_tokens > 0: input_ids = torch.tensor(input_ids, dtype=torch.int, pin_memory=prefer_pinned()) - self.input_ids_cuda[:num_tokens].copy_(input_ids, non_blocking=True) + if stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy(self.input_ids_cuda[:num_tokens], input_ids) + else: + self.input_ids_cuda[:num_tokens].copy_(input_ids, non_blocking=True) if num_draft_tokens > 0: draft_tokens = torch.tensor(draft_tokens, dtype=torch.int, pin_memory=prefer_pinned()) - self.draft_tokens_cuda[: len(draft_tokens)].copy_(draft_tokens, non_blocking=True) + if stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy(self.draft_tokens_cuda[: len(draft_tokens)], draft_tokens) + else: + self.draft_tokens_cuda[: len(draft_tokens)].copy_(draft_tokens, non_blocking=True) if self._config.is_spec_decode and len(num_accepted_draft_tokens) > 0: num_accepted_draft_tokens = torch.tensor( num_accepted_draft_tokens, dtype=torch.int, pin_memory=prefer_pinned() ) - self.num_accepted_draft_tokens_cuda[: len(num_accepted_draft_tokens)].copy_( - num_accepted_draft_tokens, non_blocking=True + if stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy( + self.num_accepted_draft_tokens_cuda[: len(num_accepted_draft_tokens)], + num_accepted_draft_tokens, + ) + else: + self.num_accepted_draft_tokens_cuda[: len(num_accepted_draft_tokens)].copy_( + num_accepted_draft_tokens, non_blocking=True + ) + if next_draft_tokens_device is not None and stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.overlap_inputs( + self, + previous_batch_indices, + previous_pos_indices, + num_tokens, + num_draft_tokens, + self.get_runtime_tokens_per_gen_step(runtime_draft_len), + len(extend_requests) - len(extend_dummy_requests), + new_tokens_device, + next_draft_tokens_device, + new_tokens_lens_device, ) - if next_draft_tokens_device is not None: + elif next_draft_tokens_device is not None: # Initialize these two values to zeros self.previous_pos_id_offsets_cuda *= 0 self.previous_kv_lens_offsets_cuda *= 0 @@ -4188,6 +4240,10 @@ def previous_seq_slots_device(): # cover the previous-batch rows; padding-request rows are covered by # neither, so without this the embedding would gather stale contents # of the uninitialized buffer, which may exceed the vocabulary size. + if stager is not None and unwritten_input_id_rows: + # A native write to input_ids_cuda inside the window needs a flush + # first; see the window banner in DecoderRunner._forward_decoder. + stager.flush() _zero_fill_row_spans(self.input_ids_cuda, unwritten_input_id_rows) position_ids = apply_position_id_offset(position_ids, model=self.model) @@ -4229,11 +4285,21 @@ def previous_seq_slots_device(): segment[:, :, : end_idx - start_idx], non_blocking=True ) final_position_ids = self.mrope_position_ids_cuda[:, :, :total_num_tokens] + elif stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.position_ids(self, self.position_ids_cuda[:total_num_tokens], host_position_ids) + final_position_ids = self.position_ids_cuda[:total_num_tokens].unsqueeze(0) else: self.position_ids_cuda[:total_num_tokens].copy_(host_position_ids, non_blocking=True) final_position_ids = self.position_ids_cuda[:total_num_tokens].unsqueeze(0) - if enable_spec_decode: + if enable_spec_decode and stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.copy( + self.gather_ids_cuda[: len(gather_ids)], + torch.tensor(gather_ids, dtype=torch.int, pin_memory=prefer_pinned()), + ) + elif enable_spec_decode: self.gather_ids_cuda[: len(gather_ids)].copy_( torch.tensor(gather_ids, dtype=torch.int, pin_memory=prefer_pinned()), non_blocking=True, @@ -4423,7 +4489,13 @@ def previous_seq_slots_device(): ) virtual_num_tokens = total_num_tokens - if attn_metadata.padded_num_tokens is not None: + if attn_metadata.padded_num_tokens is not None and stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + stager.zero(self.input_ids_cuda[total_num_tokens:padded_num_tokens]) + stager.zero(self.position_ids_cuda[total_num_tokens:padded_num_tokens]) + virtual_num_tokens = padded_num_tokens + final_position_ids = self.position_ids_cuda[:virtual_num_tokens].unsqueeze(0) + elif attn_metadata.padded_num_tokens is not None: self.input_ids_cuda[total_num_tokens:padded_num_tokens].fill_(0) virtual_num_tokens = padded_num_tokens # Match the rank of the unpadded branch: MRoPE models always use @@ -4856,29 +4928,69 @@ def _forward_decoder( execution_requests.generation_requests, ) - with self._cuda_graph_metadata_prepare_scope( - attn_metadata, - key, - execution_requests, - execution_promoted_context_ids, - enable_spec_decode=enable_spec_decode, - ): - inputs, gather_ids, runtime_draft_len = self._prepare_inputs( + # ==== Step-input window opens here ==== + # When the model declares a step-input stager and begin_if_eligible + # returns it, every device write that _prepare_inputs (and the + # metadata and KV-cache-manager code it calls) routes through + # ``step_input_stager`` (call sites marked "Staged write") is only + # recorded; stager.end() below, right before the graph replay, + # issues them all. Until then, do not read or write those buffers + # with any other device op: a read sees stale bytes and a write is + # overwritten. Route new writes through the stager or call + # stager.flush() first. Exception: input_ids_cuda and + # position_ids_cuda are also staged into the graph's static + # inputs, which the replay then does not refresh from them; to + # change them here, call stager.flush() and then write them + # natively. Nothing checks this contract in a production run. + # Contract: StepInputStager in pyexecutor/runtime_hooks.py. + stager = None + stager_source = self.runtime_hooks.step_input_stager + if stager_source is not None: + stager = stager_source.begin_if_eligible( + self, execution_requests, - kv_cache_manager, attn_metadata, spec_metadata, new_tensors_device, - cache_indirection_buffer, - resource_manager, - can_run_graph, - execution_promoted_context_ids, - use_lora_graph=use_lora_graph, - enable_spec_decode=enable_spec_decode, - runtime_draft_len=runtime_draft_len, + graph_replay=( + can_run_graph + and not self.cuda_graph_runner.needs_capture(key) + and not self.cuda_graph_runner.is_warmup_only + ), is_dummy=is_dummy, - gather_context_logits=gather_context_logits, + enable_spec_decode=enable_spec_decode, + promoted=bool(execution_promoted_context_ids), ) + try: + # The window spans the scope: the scope is a no-op whenever + # speculative decoding is on, which every stager requires. + with self._cuda_graph_metadata_prepare_scope( + attn_metadata, + key, + execution_requests, + execution_promoted_context_ids, + enable_spec_decode=enable_spec_decode, + ): + inputs, gather_ids, runtime_draft_len = self._prepare_inputs( + execution_requests, + kv_cache_manager, + attn_metadata, + spec_metadata, + new_tensors_device, + cache_indirection_buffer, + resource_manager, + can_run_graph, + execution_promoted_context_ids, + use_lora_graph=use_lora_graph, + enable_spec_decode=enable_spec_decode, + runtime_draft_len=runtime_draft_len, + is_dummy=is_dummy, + gather_context_logits=gather_context_logits, + ) + except BaseException: + if stager is not None: + stager.discard() + raise if execution_promoted_context_ids: self.iter_states["num_ctx_requests"] = scheduled_requests.num_context_requests self.iter_states["num_ctx_tokens"] = sum( @@ -4888,7 +5000,8 @@ def _forward_decoder( scheduled_requests.num_generation_requests ) self._prepare_inputs_event = torch.cuda.Event() - self._prepare_inputs_event.record() + if stager is None: + self._prepare_inputs_event.record() breakable_runner = self.breakable_cuda_graph_runner @@ -4976,10 +5089,30 @@ def capture_postprocess_fn(inputs: dict[str, Any]): finally: restore_attn_metadata_after_draft_replay(attn_metadata, saved_draft) else: - saved_draft = prepare_attn_metadata_for_draft_replay( - attn_metadata, draft_kv_cache_manager - ) try: + saved_draft = prepare_attn_metadata_for_draft_replay( + attn_metadata, draft_kv_cache_manager + ) + except BaseException: + if stager is not None: + stager.discard() + raise + try: + if stager is not None: + # ==== Step-input window closes here ==== + # end() issues every staged write; nothing between + # begin_if_eligible above and this call may have + # touched the staged buffers with other device ops. + # The input-copy event must follow the launch (host + # page-table reads happen at execution time). A + # nonzero static_inputs_tokens() makes the replay + # skip its input_ids / position_ids copy. + stager.end(record_event=False) + self._prepare_inputs_event.record() + stager.set_completion_event(self._prepare_inputs_event) + self.cuda_graph_runner.staged_static_input_tokens = ( + stager.static_inputs_tokens() + ) with MoeLoadBalancerIterContext(moe_load_balancer): outputs = self.cuda_graph_runner.replay( key, inputs, position_id_offsets=position_id_offsets diff --git a/tensorrt_llm/_torch/pyexecutor/kv_cache/kv_cache_manager_v2.py b/tensorrt_llm/_torch/pyexecutor/kv_cache/kv_cache_manager_v2.py index 5ac8934c6d85..f1f60ee84448 100644 --- a/tensorrt_llm/_torch/pyexecutor/kv_cache/kv_cache_manager_v2.py +++ b/tensorrt_llm/_torch/pyexecutor/kv_cache/kv_cache_manager_v2.py @@ -135,6 +135,7 @@ if TYPE_CHECKING: from tensorrt_llm._torch.attention.backends.interface import AttentionMetadata + from tensorrt_llm._torch.pyexecutor.runtime_hooks import StepInputStager from tensorrt_llm.llmapi.llm_args import DecodingBaseConfig KV_CACHE_ITERATION_STATS_DELTA_FIELDS = _KV_CACHE_ITERATION_STATS_DELTA_FIELDS @@ -1231,6 +1232,8 @@ def _spec_recompute_claim_limit( class KVCacheManagerV2(BaseResourceManager): # Sparse managers attach a metadata batch during initialization. sparse_metadata_batch: Batch | None = None + # A step-input stager while it records this manager's writes, else None. + step_input_stager: Optional["StepInputStager"] = None # Filled lazily by _cold_pool_group_membership(); the grouping is fixed after construction. # Declared on the class so it is present even when an instance is built without running __init__. _cold_pool_group_membership_cache: Optional[tuple[tuple[int, frozenset[int]], ...]] = None @@ -6565,6 +6568,16 @@ def copy_batch_block_offsets( ) return + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.block_offsets( + self.host_kv_cache_block_offsets, + dst_tensor, + copy_idx.tolist(), + self._index_scale_ints, + self.kv_offset.tolist(), + ) + return copy_batch_block_offsets_to_device( self.host_kv_cache_block_offsets, dst_tensor, diff --git a/tensorrt_llm/_torch/pyexecutor/py_executor.py b/tensorrt_llm/_torch/pyexecutor/py_executor.py index c4b14391194b..2bb83d55b49e 100644 --- a/tensorrt_llm/_torch/pyexecutor/py_executor.py +++ b/tensorrt_llm/_torch/pyexecutor/py_executor.py @@ -114,6 +114,7 @@ get_from_waiting_queue, merge_requests) from .resource_manager import (NoFreeSlotsError, ResourceManager, ResourceManagerType, request_context) +from .runtime_hooks import ExecutorEvents, runtime_hooks_of from .sampler import (AsyncWorkerMixin, Sampler, SamplerEvent, SampleState, SampleStateTensors) from .scheduler import (RequestScheduler, ScheduledRequests, @@ -405,6 +406,16 @@ class PendingEncoderStep: class PyExecutor: + # True while the overlap loop runs with the execution stream current + # (_overlap_loop_stream_scope). + _loop_on_execution_stream = False + # Whether the model opted into running the overlap loop on the execution + # stream (RuntimeHooks.loop_on_execution_stream); read once at construction. + _execution_stream_loop_armed = False + # The model's executor events (RuntimeHooks.executor_events); read once at + # construction. + _executor_events: Optional[ExecutorEvents] = None + # Minimum number of async micro batches for async PP execution. # This is a trade-off between memory usage and performance. # If the number of micro batches is too small, the executor will spend too much time in synchronization. @@ -522,6 +533,9 @@ def __init__( # related modules self.resource_manager = resource_manager self.model_engine = model_engine + hooks = runtime_hooks_of(model_engine.model) + self._execution_stream_loop_armed = hooks.loop_on_execution_stream + self._executor_events = hooks.executor_events self._enable_adp_dummy_fixes = getattr(model_engine, "_enable_adp_dummy_fixes", False) self._enable_scheduler_aware_adp_dummy = getattr( @@ -916,6 +930,10 @@ def __init__( # Ensure the default stream waits for execution_stream to complete # before subsequent operations. torch.cuda.current_stream().wait_stream(self.execution_stream) + if self._executor_events is not None: + # The model's actions after warmup and graph capture, on every rank + # (some are collective, so every rank reaches this point). + self._executor_events.after_warmup() self.encoder_launch_executor = (ThreadPoolExecutor( max_workers=1, thread_name_prefix="encoder-launch") if self.is_encoder_decoder else None) @@ -3021,6 +3039,9 @@ def _executor_loop_cleanup(self): f"Error waiting on PP send handles during cleanup: " f"{traceback.format_exc()}") + if self._executor_events is not None and self._event_loop_completed: + self._executor_events.at_shutdown() + def _pp_schedule_and_propagate(self, microbatch_id: int): """The first PP rank schedules the requests and propagates the result to all other PP ranks.""" @@ -5714,11 +5735,44 @@ def _wait_for_model_engine_input_copy(self): if wait_for_input_copy is not None: wait_for_input_copy() + @contextmanager + def _overlap_loop_stream_scope(self): + """Run the overlap loop with the execution stream current, when the model opted in. + + _forward_step makes the execution stream wait for the current stream + before every forward and the current stream wait for the execution + stream after it. On the thread's default stream, which in steady decode + carries only these waits and timing events, step N+1's inputs wait for + decode graph N through two cross-stream event hops. With the execution + stream current the loop's stream work is queued in the same order on one + stream and _forward_step skips both waits. + + Not taken with a two-model drafter, a guided decoder, a KV connector, + an encoder-decoder model or multimodal encoder item scheduling, whose + side streams synchronize with the current stream. + """ + if not (self._execution_stream_loop_armed and self.drafter is None and + self.draft_model_engine is None and self.guided_decoder is None + and self.kv_connector_manager is None + and not self.is_encoder_decoder + and not self._mm_encoder_item_scheduling_enabled): + yield + return + with torch.cuda.stream(self.execution_stream): + self._loop_on_execution_stream = True + try: + yield + finally: + self._loop_on_execution_stream = False + def _executor_loop_overlap(self): torch.cuda.set_device(self.device_id) # ensure the context is created, otherwise, some MPI calls will fail. CUASSERT(cudart.cudaSetDevice(self.device_id)) - with self._profiler() as profile_step, self.hang_detector: + if self._executor_events is not None: + self._executor_events.before_overlap_loop() + with self._overlap_loop_stream_scope(), self._profiler() as profile_step, \ + self.hang_detector: iter_start_time = time.time() iter_stats = None target_inputs = None @@ -8423,8 +8477,10 @@ def forward(scheduled_requests, resource_manager, new_tensors_device, cache_indirection_buffer = self.sampler.get_cache_indirection() # Run model forward on the execution stream for proper synchronization - # with KVCacheTransferManager's onboard/offload operations. - self.execution_stream.wait_stream(torch.cuda.current_stream()) + # with KVCacheTransferManager's onboard/offload operations. With the + # loop on the execution stream both waits are program order. + if not self._loop_on_execution_stream: + self.execution_stream.wait_stream(torch.cuda.current_stream()) self._attach_encoder_output_to_execution_stream(scheduled_requests) with torch.cuda.stream(self.execution_stream): outputs = forward(scheduled_requests, self.resource_manager, @@ -8438,7 +8494,8 @@ def forward(scheduled_requests, resource_manager, new_tensors_device, # Ensure the default stream waits for execution_stream to complete # before downstream operations use the outputs. - torch.cuda.current_stream().wait_stream(self.execution_stream) + if not self._loop_on_execution_stream: + torch.cuda.current_stream().wait_stream(self.execution_stream) self._kv_connector_wait_for_save() if route_capture is not None: diff --git a/tensorrt_llm/_torch/pyexecutor/runtime_hooks.py b/tensorrt_llm/_torch/pyexecutor/runtime_hooks.py new file mode 100644 index 000000000000..f8c6717b3909 --- /dev/null +++ b/tensorrt_llm/_torch/pyexecutor/runtime_hooks.py @@ -0,0 +1,216 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Step-loop behaviour a model may opt into, read through ``runtime_hooks_of``. + +A model declares its hooks as ``runtime_hooks`` (an instance attribute, a class +attribute or a property). Every field defaults to off, and a model that declares +nothing gets ``RuntimeHooks()``: the step loop, the samplers and the attention +metadata then run exactly as they do without this module. Every engine call site +of a hook runs its original statements when the hook is off. + +The hooks change *where* and *how* the step loop moves data, never what a +forward computes. The protocols below state only the methods an engine call +site needs; the objects that implement them are owned by the model. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Callable, Optional, Protocol, Sequence + +if TYPE_CHECKING: + import torch + + +class StepInputStager(Protocol): + """Records a decode step's input preparation and issues it as one launch. + + ``begin_if_eligible`` opens a deferred-write window on a step it supports + and returns the stager, or returns None and the engine prepares the step + natively. While the window is open, the engine's call sites route their + device writes to the staging methods instead of issuing them. ``end`` + issues every staged write in staging order, then the deferred native + operations; ``discard`` drops them after a failed preparation. + + While its window is open the stager sets itself as ``step_input_stager`` on + the runner, the attention metadata, the speculative metadata and the KV + cache managers, and resets that attribute to None at ``end`` and + ``discard``. Every call site reads the attribute and runs its original + statement when it is None. + """ + + def begin_if_eligible( + self, + runner: Any, + requests: Any, + attn_metadata: Any, + spec_metadata: Any, + new_tensors_device: Any, + *, + graph_replay: bool, + is_dummy: bool, + enable_spec_decode: bool, + promoted: bool, + ) -> Optional["StepInputStager"]: + """Open the window for this step, or return None to keep the native preparation.""" + + def copy( + self, dst: "torch.Tensor", src: "torch.Tensor", clamp_min: Optional[int] = None + ) -> None: + """Stage ``dst.copy_(src, non_blocking=True)`` for a host ``src``.""" + + def zero(self, dst: "torch.Tensor") -> None: + """Stage ``dst.zero_()``.""" + + def clamp_min(self, dst: "torch.Tensor", lo: int) -> None: + """Stage ``dst.clamp_(min=lo)``.""" + + def block_offsets( + self, + host_table: "torch.Tensor", + dst: "torch.Tensor", + copy_index: Sequence[int], + index_scales: Sequence[int], + kv_offsets: Sequence[int], + ) -> None: + """Stage the KV cache manager's batch copy of block offsets to the device.""" + + def position_ids( + self, runner: Any, dst: "torch.Tensor", host_position_ids: "torch.Tensor" + ) -> None: + """Stage the position-id upload and the CUDA graph's static copy of it.""" + + def overlap_inputs( + self, + runner: Any, + previous_batch_indices: Sequence[int], + previous_pos_indices: Sequence[int], + num_tokens: int, + num_draft_tokens: int, + tokens_per_gen_step: int, + num_extend_requests: int, + new_tokens: "torch.Tensor", + next_draft_tokens: "torch.Tensor", + new_tokens_lens: "torch.Tensor", + ) -> None: + """Stage the overlap scheduler's gathers of the previous step's outputs.""" + + def scheduler_metadata(self, metadata: Any) -> bool: + """Stage the sparse-attention indexer's scheduler metadata. + + Returns False, having staged nothing, when the caller must defer the + native computation instead. + """ + + def defer(self, fn: Callable[[], None]) -> None: + """Run ``fn`` (native device work that reads staged values) after the launch.""" + + def flush(self) -> None: + """Issue every write staged so far, before the window closes.""" + + def end(self, record_event: bool = True) -> None: + """Close the window and issue the staged preparation.""" + + def discard(self) -> None: + """Close the window after a failed preparation; the staged writes are dropped.""" + + def set_completion_event(self, event: "torch.cuda.Event") -> None: + """Event the caller recorded right after ``end(record_event=False)``.""" + + def static_inputs_tokens(self) -> int: + """Tokens of the CUDA graph's static inputs the last launch wrote (0: none).""" + + +class SamplerStoreWriter(Protocol): + """Stores a speculative sampler step's outputs and copies the store to the host in one launch.""" + + def supported( + self, + store: Any, + new_tokens: "torch.Tensor", + next_new_tokens: "torch.Tensor", + new_tokens_lens: "torch.Tensor", + next_draft_tokens: "torch.Tensor", + ) -> bool: + """Whether this step's output tensors fit the writer; False keeps the native stores.""" + + def store_and_copy_out( + self, + store: Any, + slots: Sequence[int], + new_tokens: "torch.Tensor", + next_new_tokens: "torch.Tensor", + new_tokens_lens: "torch.Tensor", + next_draft_tokens: "torch.Tensor", + host_new_tokens: "torch.Tensor", + host_new_tokens_lens: "torch.Tensor", + host_next_draft_tokens: "torch.Tensor", + record_event: bool = True, + ) -> None: + """Scatter the outputs into the store rows ``slots`` and copy the store to the host tensors.""" + + def set_completion_event(self, event: "torch.cuda.Event") -> None: + """Event the caller recorded right after ``store_and_copy_out(record_event=False)``.""" + + +class DecodePrologue(Protocol): + """Replaces the decode step's input prologue with one launch. + + Called first in the step's input preprocessing, inside CUDA graph capture + as well. It decides on the host before any device work; returning False + keeps the native prologue, and returning True means every engine-visible + tensor the native prologue writes holds the value it would have written. + """ + + def __call__( + self, runner: Any, inputs: Any, *, enable_spec_decode: bool, runtime_draft_len: int + ) -> bool: ... + + +class ExecutorEvents(Protocol): + """Points in the executor's life a model may act at.""" + + def after_warmup(self) -> None: + """After warmup and CUDA graph capture, before the first request is served.""" + + def before_overlap_loop(self) -> None: + """Right before the overlap executor loop starts, on every rank; must not raise.""" + + def at_shutdown(self) -> None: + """When the executor loop has completed, during cleanup; must not raise.""" + + +@dataclass(frozen=True) +class RuntimeHooks: + """The step-loop hooks a model opts into. All off by default.""" + + # Run the overlap executor loop with the execution stream current, so the + # loop's own stream work and the forward are ordered on one stream. + loop_on_execution_stream: bool = False + # Decode input preparation recorded and issued as one launch (see StepInputStager). + step_input_stager: Optional[StepInputStager] = None + # The speculative sampler's store update and host copy in one launch (see SamplerStoreWriter). + sampler_store_writer: Optional[SamplerStoreWriter] = None + # The decode step's input prologue in one launch (see DecodePrologue). + decode_prologue: Optional[DecodePrologue] = None + # Actions after warmup, before the overlap loop and at shutdown (see ExecutorEvents). + executor_events: Optional[ExecutorEvents] = None + + +DEFAULT_RUNTIME_HOOKS = RuntimeHooks() + + +def runtime_hooks_of(model: object) -> RuntimeHooks: + """The hooks ``model`` declares, or the all-off default when it declares none. + + Declaring hooks is optional for every model class, so absence is the common + case and means the default. + """ + hooks = getattr(model, "runtime_hooks", None) + if hooks is None: + return DEFAULT_RUNTIME_HOOKS + if not isinstance(hooks, RuntimeHooks): + raise TypeError( + f"{type(model).__name__}.runtime_hooks must be a RuntimeHooks, got {type(hooks).__name__}" + ) + return hooks diff --git a/tensorrt_llm/_torch/speculative/eagle3.py b/tensorrt_llm/_torch/speculative/eagle3.py index fc29a0633f41..72561a7642fe 100644 --- a/tensorrt_llm/_torch/speculative/eagle3.py +++ b/tensorrt_llm/_torch/speculative/eagle3.py @@ -352,8 +352,13 @@ def prepare(self): dtype=torch.int, device='cpu', pin_memory=prefer_pinned()) - self.batch_indices_cuda[:num_seqs].copy_(batch_indices, - non_blocking=True) + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy(self.batch_indices_cuda[:num_seqs], + batch_indices) + else: + self.batch_indices_cuda[:num_seqs].copy_(batch_indices, + non_blocking=True) # Snapshot the real (unpadded) scheduled token count before the decrement # below. maybe_capture_hidden_states runs during the target forward and # must bound the capture by this count; self.num_tokens is about to be diff --git a/tensorrt_llm/_torch/speculative/interface.py b/tensorrt_llm/_torch/speculative/interface.py index 8237c09ee8b7..030e6fc9a82b 100644 --- a/tensorrt_llm/_torch/speculative/interface.py +++ b/tensorrt_llm/_torch/speculative/interface.py @@ -19,7 +19,7 @@ from contextlib import contextmanager from dataclasses import dataclass, field from enum import IntEnum -from typing import TYPE_CHECKING, List, Optional, Protocol, Type +from typing import TYPE_CHECKING, ClassVar, List, Optional, Protocol, Type import torch from packaging.version import Version @@ -37,6 +37,7 @@ if TYPE_CHECKING: from ..pyexecutor.guided_decoder import CapturableGuidedDecoder from ..pyexecutor.llm_request import LlmRequest + from ..pyexecutor.runtime_hooks import StepInputStager if IS_FLASHINFER_AVAILABLE: import flashinfer @@ -472,6 +473,8 @@ class SpecMetadata: """ Metadata for speculative decoding. """ + # A step-input stager while it records this metadata's writes, else None. + step_input_stager: ClassVar[Optional["StepInputStager"]] = None # The max number of requests in a single batch. max_num_requests: int # The number of draft layers. (Also the number of draft tokens for the linear tree.) @@ -894,6 +897,11 @@ def _populate_request_rng_state( device='cuda') def _upload(dst: torch.Tensor, values: list[int]) -> None: + if self.step_input_stager is not None: + # Staged write; see the window banner in DecoderRunner._forward_decoder. + self.step_input_stager.copy( + dst[:len(values)], torch.tensor(values, dtype=torch.int64)) + return dst[:len(values)].copy_(torch.tensor(values, dtype=torch.int64, pin_memory=prefer_pinned()), diff --git a/tensorrt_llm/_torch/speculative/spec_sampler_base.py b/tensorrt_llm/_torch/speculative/spec_sampler_base.py index 5fb5b185dc0b..dc40cca3a7c5 100644 --- a/tensorrt_llm/_torch/speculative/spec_sampler_base.py +++ b/tensorrt_llm/_torch/speculative/spec_sampler_base.py @@ -27,9 +27,11 @@ import torch +from ..._utils import prefer_pinned from ...sampling_params import SamplingParams from ..pyexecutor.llm_request import LlmRequest, LlmRequestState, get_draft_token_length from ..pyexecutor.resource_manager import BaseResourceManager +from ..pyexecutor.runtime_hooks import SamplerStoreWriter from ..pyexecutor.sampler import ( DEFAULT_BEAM_IDX, AsyncWorkerMixin, @@ -167,6 +169,10 @@ class SpecSampler(Sampler[SampleStateSpec], AsyncWorkerMixin): SampleState = SampleStateSpec + # The model's one-launch store writer, bound once by instantiate_sampler; None keeps + # the native stores and host copies. + store_writer: Optional[SamplerStoreWriter] = None + def is_generation_model(self) -> bool: return True @@ -442,8 +448,11 @@ def sample_async( for r in sampling_requests ] - slots = torch.as_tensor([r.py_seq_slot for r in sampling_requests], dtype=torch.long) - slots = slots.to(device="cuda", non_blocking=True) + store_writer = self.store_writer + slots = None + if store_writer is None: + slots = torch.as_tensor([r.py_seq_slot for r in sampling_requests], dtype=torch.long) + slots = slots.to(device="cuda", non_blocking=True) o_new_tokens = outputs["new_tokens"][num_skip : num_skip + num_sampling_requests] o_new_tokens_lens = outputs["new_tokens_lens"][num_skip : num_skip + num_sampling_requests] @@ -514,11 +523,28 @@ def sample_async( elif o_next_new_tokens.shape[1] > next_new_tokens_width: o_next_new_tokens = o_next_new_tokens[:, :next_new_tokens_width] - # Use index_copy_ for efficient copying (slots are unique) - self.store.new_tokens.squeeze(-1).T.index_copy_(0, slots, o_new_tokens) - self.store.next_new_tokens.squeeze(-1).T.index_copy_(0, slots, o_next_new_tokens) - self.store.new_tokens_lens.index_copy_(0, slots, o_new_tokens_lens) - self.store.next_draft_tokens.index_copy_(0, slots, o_next_draft_tokens) + # The writer stores the four outputs and copies the three store tensors + # only; a step that publishes verify_lens keeps the native path. + staged_store = ( + store_writer is not None + and o_verify_lens is None + and prefer_pinned() + and not self._async_worker_active() + and store_writer.supported( + self.store, o_new_tokens, o_next_new_tokens, o_new_tokens_lens, o_next_draft_tokens + ) + ) + if not staged_store: + if slots is None: + slots = torch.as_tensor( + [r.py_seq_slot for r in sampling_requests], dtype=torch.long + ) + slots = slots.to(device="cuda", non_blocking=True) + # Use index_copy_ for efficient copying (slots are unique) + self.store.new_tokens.squeeze(-1).T.index_copy_(0, slots, o_new_tokens) + self.store.next_new_tokens.squeeze(-1).T.index_copy_(0, slots, o_next_new_tokens) + self.store.new_tokens_lens.index_copy_(0, slots, o_new_tokens_lens) + self.store.next_draft_tokens.index_copy_(0, slots, o_next_draft_tokens) # Create sample state with async D2H copy device_tensors = SampleStateTensorsSpec( @@ -527,13 +553,43 @@ def sample_async( next_draft_tokens=self.store.next_draft_tokens, ) - host_tensors = SampleStateTensorsSpec( - new_tokens=self._copy_to_host(self.store.new_tokens), - new_tokens_lens=self._copy_to_host(self.store.new_tokens_lens), - next_draft_tokens=self._copy_to_host(self.store.next_draft_tokens), - verify_lens=(self._copy_to_host(o_verify_lens) if o_verify_lens is not None else None), - ) + if staged_store: + # staged_store implies prefer_pinned(), so these buffers are pinned. + host_tensors = SampleStateTensorsSpec( + new_tokens=torch.empty_like( + self.store.new_tokens, device="cpu", pin_memory=prefer_pinned() + ), + new_tokens_lens=torch.empty_like( + self.store.new_tokens_lens, device="cpu", pin_memory=prefer_pinned() + ), + next_draft_tokens=torch.empty_like( + self.store.next_draft_tokens, device="cpu", pin_memory=prefer_pinned() + ), + ) + store_writer.store_and_copy_out( + self.store, + [r.py_seq_slot for r in sampling_requests], + o_new_tokens, + o_next_new_tokens, + o_new_tokens_lens, + o_next_draft_tokens, + host_tensors.new_tokens, + host_tensors.new_tokens_lens, + host_tensors.next_draft_tokens, + record_event=False, + ) + else: + host_tensors = SampleStateTensorsSpec( + new_tokens=self._copy_to_host(self.store.new_tokens), + new_tokens_lens=self._copy_to_host(self.store.new_tokens_lens), + next_draft_tokens=self._copy_to_host(self.store.next_draft_tokens), + verify_lens=( + self._copy_to_host(o_verify_lens) if o_verify_lens is not None else None + ), + ) sampler_event = self._record_sampler_event() + if staged_store: + store_writer.set_completion_event(sampler_event.cuda_event) # Add dummy draft tokens to context requests for KV cache preparation for request in finished_context_requests: diff --git a/tests/unittest/_torch/attention/sparse/dsa/test_dsa_indexer.py b/tests/unittest/_torch/attention/sparse/dsa/test_dsa_indexer.py index 23d90f830a49..b73dc418ef74 100644 --- a/tests/unittest/_torch/attention/sparse/dsa/test_dsa_indexer.py +++ b/tests/unittest/_torch/attention/sparse/dsa/test_dsa_indexer.py @@ -4880,3 +4880,103 @@ def _run_indexer(): assert "indexer_topk_out_buffer" in metadata.cuda_graph_buffers.buffers, ( "indexer topk-output buffer must be drawn from the cuda_graph_buffers arena" ) + + +# --- The subclass seams of Indexer.sparse_attn_indexer ---------------------------------------- + + +def _seam_indexer(): + indexer = Indexer.__new__(Indexer) + torch.nn.Module.__init__(indexer) + return indexer + + +def test_reused_topk_copy_is_the_row_copy(): + gen = torch.Generator().manual_seed(0) + shared = torch.randint(0, 1000, (4, 16), generator=gen, dtype=torch.int32) + buffer = torch.full((12, 16), -1, dtype=torch.int32) + expected = buffer.clone() + expected[2:5, :] = shared[:3, :] + + _seam_indexer()._copy_reused_topk( + SimpleNamespace(shared_topk_indices=shared), buffer, 2, 3, 3, False, None + ) + + assert torch.equal(buffer, expected) + + +def test_fill_decode_topk_leaves_the_rows_to_the_decode_branch(): + buffer = torch.full((6, 8), -1, dtype=torch.int32) + + filled = _seam_indexer()._fill_decode_topk(SimpleNamespace(), None, None, buffer, 0, 6) + + assert filled is False + assert bool((buffer == -1).all()) + + +def _stored_rows_inline( + indexer, + topk_indices_buffer, + metadata, + num_contexts, + num_generations, + num_gen_tokens, + token_offset, + has_prefill, + has_decode, + is_generation, +): + """The statements ``sparse_attn_indexer`` ran in place of ``_store_mtp_shared_rows``.""" + rows = None + if has_decode: + next_n = num_gen_tokens // num_generations + gen_topk = topk_indices_buffer[token_offset : token_offset + num_gen_tokens] + rows = indexer._mtp_last_accepted_rows( + gen_topk, metadata, num_contexts, num_generations, next_n + ) + if has_prefill: + ctx_last = torch.cumsum(metadata.seq_lens_cuda[:num_contexts].to(torch.long), dim=0) - 1 + ctx_rows = topk_indices_buffer[ctx_last] + rows = ctx_rows if rows is None else torch.cat([ctx_rows, rows]) + if rows is not None: + shared_topk_indices = metadata.shared_topk_indices + if shared_topk_indices is None: + metadata.shared_topk_indices = rows.contiguous() + else: + row_start = num_contexts if is_generation else 0 + shared_topk_indices[row_start : row_start + rows.shape[0], : rows.shape[1]].copy_(rows) + + +@pytest.mark.parametrize("has_prefill, has_decode", [(False, True), (True, True), (True, False)]) +@pytest.mark.parametrize("is_generation", [True, False]) +@pytest.mark.parametrize("preset", [False, True], ids=["unset", "preset"]) +@pytest.mark.parametrize("accepted", [None, (0, 0, 1, 4, 2)], ids=["no_counts", "counts"]) +def test_mtp_shared_rows_are_the_inline_statements( + has_prefill, has_decode, is_generation, preset, accepted +): + num_contexts, num_generations, next_n = 2, 3, 4 + seq_lens = torch.tensor([5, 3] + [next_n] * num_generations, dtype=torch.int32) + token_offset = int(seq_lens[:num_contexts].sum()) + num_gen_tokens = num_generations * next_n + gen = torch.Generator().manual_seed(1) + buffer = torch.randint( + 0, 1000, (token_offset + num_gen_tokens, 6), generator=gen, dtype=torch.int32 + ) + + def metadata(): + return SimpleNamespace( + seq_lens_cuda=seq_lens.clone(), + mtp_num_accepted=None if accepted is None else torch.tensor(accepted), + shared_topk_indices=(torch.full((8, 8), -7, dtype=torch.int32) if preset else None), + ) + + args = (num_contexts, num_generations, num_gen_tokens, token_offset) + flags = (has_prefill, has_decode, is_generation) + expected, got = metadata(), metadata() + indexer = _seam_indexer() + _stored_rows_inline(indexer, buffer.clone(), expected, *args, *flags) + seam_buffer = buffer.clone() + indexer._store_mtp_shared_rows(seam_buffer, got, *args, *flags) + + assert torch.equal(seam_buffer, buffer) + assert torch.equal(got.shared_topk_indices, expected.shared_topk_indices) diff --git a/tests/unittest/_torch/attention/test_fmha_manager.py b/tests/unittest/_torch/attention/test_fmha_manager.py index 7cfb59eee093..397bfd93a4b5 100644 --- a/tests/unittest/_torch/attention/test_fmha_manager.py +++ b/tests/unittest/_torch/attention/test_fmha_manager.py @@ -974,3 +974,60 @@ def __init__(self, attn: TrtllmAttention) -> None: assert isinstance(attn._fmha_manager.fmha_libs[0], _NewFmha) assert quant_states_during_construction == [True] assert events == [("support", "old", None)] + + +def _listed_fmha(name: str, available: bool, checked: list) -> type: + """An FMHA library class that records its availability check.""" + + def is_available(cls, attn) -> bool: + checked.append((name, attn)) + return available + + return type( + name, + (), + {"is_available": classmethod(is_available), "__init__": lambda self, attn: None}, + ) + + +@pytest.mark.parametrize("declared", [False, True], ids=["undeclared", "empty"]) +def test_manager_without_prepended_libraries_builds_the_registry_list(declared: bool) -> None: + checked: list = [] + registry = [_listed_fmha("First", True, checked), _listed_fmha("Second", True, checked)] + attn = FakeAttention() + if declared: + attn.prepended_fmha_lib_classes = () + + with patch.object(fmha_manager, "get_enabled_fmha_lib_classes", return_value=registry): + manager = FmhaManager(attn) + + assert [type(lib) for lib in manager.fmha_libs] == registry + assert TrtllmAttention.prepended_fmha_lib_classes == () + + +def test_manager_tries_a_prepended_library_first() -> None: + checked: list = [] + registry = [_listed_fmha("First", True, checked), _listed_fmha("Second", True, checked)] + prepended = _listed_fmha("Prepended", True, checked) + attn = FakeAttention() + attn.prepended_fmha_lib_classes = (prepended,) + + with patch.object(fmha_manager, "get_enabled_fmha_lib_classes", return_value=registry): + manager = FmhaManager(attn) + + assert [type(lib) for lib in manager.fmha_libs] == [prepended, *registry] + assert [name for name, _ in checked] == ["Prepended", "First", "Second"] + assert all(checked_attn is attn for _, checked_attn in checked) + + +def test_manager_skips_an_unavailable_prepended_library() -> None: + checked: list = [] + registry = [_listed_fmha("First", True, checked)] + attn = FakeAttention() + attn.prepended_fmha_lib_classes = (_listed_fmha("Prepended", False, checked),) + + with patch.object(fmha_manager, "get_enabled_fmha_lib_classes", return_value=registry): + manager = FmhaManager(attn) + + assert [type(lib) for lib in manager.fmha_libs] == registry + assert [name for name, _ in checked] == ["Prepended", "First"] diff --git a/tests/unittest/_torch/executor/test_gpu_keepalive.py b/tests/unittest/_torch/executor/test_gpu_keepalive.py index 5c3318c00690..51d9b63b277e 100644 --- a/tests/unittest/_torch/executor/test_gpu_keepalive.py +++ b/tests/unittest/_torch/executor/test_gpu_keepalive.py @@ -315,6 +315,7 @@ class Stub: # notified first, then the keepalive is closed, then the PP handles are awaited. class CleanupStub: _executor_loop_cleanup = pe.PyExecutor._executor_loop_cleanup + _executor_events = None def __init__(self, keepalive): self.events = [] diff --git a/tests/unittest/_torch/executor/test_py_executor.py b/tests/unittest/_torch/executor/test_py_executor.py index b571678d3c5c..c24923d0b935 100644 --- a/tests/unittest/_torch/executor/test_py_executor.py +++ b/tests/unittest/_torch/executor/test_py_executor.py @@ -102,6 +102,7 @@ def test_forward_step_carries_context_logits_request_to_runner(request_flags, ex iter_counter=0, sampler=Mock(get_cache_indirection=Mock(return_value=cache_indirection)), execution_stream=Mock(), + _loop_on_execution_stream=False, _iter_adp_dummy_ctx_tokens=0, _iter_adp_dummy_gen_tokens=0, _compute_adp_dummy_tokens=PyExecutor._compute_adp_dummy_tokens, @@ -2243,6 +2244,8 @@ class _CleanupStub: """Stub for _executor_loop_cleanup: records the order in which the shutdown notification and PP-handle wait happen.""" + _executor_events = None + def __init__(self, pp_handles_raise=False): self.response_lock = threading.Lock() self.response_cv = threading.Condition(self.response_lock) diff --git a/tests/unittest/_torch/executor/test_runtime_hooks.py b/tests/unittest/_torch/executor/test_runtime_hooks.py new file mode 100644 index 000000000000..1f3e63148340 --- /dev/null +++ b/tests/unittest/_torch/executor/test_runtime_hooks.py @@ -0,0 +1,918 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""How the engine reads the step-loop hooks a model declares (``runtime_hooks_of``). + +The engine's call sites run their original statements when a hook is off: every +object a hook attaches to declares it off at class level, every call a call +site makes on a hook is a method of the hook's protocol, every read of a hook in +the engine files is decided by its all-off value toward the original statements +(checked on the source), and call sites of each hook run on stand-ins issue +their original work with the hook off and hand it to the hook when it is on. +""" + +import ast +import contextlib +import dataclasses +import inspect +import types +from pathlib import Path + +import pytest +import torch + +import tensorrt_llm +from tensorrt_llm._torch.pyexecutor.runtime_hooks import ( + DEFAULT_RUNTIME_HOOKS, + ExecutorEvents, + RuntimeHooks, + SamplerStoreWriter, + StepInputStager, + runtime_hooks_of, +) + +pytestmark = pytest.mark.cpu_only + + +class _PlainModel(torch.nn.Module): + """A model that declares no hooks, like every model outside the opt-in.""" + + +class _ClassHooksModel(torch.nn.Module): + runtime_hooks = RuntimeHooks(loop_on_execution_stream=True) + + +class _Events: + def after_warmup(self) -> None: + pass + + def before_overlap_loop(self) -> None: + pass + + def at_shutdown(self) -> None: + pass + + +@pytest.mark.parametrize("model", [_PlainModel(), object()], ids=["module", "object"]) +def test_a_model_without_hooks_gets_the_all_off_default(model): + hooks = runtime_hooks_of(model) + + assert hooks is DEFAULT_RUNTIME_HOOKS + assert hooks == RuntimeHooks() + assert not hooks.loop_on_execution_stream + assert hooks.step_input_stager is None + assert hooks.sampler_store_writer is None + assert hooks.decode_prologue is None + assert hooks.executor_events is None + + +def test_every_field_defaults_to_off(): + for field in dataclasses.fields(RuntimeHooks): + assert getattr(DEFAULT_RUNTIME_HOOKS, field.name) in (False, None), field.name + + +def test_hooks_declared_as_none_mean_the_default(): + model = _PlainModel() + model.runtime_hooks = None + + assert runtime_hooks_of(model) is DEFAULT_RUNTIME_HOOKS + + +def test_class_attribute_hooks_are_returned_as_declared(): + assert runtime_hooks_of(_ClassHooksModel()) is _ClassHooksModel.runtime_hooks + + +def test_instance_attribute_hooks_are_returned_as_declared(): + hooks = RuntimeHooks(executor_events=_Events()) + model = _PlainModel() + model.runtime_hooks = hooks + + assert runtime_hooks_of(model) is hooks + + +def test_instance_attribute_overrides_class_attribute(): + hooks = RuntimeHooks() + model = _ClassHooksModel() + model.runtime_hooks = hooks + + assert runtime_hooks_of(model) is hooks + + +@pytest.mark.parametrize( + "declared", + [True, {"loop_on_execution_stream": True}, "RuntimeHooks()"], + ids=["bool", "dict", "str"], +) +def test_hooks_of_the_wrong_type_raise(declared): + model = _PlainModel() + model.runtime_hooks = declared + + with pytest.raises(TypeError, match=r"_PlainModel\.runtime_hooks must be a RuntimeHooks"): + runtime_hooks_of(model) + + +def test_hooks_are_immutable_once_declared(): + hooks = RuntimeHooks() + + with pytest.raises(dataclasses.FrozenInstanceError): + hooks.loop_on_execution_stream = True + + +def test_engine_classes_declare_the_stager_off(): + """Every object a hook attaches to declares the attribute off at class level.""" + from tensorrt_llm._torch.attention.backends.sparse.dsa.metadata import ( + DSAtrtllmAttentionMetadata, + ) + from tensorrt_llm._torch.attention.backends.trtllm import TrtllmAttentionMetadata + from tensorrt_llm._torch.pyexecutor.cuda_graph_runner import CUDAGraphRunner + from tensorrt_llm._torch.pyexecutor.engine.runners.decoder.runner import DecoderRunner + from tensorrt_llm._torch.pyexecutor.kv_cache.kv_cache_manager_v2 import KVCacheManagerV2 + from tensorrt_llm._torch.pyexecutor.py_executor import PyExecutor + from tensorrt_llm._torch.speculative.eagle3 import Eagle3OneModelSpecMetadata + from tensorrt_llm._torch.speculative.interface import SpecMetadata + from tensorrt_llm._torch.speculative.spec_sampler_base import SpecSampler + + for cls in ( + TrtllmAttentionMetadata, + DSAtrtllmAttentionMetadata, + SpecMetadata, + Eagle3OneModelSpecMetadata, + ): + assert cls.step_input_stager is None, cls + # A class attribute, not a dataclass field: the constructors do not take it. + assert "step_input_stager" not in {f.name for f in dataclasses.fields(cls)}, cls + assert DecoderRunner.step_input_stager is None + assert DecoderRunner.runtime_hooks is DEFAULT_RUNTIME_HOOKS + assert KVCacheManagerV2.step_input_stager is None + assert CUDAGraphRunner.staged_static_input_tokens == 0 + assert SpecSampler.store_writer is None + assert PyExecutor._execution_stream_loop_armed is False + assert PyExecutor._loop_on_execution_stream is False + assert PyExecutor._executor_events is None + + +# Per hook protocol: the local names and attribute names a call site holds the hook in, and +# the engine files with call sites. +_HOOK_CONSUMERS = { + StepInputStager: ( + ("stager", "stager_source"), + ("step_input_stager",), + ( + "pyexecutor/engine/runners/decoder/runner.py", + "pyexecutor/kv_cache/kv_cache_manager_v2.py", + "attention/backends/trtllm.py", + "attention/backends/sparse/dsa/metadata.py", + "attention/backends/sparse/dsa/indexer.py", + "speculative/eagle3.py", + "speculative/interface.py", + ), + ), + SamplerStoreWriter: ( + ("store_writer",), + ("store_writer",), + ("speculative/spec_sampler_base.py",), + ), + ExecutorEvents: ((), ("_executor_events",), ("pyexecutor/py_executor.py",)), +} + + +def _protocol_methods(protocol) -> set: + return { + name + for name, value in vars(protocol).items() + if inspect.isfunction(value) and not name.startswith("_") + } + + +def _hook_calls(tree: ast.AST, local_names, attr_names): + """``(method, line)`` of every call on a hook held in one of the locals or attributes.""" + for node in ast.walk(tree): + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute)): + continue + receiver = node.func.value + if (isinstance(receiver, ast.Name) and receiver.id in local_names) or ( + isinstance(receiver, ast.Attribute) and receiver.attr in attr_names + ): + yield node.func.attr, node.lineno + + +@pytest.mark.parametrize("protocol", list(_HOOK_CONSUMERS), ids=lambda p: p.__name__) +def test_every_hook_call_site_uses_its_protocol(protocol): + """A call site calls only its hook's protocol methods, and the protocol has no unused method.""" + local_names, attr_names, consumers = _HOOK_CONSUMERS[protocol] + root = Path(tensorrt_llm.__file__).resolve().parent / "_torch" + methods = _protocol_methods(protocol) + used, unknown = set(), [] + for rel in consumers: + tree = ast.parse((root / rel).read_text()) + calls = list(_hook_calls(tree, local_names, attr_names)) + assert calls, f"{rel} has no {protocol.__name__} call site" + for method, line in calls: + used.add(method) + if method not in methods: + unknown.append(f"{rel}:{line}: {method}") + assert not unknown, f"calls outside the {protocol.__name__} protocol:\n" + "\n".join(unknown) + assert used == methods + + +class _StoreWriterModel(torch.nn.Module): + def __init__(self, writer) -> None: + super().__init__() + self.runtime_hooks = RuntimeHooks(sampler_store_writer=writer) + + +def _instantiate_sampler(monkeypatch, model, sampler): + """``instantiate_sampler`` for a speculative engine whose spec decoder is ``sampler``.""" + from tensorrt_llm._torch.pyexecutor import _util + + monkeypatch.setattr(_util, "confidential_compute_enabled", lambda: False) + monkeypatch.setattr(_util, "resolve_max_num_sequences", lambda *args, **kwargs: 8) + monkeypatch.setattr(_util, "create_torch_sampler_args", lambda **kwargs: "sampler args") + monkeypatch.setattr(_util, "get_spec_decoder", lambda args, spec_config: sampler) + engine = types.SimpleNamespace( + max_seq_len=64, + spec_config=types.SimpleNamespace( + spec_dec_mode=types.SimpleNamespace(has_spec_decoder=lambda: True) + ), + model=model, + ) + llm_args = types.SimpleNamespace( + sampler_force_async_worker=False, + disable_overlap_scheduler=False, + enable_speculative_beam_history_d2h=False, + ) + return _util.instantiate_sampler( + engine, + llm_args, + None, + max_batch_size=8, + max_beam_width=1, + mm_encoder_only=False, + speculative_config=None, + ) + + +def test_instantiate_sampler_binds_no_writer_without_hooks(monkeypatch): + from tensorrt_llm._torch.speculative.spec_sampler_base import SpecSampler + + sampler = SpecSampler.__new__(SpecSampler) + + assert _instantiate_sampler(monkeypatch, _PlainModel(), sampler) is sampler + assert "store_writer" not in vars(sampler) + assert sampler.store_writer is None + + +def test_instantiate_sampler_binds_the_declared_writer(monkeypatch): + from tensorrt_llm._torch.speculative.spec_sampler_base import SpecSampler + + writer = object() + sampler = SpecSampler.__new__(SpecSampler) + + assert _instantiate_sampler(monkeypatch, _StoreWriterModel(writer), sampler) is sampler + assert sampler.store_writer is writer + + +def test_executor_events_protocol(): + assert _protocol_methods(ExecutorEvents) == { + "after_warmup", + "before_overlap_loop", + "at_shutdown", + } + events = _Events() + assert RuntimeHooks(executor_events=events).executor_events is events + + +class _StreamRecorder: + """Stands in for ``torch.cuda.stream``: records the streams it is entered with.""" + + def __init__(self) -> None: + self.entered = [] + + @contextlib.contextmanager + def __call__(self, stream): + self.entered.append(stream) + yield + + +def _scope_executor(armed: bool, **overrides): + """A ``PyExecutor`` with only the attributes ``_overlap_loop_stream_scope`` reads.""" + from tensorrt_llm._torch.pyexecutor.py_executor import PyExecutor + + executor = PyExecutor.__new__(PyExecutor) + executor._execution_stream_loop_armed = armed + executor.execution_stream = object() + attrs = dict( + drafter=None, + draft_model_engine=None, + guided_decoder=None, + kv_connector_manager=None, + is_encoder_decoder=False, + _mm_encoder_item_scheduling_enabled=False, + ) + attrs.update(overrides) + for name, value in attrs.items(): + setattr(executor, name, value) + return executor + + +def test_overlap_loop_scope_is_a_no_op_when_not_armed(monkeypatch): + executor = _scope_executor(armed=False) + recorder = _StreamRecorder() + monkeypatch.setattr(torch.cuda, "stream", recorder) + + with executor._overlap_loop_stream_scope(): + assert executor._loop_on_execution_stream is False + assert recorder.entered == [] + assert "_loop_on_execution_stream" not in vars(executor) + + +def test_overlap_loop_scope_engages_when_armed(monkeypatch): + executor = _scope_executor(armed=True) + recorder = _StreamRecorder() + monkeypatch.setattr(torch.cuda, "stream", recorder) + + with executor._overlap_loop_stream_scope(): + assert executor._loop_on_execution_stream is True + assert recorder.entered == [executor.execution_stream] + assert executor._loop_on_execution_stream is False + + +def test_overlap_loop_scope_resets_the_flag_when_the_loop_raises(monkeypatch): + executor = _scope_executor(armed=True) + monkeypatch.setattr(torch.cuda, "stream", _StreamRecorder()) + + with pytest.raises(RuntimeError, match="loop failed"): + with executor._overlap_loop_stream_scope(): + raise RuntimeError("loop failed") + assert executor._loop_on_execution_stream is False + + +@pytest.mark.parametrize( + "name, value", + [ + ("drafter", object()), + ("draft_model_engine", object()), + ("guided_decoder", object()), + ("kv_connector_manager", object()), + ("is_encoder_decoder", True), + ("_mm_encoder_item_scheduling_enabled", True), + ], +) +def test_overlap_loop_scope_stays_off_with_side_streams(monkeypatch, name, value): + executor = _scope_executor(armed=True, **{name: value}) + recorder = _StreamRecorder() + monkeypatch.setattr(torch.cuda, "stream", recorder) + + with executor._overlap_loop_stream_scope(): + assert executor._loop_on_execution_stream is False + assert recorder.entered == [] + + +def test_instantiate_sampler_leaves_other_spec_decoders_alone(monkeypatch): + sampler = types.SimpleNamespace() + + assert _instantiate_sampler(monkeypatch, _StoreWriterModel(object()), sampler) is sampler + assert "store_writer" not in vars(sampler) + + +# --------------------------------------------------------------------------------------------- +# With every hook off, every call site runs its original statements. +# +# The check below reads the engine files that consume a hook and evaluates each read of a hook +# at its all-off value. An ``if`` that reads a hook must be decided by that value; the branch it +# takes is checked like any other code and the other branch is not run, so it may use the hook +# freely. Outside such tests, live code may only bind a hook value to a local (or to an +# attribute that declares the hook) at its all-off value. Any other read is reported with its +# file and line. +# --------------------------------------------------------------------------------------------- + +_SEAM_CONSUMERS = ( + "pyexecutor/engine/runners/decoder/runner.py", + "pyexecutor/cuda_graph_runner.py", + "pyexecutor/kv_cache/kv_cache_manager_v2.py", + "pyexecutor/py_executor.py", + "pyexecutor/_util.py", + "attention/backends/trtllm.py", + "attention/backends/sparse/dsa/metadata.py", + "attention/backends/sparse/dsa/indexer.py", + "speculative/eagle3.py", + "speculative/interface.py", + "speculative/spec_sampler_base.py", +) + +# Attributes that hold a hook, and their value when every hook is off. +_SEAM_ATTRS = { + "step_input_stager": None, + "store_writer": None, + "_executor_events": None, + "_loop_on_execution_stream": False, + "_execution_stream_loop_armed": False, + "staged_static_input_tokens": 0, + "runtime_hooks": DEFAULT_RUNTIME_HOOKS, +} +# Locals the call sites hold a hook (or a decision on one) in. +_SEAM_LOCALS = ("stager", "stager_source", "store_writer", "prologue", "staged", "staged_store") +_HOOK_FIELDS = {field.name for field in dataclasses.fields(RuntimeHooks)} + + +class _Unknown: + """A value the all-off evaluation cannot decide.""" + + +_UNKNOWN = _Unknown() + + +class _AllOffWalk: + """Walks one engine file with every hook at its all-off value.""" + + def __init__(self, rel: str) -> None: + self.rel = rel + self.problems = [] + self.decided = 0 + + def report(self, node: ast.AST, what: str) -> None: + self.problems.append(f"{self.rel}:{node.lineno}: {what}") + + # -- reads of a hook ------------------------------------------------------------------ + @staticmethod + def _is_seam_call(node: ast.AST) -> bool: + if not (isinstance(node, ast.Call) and isinstance(node.func, ast.Name)): + return False + if node.func.id == "runtime_hooks_of": + return True + return ( + node.func.id == "getattr" + and len(node.args) >= 2 + and isinstance(node.args[1], ast.Constant) + and node.args[1].value == "step_input_stager" + ) + + def reads_seam(self, node: ast.AST, env: dict) -> bool: + for sub in ast.walk(node): + if isinstance(sub, ast.Attribute) and sub.attr in _SEAM_ATTRS: + return True + if isinstance(sub, ast.Name) and sub.id in env: + return True + if self._is_seam_call(sub): + return True + return False + + def value(self, node: ast.AST, env: dict): + """The all-off value of ``node``, or ``_UNKNOWN``.""" + if isinstance(node, ast.Constant): + return node.value + if isinstance(node, ast.Name): + return env.get(node.id, _UNKNOWN) + if isinstance(node, ast.Attribute): + receiver = self.value(node.value, env) + if receiver is DEFAULT_RUNTIME_HOOKS and node.attr in _HOOK_FIELDS: + return getattr(DEFAULT_RUNTIME_HOOKS, node.attr) + if node.attr in _SEAM_ATTRS: + return _SEAM_ATTRS[node.attr] + return _UNKNOWN + if self._is_seam_call(node): + return DEFAULT_RUNTIME_HOOKS if node.func.id == "runtime_hooks_of" else None + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not): + operand = self.value(node.operand, env) + return _UNKNOWN if operand is _UNKNOWN else not operand + if isinstance(node, ast.BoolOp): + values = [self.value(v, env) for v in node.values] + known = [v for v in values if v is not _UNKNOWN] + if isinstance(node.op, ast.And): + if any(not v for v in known): + return False + return True if len(known) == len(values) else _UNKNOWN + if any(known): + return True + return False if len(known) == len(values) else _UNKNOWN + if isinstance(node, ast.Compare) and len(node.ops) == 1: + left, right = self.value(node.left, env), self.value(node.comparators[0], env) + op = node.ops[0] + if isinstance(op, (ast.Is, ast.IsNot)) and left is not _UNKNOWN: + if isinstance(node.comparators[0], ast.Constant) and right is None: + return (left is None) == isinstance(op, ast.Is) + if ( + isinstance(op, ast.Eq) + and isinstance(node.left, ast.Attribute) + and node.left.attr == "staged_static_input_tokens" + ): + # No tokens were staged; a graph replay has at least one token. + return False + return _UNKNOWN + + # -- statements ------------------------------------------------------------------------- + def block(self, body, env: dict) -> bool: + """Walk live statements; True when the block always ends in return or raise.""" + for stmt in body: + if self.stmt(stmt, env): + return True + return False + + def bind(self, stmt, targets, value, env: dict) -> None: + reads = self.reads_seam(value, env) + names = [t.id for t in targets if isinstance(t, ast.Name)] + attrs = [t for t in targets if isinstance(t, ast.Attribute) and t.attr in _SEAM_ATTRS] + if not (reads or attrs or any(name in env or name in _SEAM_LOCALS for name in names)): + return + if len(names) + len(attrs) != len(targets): + self.report(stmt, "a hook value is bound to something other than a local") + return + off = self.value(value, env) + if off is _UNKNOWN: + if reads or attrs: + self.report(stmt, "a hook value with every hook off is not decided") + for name in names: + env.pop(name, None) + return + for name in names: + env[name] = off + for attr in attrs: + if off != _SEAM_ATTRS[attr.attr]: + self.report(stmt, f"{attr.attr} is set to a value other than its all-off value") + + def stmt(self, stmt: ast.stmt, env: dict) -> bool: + if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): + self.block(stmt.body, dict(env)) + return False + if isinstance(stmt, ast.ClassDef): + self.block(stmt.body, {}) + return False + if isinstance(stmt, (ast.If, ast.While)): + if not self.reads_seam(stmt.test, env): + body_ends = self.block(stmt.body, env) + else_ends = self.block(stmt.orelse, env) + return isinstance(stmt, ast.If) and body_ends and else_ends + taken = self.value(stmt.test, env) + if taken is _UNKNOWN: + self.report(stmt, "a test of a hook is not decided with every hook off") + return False + self.decided += 1 + if isinstance(stmt, ast.While) and taken: + self.report(stmt, "a loop on a hook runs with every hook off") + return False + return self.block(stmt.body if taken else stmt.orelse, env) + if isinstance(stmt, ast.Assign): + self.bind(stmt, stmt.targets, stmt.value, env) + return False + if isinstance(stmt, ast.AnnAssign): + if stmt.value is not None: + self.bind(stmt, [stmt.target], stmt.value, env) + return False + if isinstance(stmt, (ast.With, ast.AsyncWith)): + for item in stmt.items: + if self.reads_seam(item.context_expr, env): + self.report(stmt, "a hook is entered as a context") + return self.block(stmt.body, env) + if isinstance(stmt, ast.Try): + ends = self.block(stmt.body, env) + for handler in stmt.handlers: + self.block(handler.body, env) + self.block(stmt.orelse, env) + return self.block(stmt.finalbody, env) or (ends and not stmt.handlers) + if isinstance(stmt, (ast.For, ast.AsyncFor)): + if self.reads_seam(stmt.iter, env): + self.report(stmt, "a loop iterates over a hook") + self.block(stmt.body, env) + self.block(stmt.orelse, env) + return False + if self.reads_seam(stmt, env): + self.report(stmt, "a hook is used outside a test decided with every hook off") + return isinstance(stmt, (ast.Return, ast.Raise)) + + +@pytest.mark.parametrize("rel", _SEAM_CONSUMERS) +def test_every_seam_read_has_a_native_branch(rel): + """With every hook off, each call site takes the branch with its original statements.""" + root = Path(tensorrt_llm.__file__).resolve().parent / "_torch" + walk = _AllOffWalk(rel) + walk.block(ast.parse((root / rel).read_text()).body, {}) + assert not walk.problems, "\n".join(walk.problems) + assert walk.decided, f"{rel} has no call site of a hook" + + +def test_the_seam_check_reports_undecided_reads(): + source = """ +def step(self, x): + stager = self.step_input_stager + if stager is not None: + stager.copy(x) + else: + x.copy_() + stager.flush() + if x or stager: + pass + self.step_input_stager = x +""" + walk = _AllOffWalk("sample.py") + walk.block(ast.parse(source).body, {}) + assert walk.decided == 1 + assert walk.problems == [ + "sample.py:8: a hook is used outside a test decided with every hook off", + "sample.py:9: a test of a hook is not decided with every hook off", + "sample.py:11: a hook value with every hook off is not decided", + ] + + +def test_the_decode_prologue_call_site_uses_its_protocol(): + """The runner calls the prologue with the protocol's positional and keyword parameters.""" + from tensorrt_llm._torch.pyexecutor.runtime_hooks import DecodePrologue + + signature = inspect.signature(DecodePrologue.__call__) + positional = [ + p.name + for p in signature.parameters.values() + if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD and p.name != "self" + ] + keywords = { + p.name for p in signature.parameters.values() if p.kind is inspect.Parameter.KEYWORD_ONLY + } + root = Path(tensorrt_llm.__file__).resolve().parent / "_torch" + tree = ast.parse((root / "pyexecutor/engine/runners/decoder/runner.py").read_text()) + calls = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "prologue" + ] + assert len(calls) == 1 + assert len(calls[0].args) == len(positional) + assert {keyword.arg for keyword in calls[0].keywords} == keywords + + +# -- the decode prologue ------------------------------------------------------------------------ + + +class _AttnMetadata: + def __init__(self) -> None: + self.kv_lens_updates = 0 + + def on_update_kv_lens(self) -> None: + self.kv_lens_updates += 1 + + +def _prologue_runner(prologue): + """A ``DecoderRunner`` with only what ``_preprocess_inputs`` reads without spec decoding.""" + from tensorrt_llm._torch.pyexecutor.engine.runners.decoder.runner import DecoderRunner + + runner = DecoderRunner.__new__(DecoderRunner) + if prologue is not None: + runner.runtime_hooks = RuntimeHooks(decode_prologue=prologue) + runner.guided_decoder = None + runner._config = types.SimpleNamespace(disable_overlap_scheduler=True) + runner.mapping = types.SimpleNamespace(has_cp_helix=lambda: False) + return runner + + +def test_preprocess_inputs_runs_the_native_prologue_without_a_hook(): + runner = _prologue_runner(None) + inputs = {"attn_metadata": _AttnMetadata()} + + assert runner.runtime_hooks is DEFAULT_RUNTIME_HOOKS + assert runner._preprocess_inputs(inputs, enable_spec_decode=False, runtime_draft_len=0) is ( + inputs + ) + assert inputs["attn_metadata"].kv_lens_updates == 1 + + +@pytest.mark.parametrize("engaged", [False, True], ids=["declined", "engaged"]) +def test_preprocess_inputs_keeps_the_native_prologue_when_the_hook_declines(engaged): + calls = [] + + def prologue(runner, inputs, *, enable_spec_decode, runtime_draft_len): + calls.append((runner, inputs, enable_spec_decode, runtime_draft_len)) + return engaged + + runner = _prologue_runner(prologue) + inputs = {"attn_metadata": _AttnMetadata()} + + assert runner._preprocess_inputs(inputs, enable_spec_decode=False, runtime_draft_len=3) is ( + inputs + ) + assert calls == [(runner, inputs, False, 3)] + assert inputs["attn_metadata"].kv_lens_updates == (0 if engaged else 1) + + +# -- the execution-stream loop -------------------------------------------------------------------- + + +class _Stream: + def __init__(self, name: str, log: list) -> None: + self.name, self.log = name, log + + def wait_stream(self, other: "_Stream") -> None: + self.log.append(f"{self.name} waits for {other.name}") + + +def _forward_step_executor(monkeypatch, log: list): + """A ``PyExecutor`` with only what ``_forward_step`` reads, logging its stream work.""" + from tensorrt_llm._torch.pyexecutor import py_executor + from tensorrt_llm._torch.pyexecutor.py_executor import PyExecutor + + current = _Stream("current", log) + + @contextlib.contextmanager + def stream(s): + log.append(f"enter {s.name}") + yield + log.append(f"leave {s.name}") + + monkeypatch.setattr(torch.cuda, "current_stream", lambda: current) + monkeypatch.setattr(torch.cuda, "stream", stream) + monkeypatch.setattr(py_executor, "nvtx_range", lambda message: (lambda fn: fn)) + + executor = PyExecutor.__new__(PyExecutor) + executor.iter_counter = 0 + executor.execution_stream = _Stream("execution", log) + executor.resource_manager = object() + executor.sampler = types.SimpleNamespace(get_cache_indirection=lambda: None) + executor.model_engine = types.SimpleNamespace( + forward=lambda *args, **kwargs: log.append("forward") or "outputs" + ) + executor._iter_adp_dummy_ctx_tokens = 0 + executor._iter_adp_dummy_gen_tokens = 0 + executor._compute_adp_dummy_tokens = lambda requests: (0, 0) + executor._maybe_record_hang_diagnostic_phase = lambda *args, **kwargs: None + executor._attach_encoder_output_to_execution_stream = lambda requests: None + executor._mark_cross_kv_projection_consumed = lambda requests: None + executor._kv_connector_wait_for_save = lambda: None + return executor + + +_NO_REQUESTS = types.SimpleNamespace( + context_requests=[], num_context_requests=0, num_generation_requests=0 +) + + +def test_forward_step_orders_the_streams_off_the_execution_stream_loop(monkeypatch): + log = [] + executor = _forward_step_executor(monkeypatch, log) + + assert executor._forward_step(_NO_REQUESTS) == "outputs" + assert log == [ + "execution waits for current", + "enter execution", + "forward", + "leave execution", + "current waits for execution", + ] + + +def test_forward_step_skips_the_waits_on_the_execution_stream_loop(monkeypatch): + log = [] + executor = _forward_step_executor(monkeypatch, log) + executor._loop_on_execution_stream = True + + assert executor._forward_step(_NO_REQUESTS) == "outputs" + assert log == ["enter execution", "forward", "leave execution"] + + +# -- executor events -------------------------------------------------------------------------- + + +def _cleanup_executor(events, completed: bool): + """A ``PyExecutor`` with only what ``_executor_loop_cleanup`` reads, without pipeline stages.""" + import threading + + from tensorrt_llm._torch.pyexecutor.py_executor import PyExecutor + + executor = PyExecutor.__new__(PyExecutor) + executor.response_cv = threading.Condition() + executor.shutdown_event = threading.Event() + executor.num_micro_batches = 0 + executor._event_loop_completed = completed + if events is not None: + executor._executor_events = events + return executor + + +class _RecordingEvents(_Events): + def __init__(self) -> None: + self.calls = [] + + def at_shutdown(self) -> None: + self.calls.append("at_shutdown") + + +@pytest.mark.parametrize("completed", [False, True]) +def test_executor_loop_cleanup_without_events(completed): + executor = _cleanup_executor(None, completed) + + executor._executor_loop_cleanup() + assert executor.is_shutdown and executor.shutdown_event.is_set() + + +@pytest.mark.parametrize("completed", [False, True]) +def test_executor_loop_cleanup_calls_at_shutdown_after_a_completed_loop(completed): + events = _RecordingEvents() + executor = _cleanup_executor(events, completed) + + executor._executor_loop_cleanup() + assert executor.is_shutdown + assert events.calls == (["at_shutdown"] if completed else []) + + +# -- the step-input stager ------------------------------------------------------------------ + + +class _RecordingStager: + def __init__(self) -> None: + self.calls = [] + + def __getattr__(self, name): + return lambda *args, **kwargs: self.calls.append((name, args, kwargs)) + + +def _block_offsets_manager(monkeypatch, log: list): + """A ``KVCacheManagerV2`` with only what ``copy_batch_block_offsets`` reads (one page table).""" + from tensorrt_llm._torch.pyexecutor.kv_cache import kv_cache_manager_v2 + from tensorrt_llm._torch.pyexecutor.kv_cache.kv_cache_manager_v2 import KVCacheManagerV2 + + monkeypatch.setattr( + kv_cache_manager_v2, + "copy_batch_block_offsets_to_device", + lambda *args: log.append(("native", args)), + ) + manager = KVCacheManagerV2.__new__(KVCacheManagerV2) + manager._publish_sparse_metadata = lambda: None + manager.max_copy_beam_width = 1 + manager.kv_cache_type = None + manager._use_per_layer_page_tables = False + manager.index_mapper = types.SimpleNamespace( + get_copy_index=lambda request_ids, num_contexts, beam_width, cross: torch.tensor([1, 0]) + ) + manager.host_kv_cache_block_offsets = torch.zeros(2, 4, dtype=torch.int32) + manager.index_scales = torch.ones(1, dtype=torch.int32) + manager._index_scale_ints = [1] + manager.kv_offset = torch.zeros(1, dtype=torch.int32) + manager._stream = types.SimpleNamespace(cuda_stream=7) + return manager + + +def test_kv_cache_block_offsets_copy_natively_without_a_stager(monkeypatch): + log = [] + manager = _block_offsets_manager(monkeypatch, log) + dst = torch.empty(2, 4, dtype=torch.int32) + + manager.copy_batch_block_offsets(dst, [3, 4], 1, 0, 2) + assert len(log) == 1 + kind, args = log[0] + assert kind == "native" + assert args[0] is manager.host_kv_cache_block_offsets and args[1] is dst + assert args[2].tolist() == [1, 0] + assert args[3] is manager.index_scales and args[4] is manager.kv_offset and args[5] == 7 + + +def test_kv_cache_block_offsets_are_staged_with_a_stager(monkeypatch): + log = [] + manager = _block_offsets_manager(monkeypatch, log) + manager.step_input_stager = _RecordingStager() + dst = torch.empty(2, 4, dtype=torch.int32) + + manager.copy_batch_block_offsets(dst, [3, 4], 1, 0, 2) + assert log == [] + ((name, args, _),) = manager.step_input_stager.calls + assert name == "block_offsets" + assert args[0] is manager.host_kv_cache_block_offsets and args[1] is dst + assert args[2:] == ([1, 0], [1], [0]) + + +def _slot_mapping_inputs(tokens: int): + metadata = types.SimpleNamespace( + host_indexer_k_cache_block_offsets=torch.tensor([[0, 1]], dtype=torch.int32), + host_slot_mapping_fp8=torch.zeros(tokens, dtype=torch.int64), + host_slot_mapping_scale=torch.zeros(tokens, dtype=torch.int64), + slot_mapping_fp8=torch.full((tokens,), -1, dtype=torch.int64), + slot_mapping_scale=torch.full((tokens,), -1, dtype=torch.int64), + ) + params = types.SimpleNamespace( + batch_size=1, + tokens_per_block=4, + head_dim=8, + new_kv_tokens=torch.tensor([tokens]), + data_bytes_per_token=8, + cached_kv_tokens=torch.tensor([1]), + quant_block_size=8, + ) + return metadata, params + + +def test_indexer_slot_mappings_copy_natively_without_a_stager(): + from tensorrt_llm._torch.attention.backends.sparse.dsa.indexer import Indexer + + metadata, params = _slot_mapping_inputs(3) + + Indexer.recompute_slot_mappings(metadata, params) + assert torch.equal(metadata.slot_mapping_fp8, metadata.host_slot_mapping_fp8) + assert torch.equal(metadata.slot_mapping_scale, metadata.host_slot_mapping_scale) + + +def test_indexer_slot_mappings_are_staged_with_a_stager(): + from tensorrt_llm._torch.attention.backends.sparse.dsa.indexer import Indexer + + metadata, params = _slot_mapping_inputs(3) + metadata.step_input_stager = _RecordingStager() + + Indexer.recompute_slot_mappings(metadata, params) + assert [name for name, _, _ in metadata.step_input_stager.calls] == ["copy", "copy"] + assert bool((metadata.slot_mapping_fp8 == -1).all()) + assert bool((metadata.slot_mapping_scale == -1).all()) From 23324b77981d1b88d4e87657b77815cb82974217 Mon Sep 17 00:00:00 2001 From: Xiao Wang <24860335+xwang233@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:10:38 -0700 Subject: [PATCH 02/19] [None][feat] GLM-MoE-DSA: CUDA kernels and torch ops for GLM-5.2 decode Add model-specific CUDA kernels under cpp/tensorrt_llm/kernels/glmMoeDsa/ and their torch ops under cpp/tensorrt_llm/thop/glmMoeDsa/, one Op.cpp per kernel .cu, registered as trtllm::glm_moe_dsa_. They cover the decode path of GLM-5.2: the post-attention hop and its routing, the RoPE KV append, the sparse MLA decode, the cross-rank indexer top-k, the dense MLP, and the draft model's routed MoE, live-row hop and tail. The kernels build as one OBJECT library for the SM100 family and are used only by the GLM-5.2 modeling_v2 targets. Signed-off-by: Xiao Wang <24860335+xwang233@users.noreply.github.com> --- cpp/tensorrt_llm/CMakeLists.txt | 1 + cpp/tensorrt_llm/kernels/CMakeLists.txt | 3 + .../kernels/glmMoeDsa/CMakeLists.txt | 42 + .../kernels/glmMoeDsa/crossRankTopk.cu | 1557 ++++++++++ .../kernels/glmMoeDsa/crossRankTopk.h | 162 + .../glmMoeDsa/crossRankTopkClusterMerge.cu | 2420 +++++++++++++++ .../glmMoeDsa/crossRankTopkClusterMerge.h | 191 ++ .../glmMoeDsa/crossRankTopkGlobalEmit.cu | 2757 +++++++++++++++++ .../glmMoeDsa/crossRankTopkGlobalEmit.h | 252 ++ .../kernels/glmMoeDsa/denseMlp.cu | 453 +++ cpp/tensorrt_llm/kernels/glmMoeDsa/denseMlp.h | 62 + .../kernels/glmMoeDsa/draftLiveRowHop.cu | 586 ++++ .../kernels/glmMoeDsa/draftLiveRowHop.h | 88 + .../kernels/glmMoeDsa/draftRoutedMoe.cu | 2628 ++++++++++++++++ .../kernels/glmMoeDsa/draftRoutedMoe.h | 205 ++ .../kernels/glmMoeDsa/draftTail.cu | 1568 ++++++++++ .../kernels/glmMoeDsa/draftTail.h | 144 + .../glmMoeDsa/indexerLayerRopeKvAppend.cu | 439 +++ .../glmMoeDsa/indexerLayerRopeKvAppend.h | 47 + .../kernels/glmMoeDsa/postAttentionHop.cu | 680 ++++ .../kernels/glmMoeDsa/postAttentionHop.h | 88 + .../glmMoeDsa/postAttentionHopFromPartials.cu | 707 +++++ .../glmMoeDsa/postAttentionHopFromPartials.h | 92 + .../glmMoeDsa/postAttentionHopRouting.cu | 889 ++++++ .../glmMoeDsa/postAttentionHopRouting.h | 116 + .../kernels/glmMoeDsa/ropeKvAppend.cu | 515 +++ .../kernels/glmMoeDsa/ropeKvAppend.h | 136 + .../kernels/glmMoeDsa/sparseMlaDecode.cu | 714 +++++ .../kernels/glmMoeDsa/sparseMlaDecode.h | 108 + cpp/tensorrt_llm/thop/CMakeLists.txt | 10 +- .../thop/glmMoeDsa/CMakeLists.txt | 19 + .../glmMoeDsa/crossRankTopkClusterMergeOp.cpp | 108 + .../glmMoeDsa/crossRankTopkGlobalEmitOp.cpp | 187 ++ .../thop/glmMoeDsa/crossRankTopkOp.cpp | 104 + .../thop/glmMoeDsa/denseMlpOp.cpp | 131 + .../thop/glmMoeDsa/draftLiveRowHopOp.cpp | 150 + .../thop/glmMoeDsa/draftRoutedMoeOp.cpp | 246 ++ .../thop/glmMoeDsa/draftTailOp.cpp | 168 + .../glmMoeDsa/indexerLayerRopeKvAppendOp.cpp | 242 ++ .../postAttentionHopFromPartialsOp.cpp | 150 + .../thop/glmMoeDsa/postAttentionHopOp.cpp | 170 + .../glmMoeDsa/postAttentionHopRoutingOp.cpp | 189 ++ .../thop/glmMoeDsa/ropeKvAppendOp.cpp | 330 ++ .../thop/glmMoeDsa/sparseMlaDecodeOp.cpp | 121 + 44 files changed, 19974 insertions(+), 1 deletion(-) create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/CMakeLists.txt create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/denseMlp.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/denseMlp.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/draftTail.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/draftTail.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.h create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.cu create mode 100644 cpp/tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.h create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/CMakeLists.txt create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkClusterMergeOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkGlobalEmitOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/denseMlpOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/draftLiveRowHopOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/draftRoutedMoeOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/draftTailOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/indexerLayerRopeKvAppendOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopFromPartialsOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopRoutingOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/ropeKvAppendOp.cpp create mode 100644 cpp/tensorrt_llm/thop/glmMoeDsa/sparseMlaDecodeOp.cpp diff --git a/cpp/tensorrt_llm/CMakeLists.txt b/cpp/tensorrt_llm/CMakeLists.txt index 25d1d5784c91..b7d4594f53fe 100644 --- a/cpp/tensorrt_llm/CMakeLists.txt +++ b/cpp/tensorrt_llm/CMakeLists.txt @@ -194,6 +194,7 @@ set(TRTLLM_LINK_LIBS mamba2_mtp_ssm_cache_src kda_decode_src kimi_k3_attn_res_src + glm_moe_dsa_kernels_src ws_layernorm_src fusedGatedRMSNormQuant_src fpA_intB_gemm_src diff --git a/cpp/tensorrt_llm/kernels/CMakeLists.txt b/cpp/tensorrt_llm/kernels/CMakeLists.txt index 0e7fc9390dc0..7b386929da95 100644 --- a/cpp/tensorrt_llm/kernels/CMakeLists.txt +++ b/cpp/tensorrt_llm/kernels/CMakeLists.txt @@ -32,6 +32,7 @@ add_subdirectory(fusedGatedRMSNormQuant) add_subdirectory(mamba2MTPSSMCache) add_subdirectory(kdaDecode) add_subdirectory(kimiK3AttnRes) +add_subdirectory(glmMoeDsa) add_subdirectory(mhcKernels) add_subdirectory(compressorKernels) add_subdirectory(fusedSampling) @@ -68,6 +69,8 @@ list(FILTER SRC_CPP EXCLUDE REGEX "kdaDecode/.*") list(FILTER SRC_CU EXCLUDE REGEX "kdaDecode/.*") list(FILTER SRC_CPP EXCLUDE REGEX "kimiK3AttnRes/.*") list(FILTER SRC_CU EXCLUDE REGEX "kimiK3AttnRes/.*") +list(FILTER SRC_CPP EXCLUDE REGEX "glmMoeDsa/.*") +list(FILTER SRC_CU EXCLUDE REGEX "glmMoeDsa/.*") list(FILTER SRC_CPP EXCLUDE REGEX "mhcKernels/.*") list(FILTER SRC_CU EXCLUDE REGEX "mhcKernels/.*") list(FILTER SRC_CPP EXCLUDE REGEX "compressorKernels/.*") diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/CMakeLists.txt b/cpp/tensorrt_llm/kernels/glmMoeDsa/CMakeLists.txt new file mode 100644 index 000000000000..db750686e8db --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/CMakeLists.txt @@ -0,0 +1,42 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +add_library( + glm_moe_dsa_kernels_src OBJECT + crossRankTopk.cu + crossRankTopkClusterMerge.cu + crossRankTopkGlobalEmit.cu + denseMlp.cu + draftLiveRowHop.cu + draftRoutedMoe.cu + draftTail.cu + indexerLayerRopeKvAppend.cu + postAttentionHop.cu + postAttentionHopFromPartials.cu + postAttentionHopRouting.cu + ropeKvAppend.cu + sparseMlaDecode.cu) + +# The GLM-MoE-DSA kernels are written for the SM100 (datacenter Blackwell) +# family (for example 32-byte global loads), so pin the target to that family +# instead of inheriting the global architecture list. On builds without any +# sm_100-family architecture the target still compiles: device code that needs +# newer instructions is guarded by __CUDA_ARCH__ and traps. +set_cuda_architectures(glm_moe_dsa_kernels_src 100f) + +set_property(TARGET glm_moe_dsa_kernels_src PROPERTY POSITION_INDEPENDENT_CODE + ON) +set_property(TARGET glm_moe_dsa_kernels_src PROPERTY CUDA_RESOLVE_DEVICE_SYMBOLS + ON) diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.cu new file mode 100644 index 000000000000..d859f89960b4 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.cu @@ -0,0 +1,1557 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.h" + +#include + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::cross_rank_topk +{ +namespace +{ + +constexpr int32_t kBins = 2048; +static_assert(kBins >= kTopK, "the output row is staged in the histogram"); +constexpr int32_t kWarps = kThreads / 32; +//! Per-warp key statistics kept in shared memory (see KeyStats). +constexpr int32_t kNumKeyStats = 5; +//! Tokens of each page one source holds. +constexpr int32_t kBlockTokens = kPageTokens / kSourcesPerRank; +//! Static shared memory of the kernel (round-2 segment tables), kept out of the dynamic budget. +constexpr size_t kStaticSmemBytes = 1024; + +// Scalar slots in shared memory. +enum ScalarSlot : int32_t +{ + kSlotBin = 0, + kSlotAbove, + kSlotBinCount, + kSlotTotal, + kSlotCounter2, + kSlotSrcCount, // [kMaxSources] + kSlotSrcBound = kSlotSrcCount + kMaxSources, // [kMaxSources] + kSlotSrcLen = kSlotSrcBound + kMaxSources, // [kMaxSources] + kSlotSrcOffset = kSlotSrcLen + kMaxSources, // [kMaxSources + 1] + kNumScalarSlots = kSlotSrcOffset + kMaxSources + 1 +}; + +__host__ __device__ inline int32_t numSources(int32_t world) +{ + return world * kSourcesPerRank; +} + +// Even share of kTopK per source. +__host__ __device__ inline int32_t evenShare(int32_t world) +{ + return (kTopK + numSources(world) - 1) / numSources(world); +} + +// Most keys a source sends in round 1 (an even number). +__host__ __device__ inline int32_t roundCap(int32_t world) +{ + return min(kSourceCapacity, 4 * evenShare(world)); +} + +// Keys one source can hold: one block of every local page. +__host__ __device__ inline int32_t chunkCapacity(int32_t localCapacity) +{ + int32_t const pages = (localCapacity + kPageTokens - 1) / kPageTokens; + return pages * kBlockTokens; +} + +__host__ __device__ inline int32_t mergeCapacity(int32_t world) +{ + return numSources(world) * roundCap(world); +} + +__host__ __device__ inline size_t smemBytes(int32_t localCapacity, int32_t world) +{ + size_t words = 2 * size_t{kBins} + 2 * size_t{kWarps} + size_t{kNumKeyStats} * kWarps + size_t{kNumScalarSlots}; + words = (words + 3) / 4 * 4; + words += static_cast(chunkCapacity(localCapacity)) + 2 * static_cast(mergeCapacity(world)); + return words * sizeof(uint32_t); +} + +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 900) +// The kernel targets SM90+ (multicast Lamport exchange). +__global__ void __launch_bounds__(kThreads, 1) cpTopkKernel(CpTopkParams p) +{ + __trap(); +} +#else + +//! Bits of one radix-select digit (kBins bins). +constexpr int32_t kDigitBits = 11; +//! Order-preserving key of +0.0: the keys of non-negative values are at least this, those of negative values below. +//! Real indexer logits are mostly negative, and the cut of a row (and of many sources) often lies among small +//! negative values while the largest keys are positive: one key range across zero spans ~2^31 key units, where a +//! radix digit has no resolution. The selects therefore work on one sign at a time: with fewer than `need` keys at +//! or above kPositiveKey these are all taken and the cut is searched among the negative keys only, else among the +//! non-negative keys only; each range is narrow enough for one or two passes. +constexpr uint32_t kPositiveKey = 0x80000000u; +//! Independent 16-byte candidate loads one merge thread issues before it waits on any of them. +constexpr int32_t kPollBatch = 4; +//! Polls of a still-empty Lamport word between two reads of the global timer. +constexpr int32_t kPollsPerTimerRead = 256; + +// Keys a saturated source aims to send in round 1 (kSourceBudget at 8 ranks). +__device__ __forceinline__ int32_t roundNeed(int32_t world) +{ + return min(kSourceCapacity, 3 * evenShare(world)); +} + +// Tokens of `rank` among the global tokens [0, n). +__host__ __device__ __forceinline__ int32_t localLength(int32_t n, int32_t rank, int32_t world) +{ + if (n <= 0) + { + return 0; + } + int32_t const fullPages = n / kPageTokens; + int32_t const rem = n % kPageTokens; + int32_t const ownedFull = (fullPages - rank + world - 1) / world; + int32_t len = ownedFull * kPageTokens; + if (rem > 0 && fullPages % world == rank) + { + len += rem; + } + return len; +} + +// Source `src` of a rank holds one block of kBlockTokens consecutive tokens of every local page: block +// (src + page) % kSourcesPerRank. A fully selected page is thus spread over all sources of its rank. +// Keys of source `src` among `localLen` local columns (only the last local page can be partial). +__host__ __device__ __forceinline__ int32_t sourceLength(int32_t localLen, int32_t src) +{ + int32_t const pages = (localLen + kPageTokens - 1) / kPageTokens; + if (pages == 0) + { + return 0; + } + int32_t const last = pages - 1; + int32_t const tail = localLen - last * kPageTokens - ((src + last) % kSourcesPerRank) * kBlockTokens; + return last * kBlockTokens + min(max(tail, 0), kBlockTokens); +} + +// Global token of a local column under the page-interleaved partition. +__device__ __forceinline__ uint32_t localToGlobal(int32_t col, int32_t rank, int32_t world) +{ + int32_t const page = col / kPageTokens; + return static_cast((page * world + rank) * kPageTokens + (col % kPageTokens)); +} + +struct SmemLayout +{ + uint32_t* hist; // [2 * kBins] radix histograms of alternate passes; the first one stages the output row + uint32_t* warpSums; // [2 * kWarps] + uint32_t* stats; // [kNumKeyStats * kWarps] per-warp key statistics + uint32_t* scal; // [kNumScalarSlots] + uint32_t* chunk; // [chunkCapacity] order-preserving keys of this source + uint32_t* mkey; // [mergeCapacity] staged round-1 pairs, then the merge candidates (merge CTA) + uint32_t* midx; // [mergeCapacity] +}; + +__device__ inline SmemLayout carveSmem(uint32_t* base, int32_t localCapacity, int32_t world) +{ + SmemLayout s; + uint32_t* p = base; + s.hist = p; + p += 2 * kBins; + s.warpSums = p; + p += 2 * kWarps; + s.stats = p; + p += kNumKeyStats * kWarps; + s.scal = p; + p += kNumScalarSlots; + p = base + ((p - base) + 3) / 4 * 4; + s.chunk = p; + p += chunkCapacity(localCapacity); + s.mkey = p; + p += mergeCapacity(world); + s.midx = p; + return s; +} + +__device__ __forceinline__ uint32_t orderedKey(float v) +{ + uint32_t const b = __float_as_uint(v); + uint32_t const k = (b & 0x80000000u) ? ~b : (b | 0x80000000u); + // NaN keys above +inf are clamped so that no key, and no exclusive bound key + 1, equals the empty marker. + return k < kEmptyWord - 1u ? k : kEmptyWord - 2u; +} + +// Bits of x (0 for 0): the digit range of keys in [base, base + x]. +__device__ __forceinline__ int32_t bitLength(uint32_t x) +{ + return x == 0u ? 0 : 32 - __clz(x); +} + +// Statistics of a set of keys: extremes overall and per sign, and the count of non-negative keys. +struct KeyStats +{ + uint32_t maxKey = 0u; // largest key (0 when none) + uint32_t minKey = kEmptyWord; // smallest key (kEmptyWord when none) + uint32_t positives = 0u; // keys >= kPositiveKey + uint32_t maxNeg = 0u; // largest key below kPositiveKey (0 when none) + uint32_t minPos = kEmptyWord; // smallest key >= kPositiveKey (kEmptyWord when none) + + __device__ __forceinline__ void add(uint32_t k) + { + bool const pos = k >= kPositiveKey; + maxKey = max(maxKey, k); + minKey = min(minKey, k); + positives += pos ? 1u : 0u; + maxNeg = pos ? maxNeg : max(maxNeg, k); + minPos = pos ? min(minPos, k) : minPos; + } +}; + +// Reduce a thread's statistics over its warp; lane 0 stores them in the per-warp array (read with readStats after +// the next block barrier). +__device__ __forceinline__ void storeWarpStats(KeyStats const& t, uint32_t* stats) +{ + uint32_t const v0 = __reduce_max_sync(0xFFFFFFFFu, t.maxKey); + uint32_t const v1 = __reduce_min_sync(0xFFFFFFFFu, t.minKey); + uint32_t const v2 = __reduce_add_sync(0xFFFFFFFFu, t.positives); + uint32_t const v3 = __reduce_max_sync(0xFFFFFFFFu, t.maxNeg); + uint32_t const v4 = __reduce_min_sync(0xFFFFFFFFu, t.minPos); + if (threadIdx.x % 32 == 0) + { + int32_t const w = threadIdx.x / 32; + stats[w] = v0; + stats[kWarps + w] = v1; + stats[2 * kWarps + w] = v2; + stats[3 * kWarps + w] = v3; + stats[4 * kWarps + w] = v4; + } +} + +// Block statistics from the per-warp array (every warp reduces it; no barrier). +__device__ __forceinline__ KeyStats readStats(uint32_t const* stats) +{ + int32_t const lane = threadIdx.x % 32; + KeyStats b; + b.maxKey = __reduce_max_sync(0xFFFFFFFFu, stats[lane]); + b.minKey = __reduce_min_sync(0xFFFFFFFFu, stats[kWarps + lane]); + b.positives = __reduce_add_sync(0xFFFFFFFFu, stats[2 * kWarps + lane]); + b.maxNeg = __reduce_max_sync(0xFFFFFFFFu, stats[3 * kWarps + lane]); + b.minPos = __reduce_min_sync(0xFFFFFFFFu, stats[4 * kWarps + lane]); + return b; +} + +// Digit setup of a select of the need-th largest key: over the non-negative keys when there are at least `need` of +// them, else over the negative keys with every non-negative key counted as taken (`aboveHi`). +struct SignRange +{ + uint32_t lo; // smallest key of the range (also the digit base) + uint64_t hi; // exclusive end of the histogrammed range + int32_t bits; // digit range of the range + uint32_t aboveHi; // keys at or above hi, all taken + bool negative; // the cut is among the negative keys +}; + +__device__ __forceinline__ SignRange signRange(KeyStats const& st, uint32_t need) +{ + if (st.positives >= need || st.positives == 0u) + { + uint32_t const lo = st.positives >= need ? st.minPos : st.minKey; + return SignRange{lo, uint64_t{kEmptyWord}, bitLength(st.maxKey - lo), 0u, st.positives == 0u}; + } + return SignRange{st.minKey, uint64_t{kPositiveKey}, bitLength(st.maxNeg - st.minKey), st.positives, true}; +} + +__device__ __forceinline__ int64_t round1Offset(int32_t buf, int32_t src, int32_t row) +{ + return int64_t{buf} * kBufferWords + (int64_t{src} * kMaxRows + row) * kRound1SlotWords; +} + +__device__ __forceinline__ int64_t round2Offset(int32_t buf, int32_t src, int32_t row) +{ + return int64_t{buf} * kBufferWords + int64_t{kMaxSources} * kMaxRows * kRound1SlotWords + + (int64_t{src} * kMaxRows + row) * kRound2SlotWords; +} + +//! Word offset of the candidates of a slot (after the 16-byte header). +constexpr int32_t kSlotHeaderWords = 4; + +__device__ __forceinline__ uint2 loadVolatile2(uint32_t const* ptr) +{ + uint2 v; + asm volatile("ld.volatile.global.v2.u32 {%0, %1}, [%2];" : "=r"(v.x), "=r"(v.y) : "l"(ptr) : "memory"); + return v; +} + +__device__ __forceinline__ uint4 loadVolatile4(uint32_t const* ptr) +{ + uint4 v; + asm volatile("ld.volatile.global.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) + : "l"(ptr) + : "memory"); + return v; +} + +__device__ __forceinline__ void store2(uint32_t* ptr, uint32_t a, uint32_t b) +{ + asm volatile("st.global.v2.u32 [%0], {%1, %2};" ::"l"(ptr), "r"(a), "r"(b) : "memory"); +} + +__device__ __forceinline__ void store4(uint32_t* ptr, uint4 v) +{ + asm volatile("st.global.v4.u32 [%0], {%1, %2, %3, %4};" ::"l"(ptr), "r"(v.x), "r"(v.y), "r"(v.z), "r"(v.w) + : "memory"); +} + +__device__ __forceinline__ bool isEmpty(uint2 v) +{ + return v.x == kEmptyWord || v.y == kEmptyWord; +} + +// Whether the first pair of a 16-byte candidate vector, and the second one when `second`, were written. +__device__ __forceinline__ bool vectorReady(uint4 v, bool second) +{ + return v.x != kEmptyWord && v.y != kEmptyWord && (!second || (v.z != kEmptyWord && v.w != kEmptyWord)); +} + +__device__ __forceinline__ uint64_t globalTimerNs() +{ + uint64_t t; + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); + return t; +} + +// What a polling thread needs to give up: the error word, the timeout and this rank (for the report). +struct WaitCtx +{ + int32_t* state; + uint64_t timeoutNs; + int32_t rank; +}; + +// A peer did not deliver within the timeout. The selection cannot be completed correctly any more, and the peer's +// late writes could land in a buffer this rank resets or reuses, so record which wait failed and abort the kernel: +// the resulting CUDA error fails the run instead of letting any rank continue with a partial selection. +__device__ __noinline__ void abortOnTimeout(WaitCtx const& w, int32_t what) +{ + bool const first = atomicExch(&w.state[3], what) == 0; + __threadfence_system(); + if (first) + { + printf( + "[glm_moe_dsa] crossRankTopk: rank %d, row %d: wait %d for a peer exceeded %llu ms; aborting the kernel\n", + w.rank, static_cast(blockIdx.y), what, static_cast(w.timeoutNs / 1000000ull)); + } + __trap(); +} + +// Spin until both words of a Lamport pair were written; aborts the kernel after the timeout. +__device__ uint2 pollPair(uint32_t const* ptr, WaitCtx const& w, int32_t what) +{ + uint2 v = loadVolatile2(ptr); + uint64_t start = 0; + for (int32_t polls = 1; isEmpty(v); ++polls) + { + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, what); + } + } + v = loadVolatile2(ptr); + } + return v; +} + +// Spin until a 16-byte candidate vector was written (see vectorReady); aborts the kernel after the timeout. +__device__ uint4 pollVector(uint32_t const* ptr, bool second, WaitCtx const& w, int32_t what) +{ + uint4 v = loadVolatile4(ptr); + uint64_t start = 0; + for (int32_t polls = 1; !vectorReady(v, second); ++polls) + { + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, what); + } + } + v = loadVolatile4(ptr); + } + return v; +} + +// Block-wide inclusive scan of one value per thread. Returns the inclusive prefix; `total` gets the block sum. +__device__ uint32_t blockInclusiveScan(uint32_t v, uint32_t* warpSums, uint32_t& total) +{ + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + uint32_t x = v; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + if (lane >= o) + { + x += y; + } + } + if (lane == 31) + { + warpSums[warp] = x; + } + __syncthreads(); + if (warp == 0) + { + uint32_t w = warpSums[lane]; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, w, o); + if (lane >= o) + { + w += y; + } + } + warpSums[kWarps + lane] = w; + } + __syncthreads(); + total = warpSums[2 * kWarps - 1]; + uint32_t const before = warp > 0 ? warpSums[kWarps + warp - 1] : 0u; + return x + before; +} + +// Exclusive block-wide scan with a single barrier: every warp scans the per-warp sums itself. Returns this +// thread's exclusive prefix; `total` gets the block sum. warpSums must not be rewritten before the next barrier. +__device__ uint32_t blockExclusiveScan(uint32_t v, uint32_t* warpSums, uint32_t& total) +{ + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + uint32_t x = v; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + if (lane >= o) + { + x += y; + } + } + if (lane == 31) + { + warpSums[warp] = x; + } + __syncthreads(); + uint32_t const w = warpSums[lane]; + uint32_t y = w; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const z = __shfl_up_sync(0xFFFFFFFFu, y, o); + if (lane >= o) + { + y += z; + } + } + total = __shfl_sync(0xFFFFFFFFu, y, 31); + return __shfl_sync(0xFFFFFFFFu, y - w, warp) + x - v; +} + +// Find, counting from the top bin, the bin of `hist` that holds the target-th largest key, with two barriers: +// per-warp sums, then each warp scans them and only the warp holding the target scans its own bins. Returns the +// total count (every thread); when target <= total, the bin, the count above it and its own count go to the +// scalar slots. +__device__ uint32_t findBin(SmemLayout const& s, uint32_t const* hist, int32_t nbins, uint32_t target) +{ + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + int32_t const perThread = (nbins + kThreads - 1) / kThreads; + int32_t const top = nbins - 1 - perThread * static_cast(threadIdx.x); + uint32_t mine = 0; + for (int32_t i = 0; i < perThread && top - i >= 0; ++i) + { + mine += hist[top - i]; + } + uint32_t const warpTotal = __reduce_add_sync(0xFFFFFFFFu, mine); + if (lane == 0) + { + s.warpSums[warp] = warpTotal; + } + __syncthreads(); + uint32_t const w = s.warpSums[lane]; + uint32_t x = w; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + if (lane >= o) + { + x += y; + } + } + uint32_t const total = __shfl_sync(0xFFFFFFFFu, x, 31); + uint32_t const warpBefore = __shfl_sync(0xFFFFFFFFu, x - w, warp); + if (warpBefore < target && target <= warpBefore + warpTotal) + { + uint32_t t = mine; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, t, o); + if (lane >= o) + { + t += y; + } + } + uint32_t acc = warpBefore + t - mine; + if (acc < target && target <= acc + mine) + { + for (int32_t i = 0; i < perThread && top - i >= 0; ++i) + { + uint32_t const c = hist[top - i]; + if (acc + c >= target) + { + s.scal[kSlotBin] = static_cast(top - i); + s.scal[kSlotAbove] = acc; + s.scal[kSlotBinCount] = c; + break; + } + acc += c; + } + } + } + __syncthreads(); + return total; +} + +struct SelectOut +{ + uint32_t edge; // lower edge of the selected bin (the exact key after the last digit) + uint32_t above; // predicate keys strictly above the selected bin + uint32_t binCount; // predicate keys in the selected bin + uint32_t total; // predicate keys + bool all; // take every predicate key (fewer than needed, or all fit the capacity) + bool full; // exact mode: the keys at or above `edge` are exactly the `need` largest + int32_t passes; // radix passes run +}; + +// Candidate keys and token indices held in shared memory (16-byte aligned, readable in groups of four up to the +// next multiple of four). +struct SmemKeys +{ + static constexpr bool kVector = true; + uint32_t const* keys; + uint32_t const* idx; + + __device__ uint4 keys4(int32_t j) const + { + return *reinterpret_cast(keys + j); + } + + __device__ uint4 index4(int32_t j) const + { + return *reinterpret_cast(idx + j); + } +}; + +// Merge candidates in shared memory followed by round-2 candidates read from their global Lamport slots. +struct MergeKeys +{ + static constexpr bool kVector = false; + uint32_t const* smemKeys; + uint32_t const* smemIdx; + int32_t smemCount; + uint32_t const* const* slots; // round-2 slot of each segment + uint32_t const* segBegin; // [numSegments + 1] candidate offsets + int32_t numSegments; + + __device__ uint32_t const* pair(int32_t k) const + { + // Last segment whose begin is <= k. + int32_t lo = 0; + int32_t hi = numSegments - 1; + while (lo < hi) + { + int32_t const mid = (lo + hi + 1) / 2; + if (static_cast(segBegin[mid]) <= k) + { + lo = mid; + } + else + { + hi = mid - 1; + } + } + return slots[lo] + kSlotHeaderWords + 2 * (k - static_cast(segBegin[lo])); + } + + __device__ uint32_t operator()(int32_t j) const + { + return j < smemCount ? smemKeys[j] : pair(j - smemCount)[0]; + } + + __device__ uint32_t index(int32_t j) const + { + return j < smemCount ? smemIdx[j] : pair(j - smemCount)[1]; + } +}; + +// Debug slots of this CTA starting at `offset` (nullptr without a debug buffer). +__device__ __forceinline__ int64_t* debugSlots(CpTopkParams const& p, int32_t offset) +{ + if (p.debugTimes == nullptr) + { + return nullptr; + } + return p.debugTimes + (static_cast(blockIdx.y) * kSourcesPerRank + blockIdx.x) * kDebugSlots + offset; +} + +// clock64 of thread 0 into a debug slot (no-op without a debug buffer). +__device__ __forceinline__ void stamp(int64_t* dbg, int32_t slot) +{ + if (dbg != nullptr && threadIdx.x == 0) + { + dbg[slot] = clock64(); + } +} + +// A value into a debug slot (thread 0; no-op without a debug buffer). +__device__ __forceinline__ void putDebug(int64_t* dbg, int32_t slot, int64_t value) +{ + if (dbg != nullptr && threadIdx.x == 0) + { + dbg[slot] = value; + } +} + +// Phase timestamp for diagnosis (thread 0 of the CTA; no-op without a debug buffer). +__device__ __forceinline__ void markPhase(CpTopkParams const& p, int32_t phase) +{ + if (p.debugTimes != nullptr && threadIdx.x == 0) + { + uint64_t t; + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); + int64_t* d = p.debugTimes + (static_cast(blockIdx.y) * kSourcesPerRank + blockIdx.x) * kDebugSlots; + d[phase] = static_cast(t); + d[16 + phase] = static_cast(clock64()); + } +} + +// Histogram update of one key of a radix pass. +__device__ __forceinline__ void histogramKey(uint32_t* hist, bool inRange, uint32_t k, uint32_t lo, uint64_t hi, + uint32_t base, uint32_t prefMask, uint32_t prefVal, int32_t shift, uint32_t binMask) +{ + uint32_t const d = k - base; + if (inRange && k >= lo && static_cast(k) < hi && (d & prefMask) == prefVal) + { + atomicAdd(&hist[(d >> shift) & binMask], 1u); + } +} + +// Block radix select of the need-th largest of the n keys at or above lo, of which `aboveHi` (< need) lie at or +// above hi and are counted as taken without being histogrammed. Every key in [lo, hi) is at least `base`, and +// key - base < 2^topBits; digits of up to kDigitBits bits of key - base are refined from the top. With cap > 0 the +// select stops at the first bin edge whose keys at or above it number at most `cap`. With cap = 0 it runs to the +// exact key, or stops early when the selected bin is wholly taken (`full`). Alternate passes use alternate +// histograms, so the next one is cleared while the current one is searched. Counts in the result include aboveHi. +template +__device__ SelectOut blockSelect(SmemLayout const& s, KeyAt const& keyAt, int32_t n, uint32_t lo, uint64_t hi, + uint32_t need, uint32_t cap, uint32_t base, int32_t topBits, int64_t* dbg = nullptr, uint32_t aboveHi = 0u) +{ + if (dbg != nullptr && threadIdx.x == 0) + { + dbg[0] = n; + } + SelectOut out{lo, 0u, 0u, 0u, false, false, 0}; + uint32_t prefMask = 0; + uint32_t prefVal = 0; + uint32_t above = aboveHi; + int32_t remaining = max(topBits, 1); + uint32_t* hist = s.hist; + uint32_t* histNext = s.hist + kBins; + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, remaining)); b += kThreads) + { + hist[b] = 0; + } + __syncthreads(); +#pragma unroll 1 + for (int32_t pass = 0; remaining > 0; ++pass) + { + int32_t const width = min(kDigitBits, remaining); + int32_t const shift = remaining - width; + int32_t const nbins = 1 << width; + uint32_t const binMask = static_cast(nbins - 1); + out.passes = pass + 1; + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[1 + 3 * pass] = clock64(); + } + if constexpr (KeyAt::kVector) + { + for (int32_t j0 = 4 * static_cast(threadIdx.x); j0 < n; j0 += 4 * kThreads) + { + uint4 const q = keyAt.keys4(j0); + histogramKey(hist, true, q.x, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, j0 + 1 < n, q.y, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, j0 + 2 < n, q.z, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, j0 + 3 < n, q.w, lo, hi, base, prefMask, prefVal, shift, binMask); + } + } + else + { + for (int32_t j = threadIdx.x; j < n; j += kThreads) + { + histogramKey(hist, true, keyAt(j), lo, hi, base, prefMask, prefVal, shift, binMask); + } + } + __syncthreads(); + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[2 + 3 * pass] = clock64(); + } + if (shift > 0) + { + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, shift)); b += kThreads) + { + histNext[b] = 0; + } + } + // Find the bin holding the (need - above)-th largest key, counting from the top bin. + uint32_t const target = need - above; + uint32_t const total = findBin(s, hist, nbins, target); + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[3 + 3 * pass] = clock64(); + } + if (pass == 0) + { + out.total = total + aboveHi; + if (total + aboveHi <= cap || target > total) + { + out.all = true; + out.above = total + aboveHi; + return out; + } + } + // The scalars are next written after two more barriers (the next histogram and scan). + uint32_t const bin = s.scal[kSlotBin]; + above += s.scal[kSlotAbove]; + out.binCount = s.scal[kSlotBinCount]; + prefVal |= bin << shift; + prefMask |= binMask << shift; + out.edge = base + prefVal; + out.above = above; + remaining = shift; + uint32_t* const t = hist; + hist = histNext; + histNext = t; + if (cap > 0 && above + out.binCount <= cap) + { + return out; + } + if (cap == 0 && remaining > 0 && above + out.binCount == need) + { + out.full = true; + return out; + } + } + return out; +} + +// Send one source's selected keys (keys in [lo, hi) of the chunk) to its slot; returns the count sent. +// Takes every predicate key at or above the edge when they fit `cap`. Otherwise (more than `cap` keys tie at the +// exact edge) it takes every key above the edge, plus, with kPartialTies, the first keys equal to it in source +// order up to `cap` (any subset of equal keys is a valid tie-aware choice). Round 1 sends no partial ties, so the +// keys a second round requests from a source (at most the edge) were never sent before. +// The header carries the count and an exclusive upper bound of the keys left unsent (0 when none). +// With `stage` (round 1, at most kSourceCapacity keys), the pairs are gathered in shared memory first and written +// with coalesced 16-byte stores; otherwise each pair is stored where it is found. +template +__device__ uint32_t sendSource(SmemLayout const& s, SelectOut const& sel, int32_t n, uint32_t lo, uint64_t hi, + uint32_t cap, uint32_t* mcSlot, int32_t src, int32_t rank, int32_t world, uint2* stage, int64_t* dbg = nullptr) +{ + bool const strict = !sel.all && sel.above + sel.binCount > cap; + uint32_t const threshold = sel.all ? lo : sel.edge; + // Keys equal to the edge that fit after the keys above it (sel.above < need <= cap when strict). + uint32_t const tieBudget = kPartialTies && strict ? cap - sel.above : 0u; + uint32_t const sent = sel.all ? sel.total : (strict ? sel.above + tieBudget : sel.above + sel.binCount); + uint32_t const bound = sel.all ? lo : (strict ? sel.edge + 1u : sel.edge); + stamp(dbg, 42); + // One page block (kBlockTokens keys) per thread; positions from a block scan, so the slot holds the keys in + // source order: first those taken outright, then (strict only) the admitted keys equal to the edge. + int32_t const groups = (n + kBlockTokens - 1) / kBlockTokens; + uint32_t takenMain = 0; + uint32_t takenTies = 0; + uint32_t const tieBase = strict ? sel.above : 0u; + for (int32_t g0 = 0; g0 < groups; g0 += kThreads) + { + int32_t const g = g0 + static_cast(threadIdx.x); + uint32_t k[kBlockTokens]; + uint32_t mask = 0; + uint32_t tieMask = 0; + if (g < groups) + { + uint4 const a = *reinterpret_cast(s.chunk + kBlockTokens * g); + uint4 const b = *reinterpret_cast(s.chunk + kBlockTokens * g + 4); + k[0] = a.x; + k[1] = a.y; + k[2] = a.z; + k[3] = a.w; + k[4] = b.x; + k[5] = b.y; + k[6] = b.z; + k[7] = b.w; +#pragma unroll + for (int32_t i = 0; i < kBlockTokens; ++i) + { + bool const in = kBlockTokens * g + i < n && k[i] >= lo && static_cast(k[i]) < hi; + bool const take = in && (strict ? k[i] > threshold : k[i] >= threshold); + bool const tie = kPartialTies && in && strict && k[i] == threshold; + mask |= (take ? 1u : 0u) << i; + tieMask |= (tie ? 1u : 0u) << i; + } + } + // At most kThreads * kBlockTokens keys per pass: both counts fit 16 bits. + uint32_t const c = static_cast(__popc(mask)) | (static_cast(__popc(tieMask)) << 16); + uint32_t chunkTaken; + uint32_t const before = blockExclusiveScan(c, s.warpSums, chunkTaken); + uint32_t pos = takenMain + (before & 0xFFFFu); + uint32_t tiePos = takenTies + (before >> 16); + takenMain += chunkTaken & 0xFFFFu; + takenTies += chunkTaken >> 16; + if ((mask | tieMask) != 0u) + { + int32_t const colBase = g * kPageTokens + ((src + g) % kSourcesPerRank) * kBlockTokens; +#pragma unroll + for (int32_t i = 0; i < kBlockTokens; ++i) + { + uint32_t slot = kEmptyWord; + if ((mask >> i) & 1u) + { + slot = pos++; + } + else if constexpr (kPartialTies) + { + if ((tieMask >> i) & 1u) + { + slot = tiePos < tieBudget ? tieBase + tiePos : kEmptyWord; + ++tiePos; + } + } + if (slot != kEmptyWord) + { + uint32_t const token = localToGlobal(colBase + i, rank, world); + if (stage != nullptr) + { + stage[slot] = make_uint2(k[i], token); + } + else + { + store2(mcSlot + kSlotHeaderWords + 2 * static_cast(slot), k[i], token); + } + } + } + } + // The staged pairs are complete, and the scan's warp sums may be rewritten. + __syncthreads(); + } + stamp(dbg, 43); + if (stage != nullptr) + { + // Two pairs per 16-byte store; an odd count leaves the last half of the last vector empty. + int32_t const vectors = static_cast(sent + 1u) / 2; + for (int32_t v = threadIdx.x; v < vectors; v += kThreads) + { + uint2 const a = stage[2 * v]; + uint2 const b + = 2 * v + 1 < static_cast(sent) ? stage[2 * v + 1] : make_uint2(kEmptyWord, kEmptyWord); + store4(mcSlot + kSlotHeaderWords + 4 * static_cast(v), make_uint4(a.x, a.y, b.x, b.y)); + } + } + // Receivers check every candidate word, so the header needs no ordering against the candidates. + if (threadIdx.x == kThreads - 1) + { + store2(mcSlot, sent, bound); + } + stamp(dbg, 44); + return sent; +} + +// Load one source's keys (one block of kBlockTokens tokens of every local page) from the local logits into the +// chunk; returns the number of keys. Consecutive threads read consecutive keys (whole 32-byte blocks per eight +// threads), and each thread issues kLoadBatch loads before it uses any. With `withStats`, the per-warp key +// statistics are stored for readStats after the next block barrier. +__device__ int32_t loadSource( + SmemLayout const& s, float const* rowLogits, int32_t localLen, int32_t src, bool withStats) +{ + constexpr int32_t kLoadBatch = 8; + int32_t const n = sourceLength(localLen, src); + KeyStats st; + for (int32_t j0 = threadIdx.x; j0 < n; j0 += kLoadBatch * kThreads) + { + float v[kLoadBatch]; +#pragma unroll + for (int32_t i = 0; i < kLoadBatch; ++i) + { + int32_t const j = j0 + i * kThreads; + int32_t const page = j / kBlockTokens; + int32_t const col = page * kPageTokens + ((src + page) % kSourcesPerRank) * kBlockTokens + j % kBlockTokens; + v[i] = j < n ? rowLogits[col] : 0.F; + } +#pragma unroll + for (int32_t i = 0; i < kLoadBatch; ++i) + { + int32_t const j = j0 + i * kThreads; + if (j < n) + { + uint32_t const k = orderedKey(v[i]); + s.chunk[j] = k; + st.add(k); + } + } + } + if (withStats) + { + storeWarpStats(st, s.stats); + } + return n; +} + +// Whether source q withheld keys that may lie above the merged k-th key. +__device__ __forceinline__ bool sourceOverflows(SmemLayout const& s, int32_t q, uint32_t kth) +{ + uint32_t const cnt = s.scal[kSlotSrcCount + q]; + uint64_t const bound = s.scal[kSlotSrcBound + q]; + return cnt < s.scal[kSlotSrcLen + q] && bound > uint64_t{kth} + 1u; +} + +// Emit the top-kTopK of the merge candidates. Keys above `kth` are all taken; ties at `kth` are taken in candidate +// order when only some of them fit (deterministic on every rank), otherwise all of them. With `takeAll`, every +// candidate is taken and the row is padded with -1. The row is assembled in shared memory (the first histogram) +// and written with coalesced stores. +template +__device__ bool emitSelection(SmemLayout const& s, KeyAt const& keys, int32_t total, uint32_t kth, bool takeAll, + int32_t* outRow, int32_t overflowSources, int64_t* dbg = nullptr) +{ + uint32_t const want = static_cast(kTopK); + // Positions from one block scan over contiguous candidate ranges (no contended counter); the order is the + // candidate order, identical on every rank. Shared-memory candidates are read four at a time. + int32_t per = (total + kThreads - 1) / kThreads; + if constexpr (KeyAt::kVector) + { + per = (per + 3) / 4 * 4; + } + int32_t const j0 = min(total, static_cast(threadIdx.x) * per); + int32_t const j1 = min(total, j0 + per); + uint32_t cGt = 0; + uint32_t cEq = 0; + if constexpr (KeyAt::kVector) + { + for (int32_t j = j0; j < j1; j += 4) + { + uint4 const q = keys.keys4(j); + uint32_t const kk[4] = {q.x, q.y, q.z, q.w}; +#pragma unroll + for (int32_t i = 0; i < 4; ++i) + { + bool const in = j + i < j1; + cGt += (in && (takeAll || kk[i] > kth)) ? 1u : 0u; + cEq += (in && !takeAll && kk[i] == kth) ? 1u : 0u; + } + } + } + else + { + for (int32_t j = j0; j < j1; ++j) + { + uint32_t const k = keys(j); + cGt += (takeAll || k > kth) ? 1u : 0u; + cEq += (!takeAll && k == kth) ? 1u : 0u; + } + } + // The first `overflowSources` threads flag a source that withheld keys above the threshold (the flag slot is + // zero on entry); with a flag set, nothing is written and the caller runs the second round. + if (static_cast(threadIdx.x) < overflowSources && sourceOverflows(s, threadIdx.x, kth)) + { + atomicOr(&s.scal[kSlotCounter2], 1u); + } + uint32_t allGt; + uint32_t allEq; + uint32_t posGt; + uint32_t posEq; + if (total < 0x10000) + { + // Both counts fit 16 bits: one scan of the packed pair. + uint32_t packedTotal; + uint32_t const packed = cGt | (cEq << 16); + uint32_t const before = blockExclusiveScan(packed, s.warpSums, packedTotal); + allGt = packedTotal & 0xFFFFu; + allEq = packedTotal >> 16; + posGt = before & 0xFFFFu; + posEq = allGt + (before >> 16); + } + else + { + posGt = blockInclusiveScan(cGt, s.warpSums, allGt) - cGt; + __syncthreads(); + posEq = blockInclusiveScan(cEq, s.warpSums, allEq) - cEq; + posEq += allGt; + } + stamp(dbg, 59); + if (overflowSources > 0 && s.scal[kSlotCounter2] != 0u) + { + return true; + } + uint32_t* row = s.hist; + auto place = [&](uint32_t k, uint32_t idx) + { + if (takeAll || k > kth) + { + if (posGt < want) + { + row[posGt] = idx; + } + ++posGt; + } + else if (k == kth) + { + if (posEq < want) + { + row[posEq] = idx; + } + ++posEq; + } + }; + if constexpr (KeyAt::kVector) + { + for (int32_t j = j0; j < j1; j += 4) + { + uint4 const q = keys.keys4(j); + uint4 const x = keys.index4(j); + uint32_t const kk[4] = {q.x, q.y, q.z, q.w}; + uint32_t const xx[4] = {x.x, x.y, x.z, x.w}; +#pragma unroll + for (int32_t i = 0; i < 4; ++i) + { + if (j + i < j1) + { + place(kk[i], xx[i]); + } + } + } + } + else + { + for (int32_t j = j0; j < j1; ++j) + { + place(keys(j), keys.index(j)); + } + } + __syncthreads(); + stamp(dbg, 60); + uint32_t const selected = min(allGt + allEq, want); + for (uint32_t j = threadIdx.x; j < want; j += kThreads) + { + outRow[j] = j < selected ? static_cast(row[j]) : -1; + } + stamp(dbg, 61); + return false; +} + +// Threshold of an exact (cap = 0) merge select: keys above `kth` are taken, ties at `kth` while they fit. +__device__ __forceinline__ uint32_t mergeThreshold(SelectOut const& m, bool& takeAll) +{ + takeAll = m.all || (m.full && m.edge == 0u); + if (takeAll) + { + return 0u; + } + // A wholly taken bin: the keys at or above its edge are exactly kTopK. + return m.full ? m.edge - 1u : m.edge; +} + +// Reset the slots of Lamport buffer `buf` on this rank's copy, using one warp per slot. A slot was written iff its +// header was; the header's count bounds the candidates to reset. +__device__ void clearBuffer(CpTopkParams const& p, int32_t buf, int32_t clearer, int32_t numClearers) +{ + int32_t const warp = threadIdx.x / 32; + int32_t const lane = threadIdx.x % 32; + int32_t const round1Slots = kMaxSources * kMaxRows; + uint4 const empty = make_uint4(kEmptyWord, kEmptyWord, kEmptyWord, kEmptyWord); + for (int32_t slot = clearer * kWarps + warp; slot < 2 * round1Slots; slot += numClearers * kWarps) + { + bool const second = slot >= round1Slots; + int32_t const qr = second ? slot - round1Slots : slot; + uint32_t* base = p.ucWorkspace + + (second ? round2Offset(buf, qr / kMaxRows, qr % kMaxRows) + : round1Offset(buf, qr / kMaxRows, qr % kMaxRows)); + uint2 h = make_uint2(kEmptyWord, kEmptyWord); + if (lane == 0) + { + h = loadVolatile2(base); + } + h.x = __shfl_sync(0xFFFFFFFFu, h.x, 0); + h.y = __shfl_sync(0xFFFFFFFFu, h.y, 0); + if (h.x == kEmptyWord && h.y == kEmptyWord) + { + continue; + } + uint32_t const capacity = second ? kRound2Capacity : kSourceCapacity; + uint32_t const count = h.x == kEmptyWord ? capacity : min(h.x, capacity); + int32_t const vectors = static_cast(count + 1u) / 2; + for (int32_t v = lane; v < vectors; v += 32) + { + store4(base + kSlotHeaderWords + 4 * static_cast(v), empty); + } + if (lane == 0) + { + store2(base, kEmptyWord, kEmptyWord); + } + } +} + +// Merge of one row on this rank (one CTA): gathers every source's round-1 candidates, selects the exact +// top-kTopK, runs the second round when a source withheld keys above the threshold, and writes the row. +__device__ void mergeRow(CpTopkParams const& p, SmemLayout const& s, uint32_t const** segSlots, uint32_t* segBegin, + int32_t buf, int32_t row, int32_t rowLen, int32_t localLen, float const* rowLogits, int32_t* outRow, + WaitCtx const& wait) +{ + int32_t const world = p.worldSize; + int32_t const rank = p.rank; + int32_t const sources = numSources(world); + uint32_t const cap = static_cast(roundCap(world)); + __syncthreads(); + if (threadIdx.x == 0) + { + s.scal[kSlotCounter2] = 0u; + } + if (static_cast(threadIdx.x) < sources) + { + // Keys each source holds, computed while its header is in flight. + int32_t const q = threadIdx.x; + s.scal[kSlotSrcLen + q] = static_cast( + sourceLength(min(localLength(rowLen, q / kSourcesPerRank, world), p.localCapacity), q % kSourcesPerRank)); + uint2 const h = pollPair(p.ucWorkspace + round1Offset(buf, threadIdx.x, row), wait, kWaitRound1Header); + s.scal[kSlotSrcCount + threadIdx.x] = min(h.x, cap); + s.scal[kSlotSrcBound + threadIdx.x] = h.y; + } + __syncthreads(); + if (threadIdx.x < 32) + { + // Exclusive prefix of the source counts (at most 64 sources: two per lane). + uint32_t const a = static_cast(threadIdx.x) < sources ? s.scal[kSlotSrcCount + threadIdx.x] : 0u; + uint32_t const b + = static_cast(threadIdx.x) + 32 < sources ? s.scal[kSlotSrcCount + threadIdx.x + 32] : 0u; + uint32_t x = a; + uint32_t z = b; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + uint32_t const w = __shfl_up_sync(0xFFFFFFFFu, z, o); + if (static_cast(threadIdx.x) >= o) + { + x += y; + z += w; + } + } + uint32_t const totalA = __shfl_sync(0xFFFFFFFFu, x, 31); + s.scal[kSlotSrcOffset + threadIdx.x] = x - a; + s.scal[kSlotSrcOffset + 32 + threadIdx.x] = totalA + z - b; + if (threadIdx.x == 31) + { + s.scal[kSlotSrcOffset + kMaxSources] = totalA + z; + } + } + __syncthreads(); + int32_t const total = static_cast(s.scal[kSlotSrcOffset + kMaxSources]); + markPhase(p, 4); + + // Candidates: padded (source, vector) positions; issue a batch of independent 16-byte loads, then re-poll the + // ones not yet written. The key statistics are tracked for the merge select. + int32_t const vectorsPerSource = static_cast(cap) / 2; + int32_t const padded = sources * vectorsPerSource; + KeyStats st; +#pragma unroll 1 + for (int32_t v0 = 0; v0 < padded; v0 += kThreads * kPollBatch) + { + uint4 v[kPollBatch]; +#pragma unroll + for (int32_t i = 0; i < kPollBatch; ++i) + { + int32_t const jv = v0 + i * kThreads + static_cast(threadIdx.x); + int32_t const q = jv / vectorsPerSource; + int32_t const kv = jv % vectorsPerSource; + v[i] = make_uint4(0u, 0u, 0u, 0u); + if (jv < padded && 2 * kv < static_cast(s.scal[kSlotSrcCount + q])) + { + v[i] = loadVolatile4(p.ucWorkspace + round1Offset(buf, q, row) + kSlotHeaderWords + 4 * kv); + } + } +#pragma unroll + for (int32_t i = 0; i < kPollBatch; ++i) + { + int32_t const jv = v0 + i * kThreads + static_cast(threadIdx.x); + int32_t const q = jv / vectorsPerSource; + int32_t const kv = jv % vectorsPerSource; + if (jv < padded && 2 * kv < static_cast(s.scal[kSlotSrcCount + q])) + { + bool const second = 2 * kv + 1 < static_cast(s.scal[kSlotSrcCount + q]); + if (!vectorReady(v[i], second)) + { + v[i] = pollVector(p.ucWorkspace + round1Offset(buf, q, row) + kSlotHeaderWords + 4 * kv, second, + wait, kWaitRound1Candidates); + } + int32_t const j = static_cast(s.scal[kSlotSrcOffset + q]) + 2 * kv; + s.mkey[j] = v[i].x; + s.midx[j] = v[i].y; + st.add(v[i].x); + if (second) + { + s.mkey[j + 1] = v[i].z; + s.midx[j + 1] = v[i].w; + st.add(v[i].z); + } + } + } + } + stamp(debugSlots(p, 0), 45); + storeWarpStats(st, s.stats); + __syncthreads(); + markPhase(p, 5); + + // Digits of the merge select span the candidates of the sign the cut lies in (see kPositiveKey). + SignRange const r = total > 0 ? signRange(readStats(s.stats), static_cast(kTopK)) + : SignRange{0u, uint64_t{kEmptyWord}, 32, 0u, false}; + SmemKeys const merged{s.mkey, s.midx}; + SelectOut const m = blockSelect( + s, merged, total, r.lo, r.hi, static_cast(kTopK), 0u, r.lo, r.bits, debugSlots(p, 48), r.aboveHi); + putDebug(debugSlots(p, 0), 62, total); + putDebug(debugSlots(p, 0), 63, m.passes + (r.negative ? 10 : 0)); + bool takeAll = false; + uint32_t const kth = mergeThreshold(m, takeAll); + markPhase(p, 6); + stamp(debugSlots(p, 0), 58); + if (!emitSelection(s, merged, total, kth, takeAll, outRow, sources, debugSlots(p, 0))) + { + return; + } + + // ---- Round 2 (rare; every rank takes it): withheld keys in [kth, bound) of overflowing sources ---------- + __syncthreads(); + // 1. This rank's overflowing sources send their top-kRound2Capacity withheld keys. + for (int32_t c = 0; c < kSourcesPerRank; ++c) + { + int32_t const q = rank * kSourcesPerRank + c; + if (!sourceOverflows(s, q, kth)) + { + continue; + } + uint64_t const b = s.scal[kSlotSrcBound + q]; + int32_t const nq = loadSource(s, rowLogits, localLen, c, false); + __syncthreads(); + SelectOut const sel2 = blockSelect( + s, SmemKeys{s.chunk, nullptr}, nq, kth, b, static_cast(kRound2Capacity), kRound2Capacity, 0u, 32); + (void) sendSource( + s, sel2, nq, kth, b, kRound2Capacity, p.mcWorkspace + round2Offset(buf, q, row), c, rank, world, nullptr); + __syncthreads(); + } + // 2. Gather the round-2 segments of every overflowing source (all ranks). + if (threadIdx.x == 0) + { + int32_t segs = 0; + uint32_t off = 0; + for (int32_t q = 0; q < sources; ++q) + { + if (!sourceOverflows(s, q, kth)) + { + continue; + } + uint32_t const* slot = p.ucWorkspace + round2Offset(buf, q, row); + uint2 const h = pollPair(slot, wait, kWaitRound2Header); + segSlots[segs] = slot; + segBegin[segs] = off; + off += min(h.x, static_cast(kRound2Capacity)); + ++segs; + } + segBegin[segs] = off; + s.scal[kSlotTotal] = static_cast(segs); + } + __syncthreads(); + int32_t const segs = static_cast(s.scal[kSlotTotal]); + int32_t const total2 = static_cast(segBegin[segs]); + MergeKeys const keys{s.mkey, s.midx, total, segSlots, segBegin, segs}; + for (int32_t k = threadIdx.x; k < total2; k += kThreads) + { + (void) pollPair(keys.pair(k), wait, kWaitRound2Candidates); + } + __syncthreads(); + SelectOut const m2 + = blockSelect(s, keys, total + total2, 0u, uint64_t{kEmptyWord}, static_cast(kTopK), 0u, 0u, 32); + bool takeAll2 = false; + uint32_t const kth2 = mergeThreshold(m2, takeAll2); + (void) emitSelection(s, keys, total + total2, kth2, takeAll2, outRow, 0); + if (threadIdx.x == 0) + { + atomicAdd(&p.state[2], 1); + } +} + +__global__ void __launch_bounds__(kThreads, 1) cpTopkKernel(CpTopkParams p) +{ + markPhase(p, 0); + extern __shared__ __align__(16) uint32_t smemRaw[]; + __shared__ uint32_t const* segSlots[kMaxSources]; + __shared__ uint32_t segBegin[kMaxSources + 1]; + int32_t const world = p.worldSize; + int32_t const rank = p.rank; + int32_t const src = blockIdx.x; + int32_t const row = blockIdx.y; + SmemLayout const s = carveSmem(smemRaw, p.localCapacity, world); + int32_t* outRow = p.out + static_cast(row) * kTopK; + // Every global read below (lengths, epoch, logits) comes after the wait, so a programmatic early launch is safe + // whatever the preceding kernel writes. + asm volatile("griddepcontrol.wait;" ::: "memory"); + int32_t const kvLen = p.kvLens[0]; + int32_t const epoch = *reinterpret_cast(&p.state[0]); + int32_t const buf = epoch % kNumLamportBuffers; + int32_t const rowLen = max(0, kvLen - p.nextN + row + 1); + // Identity launch (every row has at most kTopK tokens; the same decision on every rank): no exchange, and + // neither the workspace nor the epoch is touched, so identity launches never shorten the reuse chain of the + // Lamport buffers (see crossRankTopk.h). + bool const exchange = kvLen - p.nextN + p.numRows > kTopK; + __syncthreads(); + + if (rowLen <= kTopK) + { + // Every rank agrees on this branch: identity selection, no exchange for this row. + if (src == 0) + { + for (int32_t j = threadIdx.x; j < kTopK; j += kThreads) + { + outRow[j] = j < rowLen ? j : -1; + } + } + } + else + { + int32_t const localLen = min(localLength(rowLen, rank, world), p.localCapacity); + float const* rowLogits = p.logits + static_cast(row) * p.logitsStride; + + // ---- Round 1: this source's local top keys --------------------------------------------------------- + int32_t const n = loadSource(s, rowLogits, localLen, src, true); + __syncthreads(); + markPhase(p, 1); + uint32_t const cap = static_cast(roundCap(world)); + uint32_t const need = static_cast(roundNeed(world)); + SelectOut sel{0u, 0u, 0u, static_cast(n), true, false, 0}; + uint32_t selLo = 0u; + int32_t passInfo = 0; + if (n > static_cast(cap)) + { + // The cut lies among the keys of one sign (see kPositiveKey); the digits span that sign's key range. + SignRange const r = signRange(readStats(s.stats), need); + selLo = r.lo; + SmemKeys const keys{s.chunk, nullptr}; + sel = blockSelect(s, keys, n, r.lo, r.hi, need, cap, r.lo, r.bits, debugSlots(p, 32), r.aboveHi); + passInfo = sel.passes + (r.negative ? 10 : 0); + } + markPhase(p, 2); + uint32_t const sent = sendSource(s, sel, n, selLo, uint64_t{kEmptyWord}, cap, + p.mcWorkspace + round1Offset(buf, rank * kSourcesPerRank + src, row), src, rank, world, + reinterpret_cast(s.mkey), debugSlots(p, 0)); + putDebug(debugSlots(p, 0), 46, sent); + putDebug(debugSlots(p, 0), 47, passInfo); + markPhase(p, 3); + + // ---- Merge: one CTA per row and rank --------------------------------------------------------------- + if (src == 0) + { + WaitCtx const wait{p.state, p.timeoutNs, rank}; + mergeRow(p, s, segSlots, segBegin, buf, row, rowLen, localLen, rowLogits, outRow, wait); + markPhase(p, 7); + } + } + if (!exchange) + { + return; + } + + // The other CTAs reset the buffer of the previous exchange launch, whose candidates every rank has consumed: a + // peer writes it again only two exchange launches later, after this launch completed on this rank. + if (src != 0) + { + clearBuffer(p, (epoch + kNumLamportBuffers - 1) % kNumLamportBuffers, row * (kSourcesPerRank - 1) + src - 1, + static_cast(gridDim.y) * (kSourcesPerRank - 1)); + } + + // Advance the epoch once every CTA of this launch has read it. + __syncthreads(); + markPhase(p, 8); + if (threadIdx.x == 0) + { + __threadfence(); + int32_t const numCtas = static_cast(gridDim.x * gridDim.y); + int32_t const arrived = atomicAdd(&p.state[1], 1); + if (arrived == numCtas - 1) + { + p.state[1] = 0; + __threadfence(); + atomicAdd(&p.state[0], 1); + } + } +} + +#endif + +} // namespace + +size_t cpTopkSmemBytes(int32_t localCapacity, int32_t worldSize) +{ + return smemBytes(localCapacity, worldSize); +} + +int32_t cpTopkMaxLocalCapacity(int32_t worldSize) +{ + static int32_t cached[kMaxRanks + 1] = {}; + if (worldSize < 1 || worldSize > kMaxRanks) + { + return 0; + } + if (cached[worldSize] == 0) + { + int device = 0; + cudaGetDevice(&device); + int maxSmemOptin = 0; + cudaDeviceGetAttribute(&maxSmemOptin, cudaDevAttrMaxSharedMemoryPerBlockOptin, device); + int32_t cap = 0; + for (int32_t c = 1 << 22; c >= kPageTokens * kSourcesPerRank; c -= kPageTokens * kSourcesPerRank) + { + if (smemBytes(c, worldSize) + kStaticSmemBytes <= static_cast(maxSmemOptin)) + { + cap = c; + break; + } + } + cached[worldSize] = cap; + } + return cached[worldSize]; +} + +int64_t cpTopkDefaultTimeoutMs() +{ + static int64_t const timeoutMs = []() + { + char const* env = std::getenv("TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS"); + if (env == nullptr || env[0] == '\0') + { + return kDefaultTimeoutMs; + } + char* end = nullptr; + long long const v = std::strtoll(env, &end, 10); + if (end == env || *end != '\0' || v <= 0) + { + throw std::invalid_argument( + std::string("glm_moe_dsa_cross_rank_topk: bad TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS=") + + env); + } + return static_cast(v); + }(); + return timeoutMs; +} + +void invokeCpTopk(CpTopkParams const& inParams, cudaStream_t stream) +{ + if (inParams.numRows <= 0) + { + return; + } + CpTopkParams params = inParams; + if (params.timeoutNs == 0) + { + params.timeoutNs = static_cast(cpTopkDefaultTimeoutMs()) * 1000000ull; + } + if (params.numRows > kMaxRows || params.worldSize <= 0 || params.worldSize > kMaxRanks || params.rank < 0 + || params.rank >= params.worldSize) + { + throw std::invalid_argument("glm_moe_dsa_cross_rank_topk: unsupported rows/world/rank"); + } + size_t const smem = smemBytes(params.localCapacity, params.worldSize); + // The attribute is per function and device; raise it only when a larger footprint is requested. + static thread_local size_t configuredSmem[64] = {}; + int device = 0; + cudaGetDevice(&device); + cudaError_t err = cudaSuccess; + if (device >= 64 || configuredSmem[device] < smem) + { + err = cudaFuncSetAttribute(cpTopkKernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(smem)); + if (err != cudaSuccess) + { + throw std::runtime_error( + std::string("glm_moe_dsa_cross_rank_topk: smem attribute: ") + cudaGetErrorString(err)); + } + if (device < 64) + { + configuredSmem[device] = smem; + } + } + // Programmatic dependent launch as for the other TensorRT-LLM kernels (on unless TRTLLM_ENABLE_PDL=0). + static bool const enablePdl = []() + { + char const* env = std::getenv("TRTLLM_ENABLE_PDL"); + return !(env != nullptr && env[0] == '0' && env[1] == '\0'); + }(); + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kSourcesPerRank, params.numRows); + config.blockDim = dim3(kThreads); + config.dynamicSmemBytes = smem; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = enablePdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 1; + err = cudaLaunchKernelEx(&config, cpTopkKernel, params); + if (err == cudaSuccess) + { + err = cudaGetLastError(); + } + if (err != cudaSuccess) + { + throw std::runtime_error(std::string("glm_moe_dsa_cross_rank_topk: launch: ") + cudaGetErrorString(err)); + } +} + +} // namespace kernels::glm_moe_dsa::cross_rank_topk + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.h new file mode 100644 index 000000000000..2ff17f85e48e --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.h @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::cross_rank_topk +{ + +// Context-partitioned DSA indexer top-k. +// +// The indexer context of one generation request is partitioned over the ranks of a tensor-parallel group by whole +// 64-token pages: global page p belongs to rank p % worldSize, and a rank's local column c maps to global token +// ((c / 64) * worldSize + rank) * 64 + c % 64. Every rank scores only its own pages (the caller supplies the local +// logits). Within a rank, each verify row is handled by kSourcesPerRank CTAs; CTA s owns one 8-token block of every +// local page (block (s + page) % kSourcesPerRank), so the (rank, CTA) "sources" interleave the context finely and a +// fully selected page is spread over all sources of its rank. Each source +// 1. selects its local top keys (kSourceBudget at 8 ranks, see below) with a block radix select, +// 2. broadcasts them as (order-preserving key, global token) pairs, plus a header (count, exclusive bound of the +// keys it withheld), through a multicast Lamport workspace. +// One CTA per row and rank then merges the candidates of all sources into the exact top-kTopK of the row, +// identically on every rank. A source that withheld a key above the merged threshold makes every rank run a +// second round in which that source's top keys between the threshold and its bound are sent (at most +// kRound2Capacity, by the merge CTA of the source's rank, from the local logits), so the result is the tie-aware +// exact top-kTopK for any input. In the second round, a source whose cut falls inside a run of equal keys sends the +// keys above the cut and the first equal ones in source order up to its capacity (round 1 sends only the keys above +// such a cut, so the two rounds never send the same key). +// Rows of at most kTopK valid tokens produce the identity selection followed by -1, without an exchange. +// A launch whose rows all have at most kTopK valid tokens (the same on every rank: the lengths come from the shared +// KV length) is an identity launch: it touches neither the workspace nor the epoch. Every other launch is an +// exchange launch. Exchange launches rotate kNumLamportBuffers buffers by a device epoch that counts exchange +// launches; in exchange launch x the CTAs that do not merge reset, on their own rank, the buffer of exchange launch +// x - 1. Every rank's merges of x - 1 have read their copy of it, and a peer writes it again only in exchange +// launch x + 2, which starts after the peer's merge of x + 1 received this rank's sources of x + 1, i.e. after this +// rank completed launch x (identity launches in between neither write nor wait, so they do not shorten this chain). +// +// A merge that waits longer than the timeout (CpTopkParams::timeoutNs, %globaltimer) for a peer's words records the +// wait in the state's error word and aborts the kernel with a trap: the exchange cannot complete correctly any more, +// and a late peer write could land in a buffer this rank reuses, so the run fails instead of continuing. + +//! Selected indices per verify row (DSA index_topk). +constexpr int32_t kTopK = 2048; +//! Ranks of the context-parallel group. +constexpr int32_t kMaxRanks = 8; +//! Verify rows per call. +constexpr int32_t kMaxRows = 8; +//! Tokens per indexer K page; the context is partitioned by whole pages. +constexpr int32_t kPageTokens = 64; +//! CTAs (exchange sources) per rank and verify row. +constexpr int32_t kSourcesPerRank = 8; +//! Exchange sources per row. +constexpr int32_t kMaxSources = kMaxRanks * kSourcesPerRank; +//! Round-1 slots per (source, row). +constexpr int32_t kSourceCapacity = 256; +//! Keys each saturated source aims to send in round 1 at 8 ranks (64 sources): three times the even share of kTopK +//! (the largest share of the exact top-2048 that one source of this layout held on real GLM-5.2 indexer logits was +//! 69). A source stops refining as soon as its keys at or above a bin edge number at most four times the even +//! share, so it sends between 96 and 128 keys at 64 sources. Other group sizes scale both with ceil(kTopK / sources). +constexpr int32_t kSourceBudget = 96; +//! Round-2 slots per (source, row). +constexpr int32_t kRound2Capacity = 2048; +//! Threads per CTA. +constexpr int32_t kThreads = 1024; +//! Lamport buffers rotated by the device epoch. +constexpr int32_t kNumLamportBuffers = 3; +//! 32-bit word that marks an empty Lamport slot; never a valid key, token index, count or bound. +constexpr uint32_t kEmptyWord = 0xFFFFFFFFu; + +//! 32-bit words of one (source, row) slot: a 16-byte header (count, bound, two unused words) then two words per +//! candidate, so candidates can be written and read with 16-byte vector accesses. +constexpr int64_t kRound1SlotWords = 4 + 2 * int64_t{kSourceCapacity}; +constexpr int64_t kRound2SlotWords = 4 + 2 * int64_t{kRound2Capacity}; +//! 32-bit words of one Lamport buffer (round-1 region, then round-2 region). +constexpr int64_t kBufferWords = int64_t{kMaxSources} * kMaxRows * (kRound1SlotWords + kRound2SlotWords); +//! 32-bit words of the whole exchange workspace; every word must start as kEmptyWord. +constexpr int64_t kWorkspaceWords = kNumLamportBuffers * kBufferWords; +//! int32 words of the per-rank device state: epoch (exchange launches), CTA arrivals, second rounds taken, error +//! word (nonzero = the wait that timed out, written right before the kernel aborts). +constexpr int32_t kStateWords = 4; +//! Default wait for a peer's Lamport words before the kernel aborts +//! (TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS overrides it). +constexpr int64_t kDefaultTimeoutMs = 120000; + +//! Values of the error word. +enum CpTopkWait : int32_t +{ + kWaitRound1Header = 1, + kWaitRound1Candidates = 2, + kWaitRound2Header = 3, + kWaitRound2Candidates = 4 +}; + +struct CpTopkParams +{ + //! [numRows, logitsStride] fp32 logits of this rank's local columns. + float const* logits; + int64_t logitsStride; + //! Local columns the logits view holds (each row's local length is clamped to it). + int32_t localCapacity; + //! [1] int32 global indexer KV length of the request; row r has kvLen - nextN + r + 1 valid tokens. + int32_t const* kvLens; + //! [numRows, kTopK] int32 output: global token indices, -1 for unused slots. + int32_t* out; + int32_t numRows; + int32_t nextN; + int32_t rank; + int32_t worldSize; + //! Multicast address of the exchange workspace (kWorkspaceWords words, identical layout on every rank). + uint32_t* mcWorkspace; + //! This rank's unicast address of the same workspace. + uint32_t* ucWorkspace; + //! [kStateWords] int32 per-rank device state, zero-initialized. + int32_t* state; + //! Optional [numRows, kSourcesPerRank, kDebugSlots] int64 phase timestamps (diagnosis only; nullptr = off). + int64_t* debugTimes = nullptr; + //! Longest wait for a peer's words before the kernel aborts (ns); 0 = the default (kDefaultTimeoutMs, or + //! TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS). + uint64_t timeoutNs = 0; +}; + +//! Timestamps per CTA in CpTopkParams::debugTimes: [0, 16) %globaltimer (ns), [16, 32) clock64 per phase, +//! [32, 42) local select (keys, then clock64 at the start of / after the histogram of / after the bin search of +//! each pass), [42, 45) send (counter ready, keys taken, stores issued), 45 candidates polled before the key-range +//! reduction, 46 round-1 keys sent by this source, 47 local select passes (+10 when the cut was searched among the +//! negative keys), [48, 58) merge select (as the local select), [58, 62) emission (threshold decided, positions +//! scanned, row assembled, row written), 62 merge candidates, 63 merge select passes (+10 likewise); clock64 and +//! values of thread 0. +constexpr int32_t kDebugSlots = 64; + +//! Effective wait timeout of a launch with params.timeoutNs == 0 (ms). +int64_t cpTopkDefaultTimeoutMs(); + +//! Dynamic shared memory the kernel needs (bytes). +size_t cpTopkSmemBytes(int32_t localCapacity, int32_t worldSize); + +//! Largest local capacity the kernel accepts for a world size. +int32_t cpTopkMaxLocalCapacity(int32_t worldSize); + +void invokeCpTopk(CpTopkParams const& params, cudaStream_t stream); + +} // namespace kernels::glm_moe_dsa::cross_rank_topk + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.cu new file mode 100644 index 000000000000..a0b9671537c5 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.cu @@ -0,0 +1,2420 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.h" + +#include + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::cross_rank_topk_cluster_merge +{ +namespace +{ + +constexpr int32_t kBins = 2048; +static_assert(kBins >= kTopK, "the output row is staged in the histogram"); +constexpr int32_t kWarps = kThreads / 32; +//! Per-warp key statistics kept in shared memory (see KeyStats). +constexpr int32_t kNumKeyStats = 5; +//! Tokens of each page one source holds. +constexpr int32_t kBlockTokens = kPageTokens / kSourcesPerRank; +//! CTAs of one row's cluster. +constexpr int32_t kClusterCtas = kSourcesPerRank; +//! Histogram bins per group of the cluster bin search (one warp lane per bin). +constexpr int32_t kGroupBins = 32; +constexpr int32_t kMaxGroups = kBins / kGroupBins; +//! Static shared memory of the kernel (round-2 segment tables and the cluster exchange records), kept out of the +//! dynamic budget. +constexpr size_t kStaticSmemBytes = 8192; + +// Records the CTAs of a row's cluster send each other with st.async (static, so every CTA has them at the same +// address). Exchange x of a row completes on barrier bar[x % 2], phase x / 2; the record sets alternate by pass +// parity and are written again only after every CTA has completed the exchange in between. +struct ClusterRecords +{ + //! Transaction barriers of the exchanges (arrival count 1: this CTA's expect-tx). + uint64_t bar[2]; + //! [pass parity][source CTA][group] histogram totals per group of kGroupBins bins. + uint32_t groups[2][kClusterCtas][kMaxGroups]; + //! [pass parity][source CTA][bin] counts of the selected group's bins, ascending. + uint32_t bins[2][kClusterCtas][kGroupBins]; +}; + +// Scalar slots in shared memory. +enum ScalarSlot : int32_t +{ + kSlotBin = 0, + kSlotAbove, + kSlotBinCount, + kSlotTotal, + kSlotCounter2, + kSlotSrcCount, // [kMaxSources] + kSlotSrcBound = kSlotSrcCount + kMaxSources, // [kMaxSources] + kSlotSrcLen = kSlotSrcBound + kMaxSources, // [kMaxSources] + kSlotSrcOffset = kSlotSrcLen + kMaxSources, // [kMaxSources + 1] + kSlotCountBefore = kSlotSrcOffset + kMaxSources + 1, // candidates of the cluster CTAs before this one + kSlotCountMine, // candidates of this cluster CTA + kSlotCountTotal, // candidates of the row + kSlotPositivesBefore, // candidates >= +0.0 of the cluster CTAs before this one + kNumScalarSlots +}; + +__host__ __device__ inline int32_t numSources(int32_t world) +{ + return world * kSourcesPerRank; +} + +// Even share of kTopK per source. +__host__ __device__ inline int32_t evenShare(int32_t world) +{ + return (kTopK + numSources(world) - 1) / numSources(world); +} + +// Most keys a source sends in round 1 (an even number). +__host__ __device__ inline int32_t roundCap(int32_t world) +{ + return min(kSourceCapacity, 4 * evenShare(world)); +} + +// Keys one source can hold: one block of every local page. +__host__ __device__ inline int32_t chunkCapacity(int32_t localCapacity) +{ + int32_t const pages = (localCapacity + kPageTokens - 1) / kPageTokens; + return pages * kBlockTokens; +} + +__host__ __device__ inline int32_t mergeCapacity(int32_t world) +{ + return numSources(world) * roundCap(world); +} + +__host__ __device__ inline size_t smemBytes(int32_t localCapacity, int32_t world) +{ + size_t words = 2 * size_t{kBins} + 2 * size_t{kWarps} + size_t{kNumKeyStats} * kWarps + size_t{kNumScalarSlots}; + words = (words + 3) / 4 * 4; + words += static_cast(chunkCapacity(localCapacity)) + 2 * static_cast(mergeCapacity(world)); + return words * sizeof(uint32_t); +} + +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 900) +// The kernel targets SM90+ (multicast Lamport exchange, thread-block clusters). +__global__ void __launch_bounds__(kThreads, 1) cpTopkZ9Kernel(CpTopkParams p) +{ + __trap(); +} +#else + +//! Bits of one radix-select digit (kBins bins). +constexpr int32_t kDigitBits = 11; +//! Order-preserving key of +0.0: the keys of non-negative values are at least this, those of negative values below. +//! Real indexer logits are mostly negative, and the cut of a row (and of many sources) often lies among small +//! negative values while the largest keys are positive: one key range across zero spans ~2^31 key units, where a +//! radix digit has no resolution. The selects therefore work on one sign at a time: with fewer than `need` keys at +//! or above kPositiveKey these are all taken and the cut is searched among the negative keys only, else among the +//! non-negative keys only; each range is narrow enough for one or two passes. +constexpr uint32_t kPositiveKey = 0x80000000u; +//! Independent 16-byte candidate loads one merge thread issues before it waits on any of them. +constexpr int32_t kPollBatch = 4; +//! Polls of a still-empty Lamport word between two reads of the global timer. +constexpr int32_t kPollsPerTimerRead = 256; +//! Warps of the cluster merge: warps [0, kPollWarps) poll the round-1 candidates of this CTA's sources (one 16-byte +//! vector per thread: at most 520 vectors per CTA, at world sizes 5 to 7), the next kHeaderWarps warps poll the +//! round-1 headers of every source (one per thread), and the remaining kResetWarps warps reset the previous exchange +//! launch's buffer meanwhile. +constexpr int32_t kPollWarps = 17; +constexpr int32_t kHeaderWarps = 2; +constexpr int32_t kFirstResetWarp = kPollWarps + kHeaderWarps; +constexpr int32_t kResetWarps = kWarps - kFirstResetWarp; +static_assert(kHeaderWarps * 32 >= kMaxSources, "one header per header-warp thread"); + +// Keys a saturated source aims to send in round 1 (kSourceBudget at 8 ranks). +__device__ __forceinline__ int32_t roundNeed(int32_t world) +{ + return min(kSourceCapacity, 3 * evenShare(world)); +} + +// Tokens of `rank` among the global tokens [0, n). +__host__ __device__ __forceinline__ int32_t localLength(int32_t n, int32_t rank, int32_t world) +{ + if (n <= 0) + { + return 0; + } + int32_t const fullPages = n / kPageTokens; + int32_t const rem = n % kPageTokens; + int32_t const ownedFull = (fullPages - rank + world - 1) / world; + int32_t len = ownedFull * kPageTokens; + if (rem > 0 && fullPages % world == rank) + { + len += rem; + } + return len; +} + +// Source `src` of a rank holds one block of kBlockTokens consecutive tokens of every local page: block +// (src + page) % kSourcesPerRank. A fully selected page is thus spread over all sources of its rank. +// Keys of source `src` among `localLen` local columns (only the last local page can be partial). +__host__ __device__ __forceinline__ int32_t sourceLength(int32_t localLen, int32_t src) +{ + int32_t const pages = (localLen + kPageTokens - 1) / kPageTokens; + if (pages == 0) + { + return 0; + } + int32_t const last = pages - 1; + int32_t const tail = localLen - last * kPageTokens - ((src + last) % kSourcesPerRank) * kBlockTokens; + return last * kBlockTokens + min(max(tail, 0), kBlockTokens); +} + +// Global token of a local column under the page-interleaved partition. +__device__ __forceinline__ uint32_t localToGlobal(int32_t col, int32_t rank, int32_t world) +{ + int32_t const page = col / kPageTokens; + return static_cast((page * world + rank) * kPageTokens + (col % kPageTokens)); +} + +struct SmemLayout +{ + uint32_t* hist; // [2 * kBins] radix histograms of alternate passes; the first one stages the output row + uint32_t* warpSums; // [2 * kWarps] + uint32_t* stats; // [kNumKeyStats * kWarps] per-warp key statistics + uint32_t* scal; // [kNumScalarSlots] + uint32_t* chunk; // [chunkCapacity] order-preserving keys of this source + uint32_t* mkey; // [mergeCapacity] staged round-1 pairs, then the merge candidates (merge CTA) + uint32_t* midx; // [mergeCapacity] +}; + +__device__ inline SmemLayout carveSmem(uint32_t* base, int32_t localCapacity, int32_t world) +{ + SmemLayout s; + uint32_t* p = base; + s.hist = p; + p += 2 * kBins; + s.warpSums = p; + p += 2 * kWarps; + s.stats = p; + p += kNumKeyStats * kWarps; + s.scal = p; + p += kNumScalarSlots; + p = base + ((p - base) + 3) / 4 * 4; + s.chunk = p; + p += chunkCapacity(localCapacity); + s.mkey = p; + p += mergeCapacity(world); + s.midx = p; + return s; +} + +__device__ __forceinline__ uint32_t orderedKey(float v) +{ + uint32_t const b = __float_as_uint(v); + uint32_t const k = (b & 0x80000000u) ? ~b : (b | 0x80000000u); + // NaN keys above +inf are clamped so that no key, and no exclusive bound key + 1, equals the empty marker. + return k < kEmptyWord - 1u ? k : kEmptyWord - 2u; +} + +// Bits of x (0 for 0): the digit range of keys in [base, base + x]. +__device__ __forceinline__ int32_t bitLength(uint32_t x) +{ + return x == 0u ? 0 : 32 - __clz(x); +} + +// Statistics of a set of keys: extremes overall and per sign, and the count of non-negative keys. +struct KeyStats +{ + uint32_t maxKey = 0u; // largest key (0 when none) + uint32_t minKey = kEmptyWord; // smallest key (kEmptyWord when none) + uint32_t positives = 0u; // keys >= kPositiveKey + uint32_t maxNeg = 0u; // largest key below kPositiveKey (0 when none) + uint32_t minPos = kEmptyWord; // smallest key >= kPositiveKey (kEmptyWord when none) + + __device__ __forceinline__ void add(uint32_t k) + { + bool const pos = k >= kPositiveKey; + maxKey = max(maxKey, k); + minKey = min(minKey, k); + positives += pos ? 1u : 0u; + maxNeg = pos ? maxNeg : max(maxNeg, k); + minPos = pos ? min(minPos, k) : minPos; + } +}; + +// Reduce a thread's statistics over its warp; lane 0 stores them in the per-warp array (read with readStats after +// the next block barrier). +__device__ __forceinline__ void storeWarpStats(KeyStats const& t, uint32_t* stats) +{ + uint32_t const v0 = __reduce_max_sync(0xFFFFFFFFu, t.maxKey); + uint32_t const v1 = __reduce_min_sync(0xFFFFFFFFu, t.minKey); + uint32_t const v2 = __reduce_add_sync(0xFFFFFFFFu, t.positives); + uint32_t const v3 = __reduce_max_sync(0xFFFFFFFFu, t.maxNeg); + uint32_t const v4 = __reduce_min_sync(0xFFFFFFFFu, t.minPos); + if (threadIdx.x % 32 == 0) + { + int32_t const w = threadIdx.x / 32; + stats[w] = v0; + stats[kWarps + w] = v1; + stats[2 * kWarps + w] = v2; + stats[3 * kWarps + w] = v3; + stats[4 * kWarps + w] = v4; + } +} + +// Block statistics from the per-warp array (every warp reduces it; no barrier). +__device__ __forceinline__ KeyStats readStats(uint32_t const* stats) +{ + int32_t const lane = threadIdx.x % 32; + KeyStats b; + b.maxKey = __reduce_max_sync(0xFFFFFFFFu, stats[lane]); + b.minKey = __reduce_min_sync(0xFFFFFFFFu, stats[kWarps + lane]); + b.positives = __reduce_add_sync(0xFFFFFFFFu, stats[2 * kWarps + lane]); + b.maxNeg = __reduce_max_sync(0xFFFFFFFFu, stats[3 * kWarps + lane]); + b.minPos = __reduce_min_sync(0xFFFFFFFFu, stats[4 * kWarps + lane]); + return b; +} + +// Digit setup of a select of the need-th largest key: over the non-negative keys when there are at least `need` of +// them, else over the negative keys with every non-negative key counted as taken (`aboveHi`). +struct SignRange +{ + uint32_t lo; // smallest key of the range (also the digit base) + uint64_t hi; // exclusive end of the histogrammed range + int32_t bits; // digit range of the range + uint32_t aboveHi; // keys at or above hi, all taken + bool negative; // the cut is among the negative keys +}; + +__device__ __forceinline__ SignRange signRange(KeyStats const& st, uint32_t need) +{ + if (st.positives >= need || st.positives == 0u) + { + uint32_t const lo = st.positives >= need ? st.minPos : st.minKey; + return SignRange{lo, uint64_t{kEmptyWord}, bitLength(st.maxKey - lo), 0u, st.positives == 0u}; + } + return SignRange{st.minKey, uint64_t{kPositiveKey}, bitLength(st.maxNeg - st.minKey), st.positives, true}; +} + +__device__ __forceinline__ int64_t round1Offset(int32_t buf, int32_t src, int32_t row) +{ + return int64_t{buf} * kBufferWords + (int64_t{src} * kMaxRows + row) * kRound1SlotWords; +} + +__device__ __forceinline__ int64_t round2Offset(int32_t buf, int32_t src, int32_t row) +{ + return int64_t{buf} * kBufferWords + int64_t{kMaxSources} * kMaxRows * kRound1SlotWords + + (int64_t{src} * kMaxRows + row) * kRound2SlotWords; +} + +__device__ __forceinline__ uint2 loadVolatile2(uint32_t const* ptr) +{ + uint2 v; + asm volatile("ld.volatile.global.v2.u32 {%0, %1}, [%2];" : "=r"(v.x), "=r"(v.y) : "l"(ptr) : "memory"); + return v; +} + +__device__ __forceinline__ uint4 loadVolatile4(uint32_t const* ptr) +{ + uint4 v; + asm volatile("ld.volatile.global.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) + : "l"(ptr) + : "memory"); + return v; +} + +__device__ __forceinline__ void store2(uint32_t* ptr, uint32_t a, uint32_t b) +{ + asm volatile("st.global.v2.u32 [%0], {%1, %2};" ::"l"(ptr), "r"(a), "r"(b) : "memory"); +} + +__device__ __forceinline__ void store4(uint32_t* ptr, uint4 v) +{ + asm volatile("st.global.v4.u32 [%0], {%1, %2, %3, %4};" ::"l"(ptr), "r"(v.x), "r"(v.y), "r"(v.z), "r"(v.w) + : "memory"); +} + +__device__ __forceinline__ bool isEmpty(uint2 v) +{ + return v.x == kEmptyWord || v.y == kEmptyWord; +} + +// Whether the first pair of a 16-byte candidate vector, and the second one when `second`, were written. +__device__ __forceinline__ bool vectorReady(uint4 v, bool second) +{ + return v.x != kEmptyWord && v.y != kEmptyWord && (!second || (v.z != kEmptyWord && v.w != kEmptyWord)); +} + +__device__ __forceinline__ uint64_t globalTimerNs() +{ + uint64_t t; + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); + return t; +} + +// What a polling thread needs to give up: the error word, the timeout and this rank (for the report). +struct WaitCtx +{ + int32_t* state; + uint64_t timeoutNs; + int32_t rank; +}; + +// A peer did not deliver within the timeout. The selection cannot be completed correctly any more, and the peer's +// late writes could land in a buffer this rank resets or reuses, so record which wait failed and abort the kernel: +// the resulting CUDA error fails the run instead of letting any rank continue with a partial selection. +__device__ __noinline__ void abortOnTimeout(WaitCtx const& w, int32_t what) +{ + bool const first = atomicExch(&w.state[3], what) == 0; + __threadfence_system(); + if (first) + { + printf( + "[glm_moe_dsa] crossRankTopkClusterMerge: rank %d, row %d: wait %d for a peer exceeded %llu ms; aborting " + "the kernel\n", + w.rank, static_cast(blockIdx.y), what, static_cast(w.timeoutNs / 1000000ull)); + } + __trap(); +} + +// Spin until both words of a Lamport pair were written; aborts the kernel after the timeout. +__device__ uint2 pollPair(uint32_t const* ptr, WaitCtx const& w, int32_t what) +{ + uint2 v = loadVolatile2(ptr); + uint64_t start = 0; + for (int32_t polls = 1; isEmpty(v); ++polls) + { + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, what); + } + } + v = loadVolatile2(ptr); + } + return v; +} + +// Spin until a 16-byte candidate vector was written (see vectorReady); aborts the kernel after the timeout. +__device__ uint4 pollVector(uint32_t const* ptr, bool second, WaitCtx const& w, int32_t what) +{ + uint4 v = loadVolatile4(ptr); + uint64_t start = 0; + for (int32_t polls = 1; !vectorReady(v, second); ++polls) + { + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, what); + } + } + v = loadVolatile4(ptr); + } + return v; +} + +// Block-wide inclusive scan of one value per thread. Returns the inclusive prefix; `total` gets the block sum. +__device__ uint32_t blockInclusiveScan(uint32_t v, uint32_t* warpSums, uint32_t& total) +{ + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + uint32_t x = v; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + if (lane >= o) + { + x += y; + } + } + if (lane == 31) + { + warpSums[warp] = x; + } + __syncthreads(); + if (warp == 0) + { + uint32_t w = warpSums[lane]; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, w, o); + if (lane >= o) + { + w += y; + } + } + warpSums[kWarps + lane] = w; + } + __syncthreads(); + total = warpSums[2 * kWarps - 1]; + uint32_t const before = warp > 0 ? warpSums[kWarps + warp - 1] : 0u; + return x + before; +} + +// Exclusive block-wide scan with a single barrier: every warp scans the per-warp sums itself. Returns this +// thread's exclusive prefix; `total` gets the block sum. warpSums must not be rewritten before the next barrier. +__device__ uint32_t blockExclusiveScan(uint32_t v, uint32_t* warpSums, uint32_t& total) +{ + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + uint32_t x = v; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + if (lane >= o) + { + x += y; + } + } + if (lane == 31) + { + warpSums[warp] = x; + } + __syncthreads(); + uint32_t const w = warpSums[lane]; + uint32_t y = w; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const z = __shfl_up_sync(0xFFFFFFFFu, y, o); + if (lane >= o) + { + y += z; + } + } + total = __shfl_sync(0xFFFFFFFFu, y, 31); + return __shfl_sync(0xFFFFFFFFu, y - w, warp) + x - v; +} + +// Find, counting from the top bin, the bin of `hist` that holds the target-th largest key, with two barriers: +// per-warp sums, then each warp scans them and only the warp holding the target scans its own bins. Returns the +// total count (every thread); when target <= total, the bin, the count above it and its own count go to the +// scalar slots. +__device__ uint32_t findBin(SmemLayout const& s, uint32_t const* hist, int32_t nbins, uint32_t target) +{ + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + int32_t const perThread = (nbins + kThreads - 1) / kThreads; + int32_t const top = nbins - 1 - perThread * static_cast(threadIdx.x); + uint32_t mine = 0; + for (int32_t i = 0; i < perThread && top - i >= 0; ++i) + { + mine += hist[top - i]; + } + uint32_t const warpTotal = __reduce_add_sync(0xFFFFFFFFu, mine); + if (lane == 0) + { + s.warpSums[warp] = warpTotal; + } + __syncthreads(); + uint32_t const w = s.warpSums[lane]; + uint32_t x = w; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + if (lane >= o) + { + x += y; + } + } + uint32_t const total = __shfl_sync(0xFFFFFFFFu, x, 31); + uint32_t const warpBefore = __shfl_sync(0xFFFFFFFFu, x - w, warp); + if (warpBefore < target && target <= warpBefore + warpTotal) + { + uint32_t t = mine; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, t, o); + if (lane >= o) + { + t += y; + } + } + uint32_t acc = warpBefore + t - mine; + if (acc < target && target <= acc + mine) + { + for (int32_t i = 0; i < perThread && top - i >= 0; ++i) + { + uint32_t const c = hist[top - i]; + if (acc + c >= target) + { + s.scal[kSlotBin] = static_cast(top - i); + s.scal[kSlotAbove] = acc; + s.scal[kSlotBinCount] = c; + break; + } + acc += c; + } + } + } + __syncthreads(); + return total; +} + +struct SelectOut +{ + uint32_t edge; // lower edge of the selected bin (the exact key after the last digit) + uint32_t above; // predicate keys strictly above the selected bin + uint32_t binCount; // predicate keys in the selected bin + uint32_t total; // predicate keys + bool all; // take every predicate key (fewer than needed, or all fit the capacity) + bool full; // exact mode: the keys at or above `edge` are exactly the `need` largest + int32_t passes; // radix passes run +}; + +// Candidate keys and token indices held in shared memory (16-byte aligned, readable in groups of four up to the +// next multiple of four). +struct SmemKeys +{ + static constexpr bool kVector = true; + uint32_t const* keys; + uint32_t const* idx; + + __device__ uint4 keys4(int32_t j) const + { + return *reinterpret_cast(keys + j); + } + + __device__ uint4 index4(int32_t j) const + { + return *reinterpret_cast(idx + j); + } +}; + +// Merge candidates in shared memory followed by round-2 candidates read from their global Lamport slots. +struct MergeKeys +{ + static constexpr bool kVector = false; + uint32_t const* smemKeys; + uint32_t const* smemIdx; + int32_t smemCount; + uint32_t const* const* slots; // round-2 slot of each segment + uint32_t const* segBegin; // [numSegments + 1] candidate offsets + int32_t numSegments; + + __device__ uint32_t const* pair(int32_t k) const + { + // Last segment whose begin is <= k. + int32_t lo = 0; + int32_t hi = numSegments - 1; + while (lo < hi) + { + int32_t const mid = (lo + hi + 1) / 2; + if (static_cast(segBegin[mid]) <= k) + { + lo = mid; + } + else + { + hi = mid - 1; + } + } + return slots[lo] + kSlotHeaderWords + 2 * (k - static_cast(segBegin[lo])); + } + + __device__ uint32_t operator()(int32_t j) const + { + return j < smemCount ? smemKeys[j] : pair(j - smemCount)[0]; + } + + __device__ uint32_t index(int32_t j) const + { + return j < smemCount ? smemIdx[j] : pair(j - smemCount)[1]; + } +}; + +// Debug slots of this CTA starting at `offset` (nullptr without a debug buffer). +__device__ __forceinline__ int64_t* debugSlots(CpTopkParams const& p, int32_t offset) +{ + if (p.debugTimes == nullptr) + { + return nullptr; + } + return p.debugTimes + (static_cast(blockIdx.y) * kSourcesPerRank + blockIdx.x) * kDebugSlots + offset; +} + +// clock64 of thread 0 into a debug slot (no-op without a debug buffer). +__device__ __forceinline__ void stamp(int64_t* dbg, int32_t slot) +{ + if (dbg != nullptr && threadIdx.x == 0) + { + dbg[slot] = clock64(); + } +} + +// A value into a debug slot (thread 0; no-op without a debug buffer). +__device__ __forceinline__ void putDebug(int64_t* dbg, int32_t slot, int64_t value) +{ + if (dbg != nullptr && threadIdx.x == 0) + { + dbg[slot] = value; + } +} + +// Phase timestamp for diagnosis (thread 0 of the CTA; no-op without a debug buffer). +__device__ __forceinline__ void markPhase(CpTopkParams const& p, int32_t phase) +{ + if (p.debugTimes != nullptr && threadIdx.x == 0) + { + uint64_t t; + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); + int64_t* d = p.debugTimes + (static_cast(blockIdx.y) * kSourcesPerRank + blockIdx.x) * kDebugSlots; + d[phase] = static_cast(t); + d[16 + phase] = static_cast(clock64()); + } +} + +// Histogram update of one key of a radix pass. +__device__ __forceinline__ void histogramKey(uint32_t* hist, bool inRange, uint32_t k, uint32_t lo, uint64_t hi, + uint32_t base, uint32_t prefMask, uint32_t prefVal, int32_t shift, uint32_t binMask) +{ + uint32_t const d = k - base; + if (inRange && k >= lo && static_cast(k) < hi && (d & prefMask) == prefVal) + { + atomicAdd(&hist[(d >> shift) & binMask], 1u); + } +} + +// Block radix select of the need-th largest of the n keys at or above lo, of which `aboveHi` (< need) lie at or +// above hi and are counted as taken without being histogrammed. Every key in [lo, hi) is at least `base`, and +// key - base < 2^topBits; digits of up to kDigitBits bits of key - base are refined from the top. With cap > 0 the +// select stops at the first bin edge whose keys at or above it number at most `cap`. With cap = 0 it runs to the +// exact key, or stops early when the selected bin is wholly taken (`full`). Alternate passes use alternate +// histograms, so the next one is cleared while the current one is searched. Counts in the result include aboveHi. +template +__device__ SelectOut blockSelect(SmemLayout const& s, KeyAt const& keyAt, int32_t n, uint32_t lo, uint64_t hi, + uint32_t need, uint32_t cap, uint32_t base, int32_t topBits, int64_t* dbg = nullptr, uint32_t aboveHi = 0u) +{ + if (dbg != nullptr && threadIdx.x == 0) + { + dbg[0] = n; + } + SelectOut out{lo, 0u, 0u, 0u, false, false, 0}; + uint32_t prefMask = 0; + uint32_t prefVal = 0; + uint32_t above = aboveHi; + int32_t remaining = max(topBits, 1); + uint32_t* hist = s.hist; + uint32_t* histNext = s.hist + kBins; + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, remaining)); b += kThreads) + { + hist[b] = 0; + } + __syncthreads(); +#pragma unroll 1 + for (int32_t pass = 0; remaining > 0; ++pass) + { + int32_t const width = min(kDigitBits, remaining); + int32_t const shift = remaining - width; + int32_t const nbins = 1 << width; + uint32_t const binMask = static_cast(nbins - 1); + out.passes = pass + 1; + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[1 + 3 * pass] = clock64(); + } + if constexpr (KeyAt::kVector) + { + for (int32_t j0 = 4 * static_cast(threadIdx.x); j0 < n; j0 += 4 * kThreads) + { + uint4 const q = keyAt.keys4(j0); + histogramKey(hist, true, q.x, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, j0 + 1 < n, q.y, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, j0 + 2 < n, q.z, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, j0 + 3 < n, q.w, lo, hi, base, prefMask, prefVal, shift, binMask); + } + } + else + { + for (int32_t j = threadIdx.x; j < n; j += kThreads) + { + histogramKey(hist, true, keyAt(j), lo, hi, base, prefMask, prefVal, shift, binMask); + } + } + __syncthreads(); + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[2 + 3 * pass] = clock64(); + } + if (shift > 0) + { + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, shift)); b += kThreads) + { + histNext[b] = 0; + } + } + // Find the bin holding the (need - above)-th largest key, counting from the top bin. + uint32_t const target = need - above; + uint32_t const total = findBin(s, hist, nbins, target); + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[3 + 3 * pass] = clock64(); + } + if (pass == 0) + { + out.total = total + aboveHi; + if (total + aboveHi <= cap || target > total) + { + out.all = true; + out.above = total + aboveHi; + return out; + } + } + // The scalars are next written after two more barriers (the next histogram and scan). + uint32_t const bin = s.scal[kSlotBin]; + above += s.scal[kSlotAbove]; + out.binCount = s.scal[kSlotBinCount]; + prefVal |= bin << shift; + prefMask |= binMask << shift; + out.edge = base + prefVal; + out.above = above; + remaining = shift; + uint32_t* const t = hist; + hist = histNext; + histNext = t; + if (cap > 0 && above + out.binCount <= cap) + { + return out; + } + if (cap == 0 && remaining > 0 && above + out.binCount == need) + { + out.full = true; + return out; + } + } + return out; +} + +// Send one source's selected keys (keys in [lo, hi) of the chunk) to its slot; returns the count sent. +// Takes every predicate key at or above the edge when they fit `cap`. Otherwise (more than `cap` keys tie at the +// exact edge) it takes every key above the edge, plus, with kPartialTies, the first keys equal to it in source +// order up to `cap` (any subset of equal keys is a valid tie-aware choice). Round 1 sends no partial ties, so the +// keys a second round requests from a source (at most the edge) were never sent before. +// The header carries the count and an exclusive upper bound of the keys left unsent (0 when none); in round 1 +// (!kPartialTies) also the key statistics of the keys sent (see kSlotHeaderWords). +// With `stage` (round 1, at most kSourceCapacity keys), the pairs are gathered in shared memory first and written +// with coalesced 16-byte stores; otherwise each pair is stored where it is found. +template +__device__ uint32_t sendSource(SmemLayout const& s, SelectOut const& sel, int32_t n, uint32_t lo, uint64_t hi, + uint32_t cap, uint32_t* mcSlot, int32_t src, int32_t rank, int32_t world, uint2* stage, int64_t* dbg = nullptr) +{ + bool const strict = !sel.all && sel.above + sel.binCount > cap; + uint32_t const threshold = sel.all ? lo : sel.edge; + // Keys equal to the edge that fit after the keys above it (sel.above < need <= cap when strict). + uint32_t const tieBudget = kPartialTies && strict ? cap - sel.above : 0u; + uint32_t const sent = sel.all ? sel.total : (strict ? sel.above + tieBudget : sel.above + sel.binCount); + uint32_t const bound = sel.all ? lo : (strict ? sel.edge + 1u : sel.edge); + stamp(dbg, 42); + // One page block (kBlockTokens keys) per thread; positions from a block scan, so the slot holds the keys in + // source order: first those taken outright, then (strict only) the admitted keys equal to the edge. + int32_t const groups = (n + kBlockTokens - 1) / kBlockTokens; + uint32_t takenMain = 0; + uint32_t takenTies = 0; + uint32_t const tieBase = strict ? sel.above : 0u; + for (int32_t g0 = 0; g0 < groups; g0 += kThreads) + { + int32_t const g = g0 + static_cast(threadIdx.x); + uint32_t k[kBlockTokens]; + uint32_t mask = 0; + uint32_t tieMask = 0; + if (g < groups) + { + uint4 const a = *reinterpret_cast(s.chunk + kBlockTokens * g); + uint4 const b = *reinterpret_cast(s.chunk + kBlockTokens * g + 4); + k[0] = a.x; + k[1] = a.y; + k[2] = a.z; + k[3] = a.w; + k[4] = b.x; + k[5] = b.y; + k[6] = b.z; + k[7] = b.w; +#pragma unroll + for (int32_t i = 0; i < kBlockTokens; ++i) + { + bool const in = kBlockTokens * g + i < n && k[i] >= lo && static_cast(k[i]) < hi; + bool const take = in && (strict ? k[i] > threshold : k[i] >= threshold); + bool const tie = kPartialTies && in && strict && k[i] == threshold; + mask |= (take ? 1u : 0u) << i; + tieMask |= (tie ? 1u : 0u) << i; + } + } + // At most kThreads * kBlockTokens keys per pass: both counts fit 16 bits. + uint32_t const c = static_cast(__popc(mask)) | (static_cast(__popc(tieMask)) << 16); + uint32_t chunkTaken; + uint32_t const before = blockExclusiveScan(c, s.warpSums, chunkTaken); + uint32_t pos = takenMain + (before & 0xFFFFu); + uint32_t tiePos = takenTies + (before >> 16); + takenMain += chunkTaken & 0xFFFFu; + takenTies += chunkTaken >> 16; + if ((mask | tieMask) != 0u) + { + int32_t const colBase = g * kPageTokens + ((src + g) % kSourcesPerRank) * kBlockTokens; +#pragma unroll + for (int32_t i = 0; i < kBlockTokens; ++i) + { + uint32_t slot = kEmptyWord; + if ((mask >> i) & 1u) + { + slot = pos++; + } + else if constexpr (kPartialTies) + { + if ((tieMask >> i) & 1u) + { + slot = tiePos < tieBudget ? tieBase + tiePos : kEmptyWord; + ++tiePos; + } + } + if (slot != kEmptyWord) + { + uint32_t const token = localToGlobal(colBase + i, rank, world); + if (stage != nullptr) + { + stage[slot] = make_uint2(k[i], token); + } + else + { + store2(mcSlot + kSlotHeaderWords + 2 * static_cast(slot), k[i], token); + } + } + } + } + // The staged pairs are complete, and the scan's warp sums may be rewritten. + __syncthreads(); + } + stamp(dbg, 43); + if (stage != nullptr) + { + // Two pairs per 16-byte store; an odd count leaves the last half of the last vector empty. + int32_t const vectors = static_cast(sent + 1u) / 2; + for (int32_t v = threadIdx.x; v < vectors; v += kThreads) + { + uint2 const a = stage[2 * v]; + uint2 const b + = 2 * v + 1 < static_cast(sent) ? stage[2 * v + 1] : make_uint2(kEmptyWord, kEmptyWord); + store4(mcSlot + kSlotHeaderWords + 4 * static_cast(v), make_uint4(a.x, a.y, b.x, b.y)); + } + } + // Receivers check every candidate word, so the header needs no ordering against the candidates. + if constexpr (kPartialTies) + { + if (threadIdx.x == kThreads - 1) + { + store2(mcSlot, sent, bound); + } + } + else + { + // The statistics of the keys sent, from the staged pairs (round 1 always stages), by the last warp, which + // stores no vector (at most kSourceCapacity / 2), while the first warps store the vectors. + static_assert(kSourceCapacity / 2 <= kThreads - 32, "the last warp stores no candidate vector"); + if (threadIdx.x >= kThreads - 32) + { + int32_t const lane = threadIdx.x % 32; + KeyStats t; +#pragma unroll + for (int32_t j = 0; j < kSourceCapacity / 32; ++j) + { + int32_t const i = lane + 32 * j; + if (i < static_cast(sent)) + { + t.add(stage[i].x); + } + } + uint32_t const maxKey = __reduce_max_sync(0xFFFFFFFFu, t.maxKey); + uint32_t const minKey = __reduce_min_sync(0xFFFFFFFFu, t.minKey); + uint32_t const positives = __reduce_add_sync(0xFFFFFFFFu, t.positives); + uint32_t const maxNeg = __reduce_max_sync(0xFFFFFFFFu, t.maxNeg); + uint32_t const minPos = __reduce_min_sync(0xFFFFFFFFu, t.minPos); + if (lane == 31) + { + store4(mcSlot, make_uint4(sent, bound, maxKey, minKey + 1u)); + store4(mcSlot + 4, make_uint4(positives, maxNeg, minPos + 1u, 0u)); + } + } + } + stamp(dbg, 44); + return sent; +} + +// Load one source's keys (one block of kBlockTokens tokens of every local page) from the local logits into the +// chunk; returns the number of keys. Consecutive threads read consecutive keys (whole 32-byte blocks per eight +// threads), and each thread issues kLoadBatch loads before it uses any. With `withStats`, the per-warp key +// statistics are stored for readStats after the next block barrier. +__device__ int32_t loadSource( + SmemLayout const& s, float const* rowLogits, int32_t localLen, int32_t src, bool withStats) +{ + constexpr int32_t kLoadBatch = 8; + int32_t const n = sourceLength(localLen, src); + KeyStats st; + for (int32_t j0 = threadIdx.x; j0 < n; j0 += kLoadBatch * kThreads) + { + float v[kLoadBatch]; +#pragma unroll + for (int32_t i = 0; i < kLoadBatch; ++i) + { + int32_t const j = j0 + i * kThreads; + int32_t const page = j / kBlockTokens; + int32_t const col = page * kPageTokens + ((src + page) % kSourcesPerRank) * kBlockTokens + j % kBlockTokens; + v[i] = j < n ? rowLogits[col] : 0.F; + } +#pragma unroll + for (int32_t i = 0; i < kLoadBatch; ++i) + { + int32_t const j = j0 + i * kThreads; + if (j < n) + { + uint32_t const k = orderedKey(v[i]); + s.chunk[j] = k; + st.add(k); + } + } + } + if (withStats) + { + storeWarpStats(st, s.stats); + } + return n; +} + +// Whether source q withheld keys that may lie above the merged k-th key. +__device__ __forceinline__ bool sourceOverflows(SmemLayout const& s, int32_t q, uint32_t kth) +{ + uint32_t const cnt = s.scal[kSlotSrcCount + q]; + uint64_t const bound = s.scal[kSlotSrcBound + q]; + return cnt < s.scal[kSlotSrcLen + q] && bound > uint64_t{kth} + 1u; +} + +// Emit the top-kTopK of the merge candidates. Keys above `kth` are all taken; ties at `kth` are taken in candidate +// order when only some of them fit (deterministic on every rank), otherwise all of them. With `takeAll`, every +// candidate is taken and the row is padded with -1. The row is assembled in shared memory (the first histogram) +// and written with coalesced stores. +template +__device__ bool emitSelection(SmemLayout const& s, KeyAt const& keys, int32_t total, uint32_t kth, bool takeAll, + int32_t* outRow, int32_t overflowSources, int64_t* dbg = nullptr) +{ + uint32_t const want = static_cast(kTopK); + // Positions from one block scan over contiguous candidate ranges (no contended counter); the order is the + // candidate order, identical on every rank. Shared-memory candidates are read four at a time. + int32_t per = (total + kThreads - 1) / kThreads; + if constexpr (KeyAt::kVector) + { + per = (per + 3) / 4 * 4; + } + int32_t const j0 = min(total, static_cast(threadIdx.x) * per); + int32_t const j1 = min(total, j0 + per); + uint32_t cGt = 0; + uint32_t cEq = 0; + if constexpr (KeyAt::kVector) + { + for (int32_t j = j0; j < j1; j += 4) + { + uint4 const q = keys.keys4(j); + uint32_t const kk[4] = {q.x, q.y, q.z, q.w}; +#pragma unroll + for (int32_t i = 0; i < 4; ++i) + { + bool const in = j + i < j1; + cGt += (in && (takeAll || kk[i] > kth)) ? 1u : 0u; + cEq += (in && !takeAll && kk[i] == kth) ? 1u : 0u; + } + } + } + else + { + for (int32_t j = j0; j < j1; ++j) + { + uint32_t const k = keys(j); + cGt += (takeAll || k > kth) ? 1u : 0u; + cEq += (!takeAll && k == kth) ? 1u : 0u; + } + } + // The first `overflowSources` threads flag a source that withheld keys above the threshold (the flag slot is + // zero on entry); with a flag set, nothing is written and the caller runs the second round. + if (static_cast(threadIdx.x) < overflowSources && sourceOverflows(s, threadIdx.x, kth)) + { + atomicOr(&s.scal[kSlotCounter2], 1u); + } + uint32_t allGt; + uint32_t allEq; + uint32_t posGt; + uint32_t posEq; + if (total < 0x10000) + { + // Both counts fit 16 bits: one scan of the packed pair. + uint32_t packedTotal; + uint32_t const packed = cGt | (cEq << 16); + uint32_t const before = blockExclusiveScan(packed, s.warpSums, packedTotal); + allGt = packedTotal & 0xFFFFu; + allEq = packedTotal >> 16; + posGt = before & 0xFFFFu; + posEq = allGt + (before >> 16); + } + else + { + posGt = blockInclusiveScan(cGt, s.warpSums, allGt) - cGt; + __syncthreads(); + posEq = blockInclusiveScan(cEq, s.warpSums, allEq) - cEq; + posEq += allGt; + } + stamp(dbg, 59); + if (overflowSources > 0 && s.scal[kSlotCounter2] != 0u) + { + return true; + } + uint32_t* row = s.hist; + auto place = [&](uint32_t k, uint32_t idx) + { + if (takeAll || k > kth) + { + if (posGt < want) + { + row[posGt] = idx; + } + ++posGt; + } + else if (k == kth) + { + if (posEq < want) + { + row[posEq] = idx; + } + ++posEq; + } + }; + if constexpr (KeyAt::kVector) + { + for (int32_t j = j0; j < j1; j += 4) + { + uint4 const q = keys.keys4(j); + uint4 const x = keys.index4(j); + uint32_t const kk[4] = {q.x, q.y, q.z, q.w}; + uint32_t const xx[4] = {x.x, x.y, x.z, x.w}; +#pragma unroll + for (int32_t i = 0; i < 4; ++i) + { + if (j + i < j1) + { + place(kk[i], xx[i]); + } + } + } + } + else + { + for (int32_t j = j0; j < j1; ++j) + { + place(keys(j), keys.index(j)); + } + } + __syncthreads(); + stamp(dbg, 60); + uint32_t const selected = min(allGt + allEq, want); + for (uint32_t j = threadIdx.x; j < want; j += kThreads) + { + outRow[j] = j < selected ? static_cast(row[j]) : -1; + } + stamp(dbg, 61); + return false; +} + +// Threshold of an exact (cap = 0) merge select: keys above `kth` are taken, ties at `kth` while they fit. +__device__ __forceinline__ uint32_t mergeThreshold(SelectOut const& m, bool& takeAll) +{ + takeAll = m.all || (m.full && m.edge == 0u); + if (takeAll) + { + return 0u; + } + // A wholly taken bin: the keys at or above its edge are exactly kTopK. + return m.full ? m.edge - 1u : m.edge; +} + +// Reset of the previous exchange launch's buffer on this rank's copy. Its 2 * kMaxSources * kMaxRows slots (round 1, +// then round 2) are spread over the kResetWarps reset warps (the warps from kFirstResetWarp on) of every CTA of the +// launch, at most ten consecutive slots per warp, one per lane: each lane fetches its slot's header once the epoch is +// known (fetchResetHeaders, after the grid dependency wait and the first block barrier; an asynchronous copy into +// shared memory that completes during the local select and the send) and the warp resets the written slots while the +// poll warps wait for the candidates (resetSlots). A slot was written iff its header was; the header's count bounds +// the candidates to reset. +constexpr int32_t kResetSlots = 2 * kMaxSources * kMaxRows; + +// This CTA's index among the launch's clearers, and their number (from the grid; recomputed where used). +__device__ __forceinline__ int32_t resetClearer() +{ + return static_cast(blockIdx.y) * kSourcesPerRank + static_cast(blockIdx.x); +} + +__device__ __forceinline__ int32_t resetClearers() +{ + return static_cast(gridDim.y) * kSourcesPerRank; +} + +__device__ __forceinline__ int32_t resetSlotsPerWarp(int32_t numClearers) +{ + int32_t const warps = numClearers * kResetWarps; + return (kResetSlots + warps - 1) / warps; +} + +// First slot of this (reset) warp. +__device__ __forceinline__ int32_t resetFirstSlot(int32_t clearer, int32_t numClearers) +{ + int32_t const warp = static_cast(threadIdx.x) / 32 - kFirstResetWarp; + return (clearer * kResetWarps + warp) * resetSlotsPerWarp(numClearers); +} + +__device__ __forceinline__ int64_t resetSlotOffset(int32_t buf, int32_t slot) +{ + int32_t const round1Slots = kMaxSources * kMaxRows; + bool const second = slot >= round1Slots; + int32_t const qr = second ? slot - round1Slots : slot; + return second ? round2Offset(buf, qr / kMaxRows, qr % kMaxRows) : round1Offset(buf, qr / kMaxRows, qr % kMaxRows); +} + +__device__ __forceinline__ uint32_t* resetSlotBase(CpTopkParams const& p, int32_t buf, int32_t slot) +{ + return p.ucWorkspace + resetSlotOffset(buf, slot); +} + +//! Most slots one reset warp resets (one row: the fewest reset warps). +constexpr int32_t kMaxResetSlotsPerWarp + = (kResetSlots + kSourcesPerRank * kResetWarps - 1) / (kSourcesPerRank * kResetWarps); +static_assert(kMaxResetSlotsPerWarp <= 32, "one slot header per lane"); + +// Shared-memory home of the reset warps' slot headers (16 bytes per slot: count, bound and two further header words), +// in the merge candidate area beyond the round-1 staging of the send; it is free from the first block barrier until +// the reset. +constexpr int32_t kResetHeaderOffset = 2 * kSourceCapacity; +static_assert(kResetHeaderOffset + 4 * kMaxResetSlotsPerWarp * kResetWarps <= kSourcesPerRank * kSourceCapacity, + "the reset headers fit the smallest merge candidate area"); + +__device__ __forceinline__ uint4* resetHeaderHome(SmemLayout const& s) +{ + int32_t const warp = static_cast(threadIdx.x) / 32 - kFirstResetWarp; + return reinterpret_cast(s.mkey + kResetHeaderOffset) + warp * kMaxResetSlotsPerWarp; +} + +// Word offset, within one Lamport buffer, of the slot whose header this lane fetches for the reset; -1 for the lanes +// and warps without one. Arithmetic on the grid only (no memory access). +__device__ __forceinline__ int32_t resetHeaderOffset() +{ + int32_t const numClearers = resetClearers(); + int32_t const lane = threadIdx.x % 32; + if (static_cast(threadIdx.x) / 32 < kFirstResetWarp || lane >= resetSlotsPerWarp(numClearers)) + { + return -1; + } + int32_t const slot = resetFirstSlot(resetClearer(), numClearers) + lane; + if (slot >= kResetSlots) + { + return -1; + } + static_assert(kBufferWords <= int64_t{1} << 31, "slot offsets within a buffer fit int32"); + return static_cast(resetSlotOffset(0, slot)); +} + +// Starts the copy of this lane's reset slot header of buffer `buf` (offset from resetHeaderOffset) into the warp's +// home with an asynchronous copy (cp.async, generic proxy, completed in resetSlots): a pending copy holds no register +// and no block barrier waits for it. Lanes without a slot do nothing. +__device__ __forceinline__ void fetchResetHeader( + CpTopkParams const& p, SmemLayout const& s, int32_t buf, int32_t offset) +{ + if (offset >= 0) + { + uint32_t const dst = static_cast( + __cvta_generic_to_shared(resetHeaderHome(s) + static_cast(threadIdx.x % 32))); + uint32_t const* const src = p.ucWorkspace + int64_t{buf} * kBufferWords + offset; + asm volatile("cp.async.cg.shared.global [%0], [%1], 16;" ::"r"(dst), "l"(src) : "memory"); + } +} + +// Reset this reset warp's slots of buffer `buf` (headers fetched by fetchResetHeaders); other warps return. +__device__ void resetSlots(CpTopkParams const& p, SmemLayout const& s, int32_t buf) +{ + if (static_cast(threadIdx.x) / 32 < kFirstResetWarp) + { + return; + } + int32_t const clearer = resetClearer(); + int32_t const numClearers = resetClearers(); + // Each lane completes its own header copy; the warp barrier then makes every lane's header visible to the warp. + asm volatile("cp.async.wait_all;" ::: "memory"); + __syncwarp(); + int32_t const per = resetSlotsPerWarp(numClearers); + int32_t const lane = threadIdx.x % 32; + int32_t const first = resetFirstSlot(clearer, numClearers); + uint4 const* const headers = resetHeaderHome(s); + uint4 const empty = make_uint4(kEmptyWord, kEmptyWord, kEmptyWord, kEmptyWord); + for (int32_t i = 0; i < per && first + i < kResetSlots; ++i) + { + uint4 const h = headers[i]; + if (h.x == kEmptyWord && h.y == kEmptyWord) + { + continue; + } + int32_t const slot = first + i; + uint32_t* base = resetSlotBase(p, buf, slot); + uint32_t const capacity = slot >= kMaxSources * kMaxRows ? kRound2Capacity : kSourceCapacity; + uint32_t const count = h.x == kEmptyWord ? capacity : min(h.x, capacity); + int32_t const vectors = static_cast(count + 1u) / 2; + for (int32_t v = lane; v < vectors; v += 32) + { + store4(base + kSlotHeaderWords + 4 * static_cast(v), empty); + } + if (lane < 2) + { + store4(base + 4 * lane, empty); + } + } +} + +// Merge of one row on this rank (one CTA): gathers every source's round-1 candidates, selects the exact +// top-kTopK, runs the second round when a source withheld keys above the threshold, and writes the row. +__device__ void mergeRow(CpTopkParams const& p, SmemLayout const& s, uint32_t const** segSlots, uint32_t* segBegin, + int32_t buf, int32_t row, int32_t rowLen, int32_t localLen, float const* rowLogits, int32_t* outRow, + WaitCtx const& wait) +{ + int32_t const world = p.worldSize; + int32_t const rank = p.rank; + int32_t const sources = numSources(world); + uint32_t const cap = static_cast(roundCap(world)); + __syncthreads(); + if (threadIdx.x == 0) + { + s.scal[kSlotCounter2] = 0u; + } + if (static_cast(threadIdx.x) < sources) + { + // Keys each source holds, computed while its header is in flight. + int32_t const q = threadIdx.x; + s.scal[kSlotSrcLen + q] = static_cast( + sourceLength(min(localLength(rowLen, q / kSourcesPerRank, world), p.localCapacity), q % kSourcesPerRank)); + uint2 const h = pollPair(p.ucWorkspace + round1Offset(buf, threadIdx.x, row), wait, kWaitRound1Header); + s.scal[kSlotSrcCount + threadIdx.x] = min(h.x, cap); + s.scal[kSlotSrcBound + threadIdx.x] = h.y; + } + __syncthreads(); + if (threadIdx.x < 32) + { + // Exclusive prefix of the source counts (at most 64 sources: two per lane). + uint32_t const a = static_cast(threadIdx.x) < sources ? s.scal[kSlotSrcCount + threadIdx.x] : 0u; + uint32_t const b + = static_cast(threadIdx.x) + 32 < sources ? s.scal[kSlotSrcCount + threadIdx.x + 32] : 0u; + uint32_t x = a; + uint32_t z = b; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + uint32_t const w = __shfl_up_sync(0xFFFFFFFFu, z, o); + if (static_cast(threadIdx.x) >= o) + { + x += y; + z += w; + } + } + uint32_t const totalA = __shfl_sync(0xFFFFFFFFu, x, 31); + s.scal[kSlotSrcOffset + threadIdx.x] = x - a; + s.scal[kSlotSrcOffset + 32 + threadIdx.x] = totalA + z - b; + if (threadIdx.x == 31) + { + s.scal[kSlotSrcOffset + kMaxSources] = totalA + z; + } + } + __syncthreads(); + int32_t const total = static_cast(s.scal[kSlotSrcOffset + kMaxSources]); + markPhase(p, 4); + + // Candidates: padded (source, vector) positions; issue a batch of independent 16-byte loads, then re-poll the + // ones not yet written. The key statistics are tracked for the merge select. + int32_t const vectorsPerSource = static_cast(cap) / 2; + int32_t const padded = sources * vectorsPerSource; + KeyStats st; +#pragma unroll 1 + for (int32_t v0 = 0; v0 < padded; v0 += kThreads * kPollBatch) + { + uint4 v[kPollBatch]; +#pragma unroll + for (int32_t i = 0; i < kPollBatch; ++i) + { + int32_t const jv = v0 + i * kThreads + static_cast(threadIdx.x); + int32_t const q = jv / vectorsPerSource; + int32_t const kv = jv % vectorsPerSource; + v[i] = make_uint4(0u, 0u, 0u, 0u); + if (jv < padded && 2 * kv < static_cast(s.scal[kSlotSrcCount + q])) + { + v[i] = loadVolatile4(p.ucWorkspace + round1Offset(buf, q, row) + kSlotHeaderWords + 4 * kv); + } + } +#pragma unroll + for (int32_t i = 0; i < kPollBatch; ++i) + { + int32_t const jv = v0 + i * kThreads + static_cast(threadIdx.x); + int32_t const q = jv / vectorsPerSource; + int32_t const kv = jv % vectorsPerSource; + if (jv < padded && 2 * kv < static_cast(s.scal[kSlotSrcCount + q])) + { + bool const second = 2 * kv + 1 < static_cast(s.scal[kSlotSrcCount + q]); + if (!vectorReady(v[i], second)) + { + v[i] = pollVector(p.ucWorkspace + round1Offset(buf, q, row) + kSlotHeaderWords + 4 * kv, second, + wait, kWaitRound1Candidates); + } + int32_t const j = static_cast(s.scal[kSlotSrcOffset + q]) + 2 * kv; + s.mkey[j] = v[i].x; + s.midx[j] = v[i].y; + st.add(v[i].x); + if (second) + { + s.mkey[j + 1] = v[i].z; + s.midx[j + 1] = v[i].w; + st.add(v[i].z); + } + } + } + } + stamp(debugSlots(p, 0), 45); + storeWarpStats(st, s.stats); + __syncthreads(); + markPhase(p, 5); + + // Digits of the merge select span the candidates of the sign the cut lies in (see kPositiveKey). + SignRange const r = total > 0 ? signRange(readStats(s.stats), static_cast(kTopK)) + : SignRange{0u, uint64_t{kEmptyWord}, 32, 0u, false}; + SmemKeys const merged{s.mkey, s.midx}; + SelectOut const m = blockSelect( + s, merged, total, r.lo, r.hi, static_cast(kTopK), 0u, r.lo, r.bits, debugSlots(p, 48), r.aboveHi); + putDebug(debugSlots(p, 0), 62, total); + putDebug(debugSlots(p, 0), 63, m.passes + (r.negative ? 10 : 0)); + bool takeAll = false; + uint32_t const kth = mergeThreshold(m, takeAll); + markPhase(p, 6); + stamp(debugSlots(p, 0), 58); + if (!emitSelection(s, merged, total, kth, takeAll, outRow, sources, debugSlots(p, 0))) + { + return; + } + + // ---- Round 2 (rare; every rank takes it): withheld keys in [kth, bound) of overflowing sources ---------- + __syncthreads(); + // 1. This rank's overflowing sources send their top-kRound2Capacity withheld keys. + for (int32_t c = 0; c < kSourcesPerRank; ++c) + { + int32_t const q = rank * kSourcesPerRank + c; + if (!sourceOverflows(s, q, kth)) + { + continue; + } + uint64_t const b = s.scal[kSlotSrcBound + q]; + int32_t const nq = loadSource(s, rowLogits, localLen, c, false); + __syncthreads(); + SelectOut const sel2 = blockSelect( + s, SmemKeys{s.chunk, nullptr}, nq, kth, b, static_cast(kRound2Capacity), kRound2Capacity, 0u, 32); + (void) sendSource( + s, sel2, nq, kth, b, kRound2Capacity, p.mcWorkspace + round2Offset(buf, q, row), c, rank, world, nullptr); + __syncthreads(); + } + // 2. Gather the round-2 segments of every overflowing source (all ranks). + if (threadIdx.x == 0) + { + int32_t segs = 0; + uint32_t off = 0; + for (int32_t q = 0; q < sources; ++q) + { + if (!sourceOverflows(s, q, kth)) + { + continue; + } + uint32_t const* slot = p.ucWorkspace + round2Offset(buf, q, row); + uint2 const h = pollPair(slot, wait, kWaitRound2Header); + segSlots[segs] = slot; + segBegin[segs] = off; + off += min(h.x, static_cast(kRound2Capacity)); + ++segs; + } + segBegin[segs] = off; + s.scal[kSlotTotal] = static_cast(segs); + } + __syncthreads(); + int32_t const segs = static_cast(s.scal[kSlotTotal]); + int32_t const total2 = static_cast(segBegin[segs]); + MergeKeys const keys{s.mkey, s.midx, total, segSlots, segBegin, segs}; + for (int32_t k = threadIdx.x; k < total2; k += kThreads) + { + (void) pollPair(keys.pair(k), wait, kWaitRound2Candidates); + } + __syncthreads(); + SelectOut const m2 + = blockSelect(s, keys, total + total2, 0u, uint64_t{kEmptyWord}, static_cast(kTopK), 0u, 0u, 32); + bool takeAll2 = false; + uint32_t const kth2 = mergeThreshold(m2, takeAll2); + (void) emitSelection(s, keys, total + total2, kth2, takeAll2, outRow, 0); + if (threadIdx.x == 0) + { + atomicAdd(&p.state[2], 1); + } +} + +// ---- Thread-block cluster primitives (one cluster of kClusterCtas CTAs per row; cluster rank = blockIdx.x) ---------- + +__device__ __forceinline__ uint32_t smemAddr(void const* ptr) +{ + return static_cast(__cvta_generic_to_shared(ptr)); +} + +// Shared-memory window address of `ptr` (this CTA's shared memory) in CTA `rank` of the cluster. +__device__ __forceinline__ uint32_t clusterAddr(void const* ptr, int32_t rank) +{ + uint32_t remote; + asm volatile("mapa.shared::cluster.u32 %0, %1, %2;" : "=r"(remote) : "r"(smemAddr(ptr)), "r"(rank)); + return remote; +} + +// Initialize the exchange barriers and announce it to the cluster (relaxed arrive; clusterInitWait completes it +// before the first exchange). Touches shared memory only. +__device__ __forceinline__ void clusterInit(ClusterRecords& rec) +{ + if (threadIdx.x == 0) + { + asm volatile("mbarrier.init.shared::cta.b64 [%0], 1;" ::"r"(smemAddr(&rec.bar[0])) : "memory"); + asm volatile("mbarrier.init.shared::cta.b64 [%0], 1;" ::"r"(smemAddr(&rec.bar[1])) : "memory"); + asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory"); + } + asm volatile("barrier.cluster.arrive.relaxed.aligned;" ::: "memory"); +} + +__device__ __forceinline__ void clusterInitWait() +{ + asm volatile("barrier.cluster.wait.aligned;" ::: "memory"); +} + +// This CTA's single arrival on exchange barrier `bar`, expecting `bytes` from the cluster's st.async stores. +__device__ __forceinline__ void expectBytes(uint64_t* bar, uint32_t bytes) +{ + asm volatile( + "{\n\t.reg .b64 st;\n\tmbarrier.arrive.expect_tx.shared::cta.b64 st, [%0], %1;\n\t}" ::"r"(smemAddr(bar)), + "r"(bytes) + : "memory"); +} + +// Wait until the phase of exchange barrier `bar` with parity `parity` completed (every expected byte arrived). +__device__ __forceinline__ void waitExchange(uint64_t* bar, uint32_t parity) +{ + uint32_t done; + do + { + asm volatile( + "{\n\t.reg .pred p;\n\tmbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 p, [%1], %2;\n\t" + "selp.u32 %0, 1, 0, p;\n\t}" + : "=r"(done) + : "r"(smemAddr(bar)), "r"(parity) + : "memory"); + } while (done == 0u); +} + +// Store 16 / 8 bytes at `ptr` (a record of this CTA's layout) in CTA `rank`, completing them on its barrier `bar`. +__device__ __forceinline__ void sendTo(int32_t rank, void const* ptr, uint64_t const* bar, uint4 v) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.v4.b32 [%0], {%1, %2, %3, %4}, [%5];" ::"r"( + clusterAddr(ptr, rank)), + "r"(v.x), "r"(v.y), "r"(v.z), "r"(v.w), "r"(clusterAddr(bar, rank)) + : "memory"); +} + +__device__ __forceinline__ void sendTo(int32_t rank, void const* ptr, uint64_t const* bar, uint2 v) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.v2.b32 [%0], {%1, %2}, [%3];" ::"r"( + clusterAddr(ptr, rank)), + "r"(v.x), "r"(v.y), "r"(clusterAddr(bar, rank)) + : "memory"); +} + +// Exchange sequence of a row: per pass x = 2 * pass group totals and x = 2 * pass + 1 the selected group's bins. +// Every CTA sends the records of exchange x + 1 only after its own exchange x completed, so a barrier phase (and an +// alternating record set) is reused only after every CTA consumed the previous one. +__device__ __forceinline__ uint64_t* exchangeBar(ClusterRecords& rec, uint32_t x) +{ + return &rec.bar[x & 1u]; +} + +__device__ __forceinline__ uint32_t exchangeParity(uint32_t x) +{ + return (x >> 1) & 1u; +} + +// Inclusive scan over the lanes of a warp. +__device__ __forceinline__ uint32_t warpInclusiveScan(uint32_t v) +{ + int32_t const lane = threadIdx.x % 32; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, v, o); + if (lane >= o) + { + v += y; + } + } + return v; +} + +// Tallies of the cluster select over the candidates of the cluster CTAs before this one (in candidate order), kept +// by warp 0 and published in shared memory: those strictly above the selected bins of all passes so far, and those +// in the selected bin of the last pass. +struct CtaTally +{ + uint32_t aboveBefore; + uint32_t binBefore; +}; + +// Cluster radix select of the need-th largest key among the candidates of every CTA of the cluster (this thread +// holds k0 and k1; kEmptyWord is no candidate, as it lies above every range). It computes exactly what blockSelect +// with cap = 0 computes over the union of the candidates: the same digits (from `base` and topBits), the same bins +// and counts from the cluster-summed histograms, and the same stop rules. Per pass: every CTA histograms its own +// candidates and sends its totals per group of kGroupBins bins to every CTA (st.async, two groups per warp); warp 0 +// of every CTA finds, from the top, the group holding the target, sends its own counts of that group's bins to every +// CTA, and finds the bin from the eight CTAs' counts. From the same records, warp 0 also tallies the candidates of +// the CTAs before this one above the selected bins and in the last selected bin (`tally`), which give this CTA's +// emission offsets without another exchange. +__device__ SelectOut clusterSelect(SmemLayout const& s, ClusterRecords& rec, CtaTally& tally, uint32_t k0, uint32_t k1, + uint32_t lo, uint64_t hi, uint32_t need, uint32_t base, int32_t topBits, uint32_t aboveHi, int64_t* dbg) +{ + int32_t const me = blockIdx.x; + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + SelectOut out{lo, 0u, 0u, 0u, false, false, 0}; + uint32_t prefMask = 0; + uint32_t prefVal = 0; + uint32_t above = aboveHi; + int32_t remaining = max(topBits, 1); + uint32_t* hist = s.hist; + uint32_t* histNext = s.hist + kBins; + // Warp 0: the running tallies of the CTAs before this one. + uint32_t aboveBefore = 0u; + uint32_t binBefore = 0u; + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, remaining)); b += kThreads) + { + hist[b] = 0; + } + __syncthreads(); +#pragma unroll 1 + for (int32_t pass = 0; remaining > 0; ++pass) + { + int32_t const width = min(kDigitBits, remaining); + int32_t const shift = remaining - width; + int32_t const nbins = 1 << width; + uint32_t const binMask = static_cast(nbins - 1); + int32_t const groups = (nbins + kGroupBins - 1) / kGroupBins; + int32_t const pairs = (groups + 1) / 2; + uint32_t const xg = 2u * pass; + uint32_t const xb = xg + 1u; + out.passes = pass + 1; + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[1 + 3 * pass] = clock64(); + } + if (threadIdx.x == 0) + { + expectBytes(exchangeBar(rec, xg), kClusterCtas * pairs * 8); + } + // The next pass's histogram: its last reads (the previous pass's bins sent by warp 0) are complete. + if (shift > 0) + { + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, shift)); b += kThreads) + { + histNext[b] = 0; + } + } + histogramKey(hist, true, k0, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, true, k1, lo, hi, base, prefMask, prefVal, shift, binMask); + __syncthreads(); + uint32_t(*const groupRec)[kMaxGroups] = rec.groups[pass & 1]; + uint32_t(*const binRec)[kGroupBins] = rec.bins[pass & 1]; + if (warp < pairs) + { + int32_t const b0 = 2 * warp * kGroupBins + lane; + int32_t const b1 = b0 + kGroupBins; + uint32_t const t0 = __reduce_add_sync(0xFFFFFFFFu, b0 < nbins ? hist[b0] : 0u); + uint32_t const t1 = __reduce_add_sync(0xFFFFFFFFu, b1 < nbins ? hist[b1] : 0u); + if (lane < kClusterCtas) + { + sendTo(lane, &groupRec[me][2 * warp], exchangeBar(rec, xg), make_uint2(t0, t1)); + } + } + uint32_t const target = need - above; + if (warp == 0) + { + waitExchange(exchangeBar(rec, xg), exchangeParity(xg)); + if (dbg != nullptr && lane == 0 && pass < 3) + { + dbg[2 + 3 * pass] = clock64(); + } + // Lane l sums the groups at positions 2l and 2l + 1 from the top over the cluster. + int32_t const ga = groups - 1 - 2 * lane; + int32_t const gb = ga - 1; + uint32_t a = 0; + uint32_t b = 0; + uint32_t beforeA = 0; + uint32_t beforeB = 0; +#pragma unroll + for (int32_t c = 0; c < kClusterCtas; ++c) + { + uint32_t const x = ga >= 0 ? groupRec[c][ga] : 0u; + uint32_t const y = gb >= 0 ? groupRec[c][gb] : 0u; + a += x; + b += y; + beforeA += c < me ? x : 0u; + beforeB += c < me ? y : 0u; + } + uint32_t const incl = warpInclusiveScan(a + b); + uint32_t const before = incl - a - b; + uint32_t const total = __shfl_sync(0xFFFFFFFFu, incl, 31); + bool const holds = before < target && target <= incl; + uint32_t const holder = __ballot_sync(0xFFFFFFFFu, holds); + if (lane == 0) + { + s.scal[kSlotTotal] = total; + } + if (holder != 0u) + { + int32_t const hl = __ffs(holder) - 1; + bool const first = target <= before + a; + int32_t const gSel = __shfl_sync(0xFFFFFFFFu, first ? ga : gb, hl); + uint32_t const gAbove = __shfl_sync(0xFFFFFFFFu, first ? before : before + a, hl); + if (lane == 0) + { + expectBytes(exchangeBar(rec, xb), kClusterCtas * kGroupBins * 4); + } + // This CTA's counts of the selected group's bins to every CTA: lane l sends vector l % 8 to CTAs + // l / 8 and l / 8 + 4. + int32_t const v = lane % 8; + int32_t const b0 = gSel * kGroupBins + 4 * v; + uint4 const q = make_uint4(b0 < nbins ? hist[b0] : 0u, b0 + 1 < nbins ? hist[b0 + 1] : 0u, + b0 + 2 < nbins ? hist[b0 + 2] : 0u, b0 + 3 < nbins ? hist[b0 + 3] : 0u); + sendTo(lane / 8, &binRec[me][4 * v], exchangeBar(rec, xb), q); + sendTo(lane / 8 + 4, &binRec[me][4 * v], exchangeBar(rec, xb), q); + // While the bins travel: the earlier CTAs' candidates in the groups above the selected one (both + // groups of the lanes before the holder, and the holder's first group when it selected the second). + bool const selFirst = __shfl_sync(0xFFFFFFFFu, first, hl); + aboveBefore += __reduce_add_sync( + 0xFFFFFFFFu, lane < hl ? beforeA + beforeB : (lane == hl && !selFirst ? beforeA : 0u)); + waitExchange(exchangeBar(rec, xb), exchangeParity(xb)); + // Bins of the selected group from the top: lane l holds bin 32 * gSel + 31 - l. + int32_t const bin = gSel * kGroupBins + kGroupBins - 1 - lane; + uint32_t c = 0; + uint32_t cBefore = 0; +#pragma unroll + for (int32_t d = 0; d < kClusterCtas; ++d) + { + uint32_t const x = binRec[d][kGroupBins - 1 - lane]; + c += x; + cBefore += d < me ? x : 0u; + } + uint32_t const y = warpInclusiveScan(c); + uint32_t const acc = gAbove + y - c; + bool const found = acc < target && target <= acc + c; + if (found) + { + s.scal[kSlotBin] = static_cast(bin); + s.scal[kSlotAbove] = acc; + s.scal[kSlotBinCount] = c; + } + // The earlier CTAs' candidates in the bins above the selected one (lanes before the finder) and in + // the selected bin (the finder's). + int32_t const finder = __ffs(__ballot_sync(0xFFFFFFFFu, found)) - 1; + aboveBefore += __reduce_add_sync(0xFFFFFFFFu, lane < finder ? cBefore : 0u); + binBefore = __shfl_sync(0xFFFFFFFFu, cBefore, finder); + } + else if (lane == 0) + { + // Every key is taken (pass 0 only, the same decision in every CTA): the bin exchange still + // completes, with no bytes, so that the exchange sequence stays aligned. + expectBytes(exchangeBar(rec, xb), 0u); + waitExchange(exchangeBar(rec, xb), exchangeParity(xb)); + } + if (lane == 0) + { + tally.aboveBefore = aboveBefore; + tally.binBefore = binBefore; + } + } + __syncthreads(); + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[3 + 3 * pass] = clock64(); + } + uint32_t const total = s.scal[kSlotTotal]; + if (pass == 0) + { + out.total = total + aboveHi; + if (total + aboveHi == 0u || target > total) + { + out.all = true; + out.above = total + aboveHi; + return out; + } + } + // The scalars are next written by warp 0 after the next histogram's block barrier. + uint32_t const bin = s.scal[kSlotBin]; + above += s.scal[kSlotAbove]; + out.binCount = s.scal[kSlotBinCount]; + prefVal |= bin << shift; + prefMask |= binMask << shift; + out.edge = base + prefVal; + out.above = above; + remaining = shift; + uint32_t* const t = hist; + hist = histNext; + histNext = t; + if (remaining > 0 && above + out.binCount == need) + { + out.full = true; + return out; + } + } + return out; +} + +// Spin until the header of a round-1 slot was written and, when the slot's count covers candidate vector kv, that +// vector too; returns the header's count (clamped to `cap`) and bound. Both are polled together, so neither waits +// for the other; aborts the kernel after the timeout. +__device__ uint2 pollSlotVector(uint32_t const* slot, int32_t kv, uint32_t cap, uint4& v, WaitCtx const& w) +{ + uint32_t const* vec = slot + kSlotHeaderWords + 4 * kv; + uint2 h = loadVolatile2(slot); + v = loadVolatile4(vec); + uint64_t start = 0; + for (int32_t polls = 1;; ++polls) + { + bool const headerReady = !isEmpty(h); + uint32_t const cnt = headerReady ? min(h.x, cap) : 0u; + if (headerReady && (2u * kv >= cnt || vectorReady(v, 2u * kv + 1u < cnt))) + { + return make_uint2(cnt, h.y); + } + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, headerReady ? kWaitRound1Candidates : kWaitRound1Header); + } + } + if (!headerReady) + { + h = loadVolatile2(slot); + } + v = loadVolatile4(vec); + } +} + +// Spin until the whole 32-byte round-1 header of a slot was written; aborts the kernel after the timeout. +__device__ void pollHeader(uint32_t const* slot, uint4& a, uint4& b, WaitCtx const& w) +{ + a = loadVolatile4(slot); + b = loadVolatile4(slot + 4); + uint64_t start = 0; + for (int32_t polls = 1; !(vectorReady(a, true) && vectorReady(b, true)); ++polls) + { + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, kWaitRound1Header); + } + } + a = loadVolatile4(slot); + b = loadVolatile4(slot + 4); + } +} + +// Merge of one row by its cluster. CTA c gathers the round-1 candidates of sources [c * world, (c + 1) * world) (one +// 16-byte vector, two candidate slots, per thread of the poll warps, kept in registers: thread order is crossRankTopk's +// candidate order) while its header warps read every source's header (counts, bounds and the key statistics of the keys +// sent, so every CTA has the row's statistics and can decide every source's overflow without an exchange). The cluster +// runs crossRankTopk's merge select on the summed histograms (clusterSelect), whose records also give every CTA's +// number of keys above the threshold and of ties, and every CTA writes its selected candidates at crossRankTopk's +// positions: keys above the threshold in candidate order, then ties in candidate order while they fit, then -1. Returns +// true, writing nothing, when a source withheld keys that may lie above the threshold (the same decision on every CTA +// and rank); the row is then merged by mergeRow. +__device__ bool clusterMergeRow(CpTopkParams const& p, SmemLayout const& s, ClusterRecords& rec, CtaTally& tally, + int32_t buf, int32_t row, int32_t rowLen, int32_t* outRow, WaitCtx const& wait) +{ + int32_t const world = p.worldSize; + int32_t const sources = numSources(world); + int32_t const me = blockIdx.x; + int32_t const tid = threadIdx.x; + int32_t const lane = tid % 32; + int32_t const warp = tid / 32; + uint32_t const cap = static_cast(roundCap(world)); + int32_t const vectorsPerSource = static_cast(cap) / 2; + int32_t const q0 = me * world; + int64_t* const dbg = debugSlots(p, 0); + // This thread's candidates (kEmptyWord: none; never a key). + uint32_t k0 = kEmptyWord; + uint32_t k1 = kEmptyWord; + if (tid == 0) + { + s.scal[kSlotCounter2] = 0u; + } + if (tid < world * vectorsPerSource) + { + int32_t const q = q0 + tid / vectorsPerSource; + int32_t const kv = tid % vectorsPerSource; + uint4 v; + uint2 const h = pollSlotVector(p.ucWorkspace + round1Offset(buf, q, row), kv, cap, v, wait); + if (2u * kv < h.x) + { + k0 = v.x; + if (2u * kv + 1u < h.x) + { + k1 = v.z; + } + } + // The tokens wait in shared memory until the emission (s.midx is free in the cluster merge). + *reinterpret_cast(s.midx + 2 * tid) = make_uint2(v.y, v.w); + } + else if (warp >= kPollWarps && warp < kFirstResetWarp) + { + // Header warps: thread h reads source h's header; per source its count (clamped to the round-1 capacity), + // bound and length, and per header warp the reduced key statistics. + int32_t const q = tid - kPollWarps * 32; + KeyStats st; + if (q < sources) + { + uint4 a; + uint4 b; + pollHeader(p.ucWorkspace + round1Offset(buf, q, row), a, b, wait); + s.scal[kSlotSrcCount + q] = min(a.x, cap); + s.scal[kSlotSrcBound + q] = a.y; + s.scal[kSlotSrcLen + q] = static_cast(sourceLength( + min(localLength(rowLen, q / kSourcesPerRank, world), p.localCapacity), q % kSourcesPerRank)); + st.maxKey = a.z; + st.minKey = a.w - 1u; + st.positives = b.x; + st.maxNeg = b.y; + st.minPos = b.z - 1u; + } + uint32_t const cnt = q < sources ? s.scal[kSlotSrcCount + q] : 0u; + // Candidates and keys >= +0.0 of the cluster CTAs before this one (CTA c merges sources + // [c * world, (c + 1) * world)), of this one and of the row. + int32_t const c = q < sources ? q / world : kClusterCtas; + uint32_t const nBefore = __reduce_add_sync(0xFFFFFFFFu, c < me ? cnt : 0u); + uint32_t const nMine = __reduce_add_sync(0xFFFFFFFFu, c == me ? cnt : 0u); + uint32_t const nAll = __reduce_add_sync(0xFFFFFFFFu, cnt); + uint32_t const pBefore = __reduce_add_sync(0xFFFFFFFFu, c < me ? st.positives : 0u); + if (lane == 0) + { + atomicAdd(&s.scal[kSlotCountBefore], nBefore); + atomicAdd(&s.scal[kSlotCountMine], nMine); + atomicAdd(&s.scal[kSlotCountTotal], nAll); + atomicAdd(&s.scal[kSlotPositivesBefore], pBefore); + } + uint32_t const v0 = __reduce_max_sync(0xFFFFFFFFu, st.maxKey); + uint32_t const v1 = __reduce_min_sync(0xFFFFFFFFu, st.minKey); + uint32_t const v2 = __reduce_add_sync(0xFFFFFFFFu, st.positives); + uint32_t const v3 = __reduce_max_sync(0xFFFFFFFFu, st.maxNeg); + uint32_t const v4 = __reduce_min_sync(0xFFFFFFFFu, st.minPos); + if (lane == 0) + { + // In the scan scratch (s.stats may still be read by the send's last warp; s.warpSums is next written + // by the emission scan, after the reads below). + uint32_t* const out = s.warpSums + kNumKeyStats * (warp - kPollWarps); + out[0] = v0; + out[1] = v1; + out[2] = v2; + out[3] = v3; + out[4] = v4; + } + } + stamp(dbg, 45); + __syncthreads(); + markPhase(p, 4); + + // Statistics of the row's candidates from every source's header (identical in every CTA and rank). + KeyStats g; +#pragma unroll + for (int32_t w = 0; w < kHeaderWarps; ++w) + { + uint32_t const* const in = s.warpSums + kNumKeyStats * w; + g.maxKey = max(g.maxKey, in[0]); + g.minKey = min(g.minKey, in[1]); + g.positives += in[2]; + g.maxNeg = max(g.maxNeg, in[3]); + g.minPos = min(g.minPos, in[4]); + } + uint32_t const total = s.scal[kSlotCountTotal]; + stamp(dbg, 61); + markPhase(p, 5); + + SignRange const r + = total > 0u ? signRange(g, static_cast(kTopK)) : SignRange{0u, uint64_t{kEmptyWord}, 32, 0u, false}; + clusterInitWait(); + SelectOut const m = clusterSelect( + s, rec, tally, k0, k1, r.lo, r.hi, static_cast(kTopK), r.lo, r.bits, r.aboveHi, debugSlots(p, 48)); + putDebug(debugSlots(p, 48), 0, s.scal[kSlotCountMine]); + putDebug(dbg, 62, total); + putDebug(dbg, 63, m.passes + (r.negative ? 10 : 0)); + bool takeAll = false; + uint32_t const kth = mergeThreshold(m, takeAll); + markPhase(p, 6); + + // Keys above the threshold and ties of the cluster CTAs before this one and of the row (identical in every CTA), + // from the select's tallies; with the threshold below a wholly taken bin the ties fall beyond kTopK and are taken + // as keys above it. + uint32_t gtBefore; + uint32_t eqBefore; + uint32_t allGt; + uint32_t allEq; + if (takeAll) + { + gtBefore = s.scal[kSlotCountBefore]; + eqBefore = 0u; + allGt = total; + allEq = 0u; + } + else + { + uint32_t const aboveBefore = tally.aboveBefore + (r.aboveHi > 0u ? s.scal[kSlotPositivesBefore] : 0u); + gtBefore = m.full ? aboveBefore + tally.binBefore : aboveBefore; + eqBefore = m.full ? 0u : tally.binBefore; + allGt = m.full ? m.above + m.binCount : m.above; + allEq = m.full ? 0u : m.binCount; + } + // This CTA's positions, scanned in thread order; a source of the row that withheld keys above the threshold + // sends the row to the second round. + bool const gt0 = k0 != kEmptyWord && (takeAll || k0 > kth); + bool const eq0 = k0 != kEmptyWord && !takeAll && k0 == kth; + bool const gt1 = k1 != kEmptyWord && (takeAll || k1 > kth); + bool const eq1 = k1 != kEmptyWord && !takeAll && k1 == kth; + if (tid < sources && sourceOverflows(s, tid, kth)) + { + atomicOr(&s.scal[kSlotCounter2], 1u); + } + // Exclusive prefix in thread order (k0 before k1): within the warp from ballots, across warps from the warp + // totals (one block barrier). + uint32_t const below = (1u << lane) - 1u; + uint32_t const bGt0 = __ballot_sync(0xFFFFFFFFu, gt0); + uint32_t const bGt1 = __ballot_sync(0xFFFFFFFFu, gt1); + uint32_t const bEq0 = __ballot_sync(0xFFFFFFFFu, eq0); + uint32_t const bEq1 = __ballot_sync(0xFFFFFFFFu, eq1); + if (lane == 0) + { + s.warpSums[warp] = static_cast(__popc(bGt0) + __popc(bGt1)) + + (static_cast(__popc(bEq0) + __popc(bEq1)) << 16); + } + __syncthreads(); + uint32_t const warpsBefore = __reduce_add_sync(0xFFFFFFFFu, lane < warp ? s.warpSums[lane] : 0u); + uint32_t const before = warpsBefore + static_cast(__popc(bGt0 & below) + __popc(bGt1 & below)) + + (static_cast(__popc(bEq0 & below) + __popc(bEq1 & below)) << 16); + stamp(dbg, 58); + if (s.scal[kSlotCounter2] != 0u) + { + return true; + } + uint32_t const want = static_cast(kTopK); + uint32_t posGt = gtBefore + (before & 0xFFFFu); + uint32_t posEq = allGt + eqBefore + (before >> 16); + uint32_t x0 = 0u; + uint32_t x1 = 0u; + if (k0 != kEmptyWord) + { + uint2 const x = *reinterpret_cast(s.midx + 2 * tid); + x0 = x.x; + x1 = x.y; + } + if (gt0 && posGt < want) + { + outRow[posGt] = static_cast(x0); + } + posGt += gt0 ? 1u : 0u; + if (eq0 && posEq < want) + { + outRow[posEq] = static_cast(x0); + } + posEq += eq0 ? 1u : 0u; + if (gt1 && posGt < want) + { + outRow[posGt] = static_cast(x1); + } + if (eq1 && posEq < want) + { + outRow[posEq] = static_cast(x1); + } + uint32_t const selected = min(allGt + allEq, want); + for (uint32_t j = selected + static_cast(me * kThreads + tid); j < want; j += kClusterCtas * kThreads) + { + outRow[j] = -1; + } + stamp(dbg, 60); + return false; +} + +__global__ void __launch_bounds__(kThreads, 1) cpTopkZ9Kernel(CpTopkParams p) +{ + markPhase(p, 0); + extern __shared__ __align__(16) uint32_t smemRaw[]; + __shared__ uint32_t const* segSlots[kMaxSources]; + __shared__ uint32_t segBegin[kMaxSources + 1]; + __shared__ __align__(16) ClusterRecords rec; + __shared__ CtaTally tally; + __shared__ int32_t epochShared; + int32_t const world = p.worldSize; + int32_t const rank = p.rank; + int32_t const src = blockIdx.x; + int32_t const row = blockIdx.y; + SmemLayout const s = carveSmem(smemRaw, p.localCapacity, world); + int32_t* outRow = p.out + static_cast(row) * kTopK; + clusterInit(rec); + if (threadIdx.x <= kSlotPositivesBefore - kSlotCountBefore) + { + s.scal[kSlotCountBefore + threadIdx.x] = 0u; + } + int32_t const resetOffset = resetHeaderOffset(); + // Every global read (lengths, logits, the epoch word, the workspace) comes after the grid dependency wait, so a + // programmatic early launch is safe whatever the preceding kernels write. + asm volatile("griddepcontrol.wait;" ::: "memory"); + markPhase(p, 10); + // Thread 0 and the reset warps read this CTA's epoch word, written only by the previous exchange launch of this + // op, before the key loads; nothing waits for the value until the key loads are done. Thread 0 publishes it to the + // CTA at the block barrier that follows the key loads, and the reset warps start the copies of the previous + // buffer's headers right before that barrier (slot offsets computed before the wait, from the grid only). + int32_t* const epochWord = p.state + kEpochBase + row * kSourcesPerRank + src; + int32_t epochRead = 0; + if (threadIdx.x == 0 || static_cast(threadIdx.x) >= kFirstResetWarp * 32) + { + epochRead = *reinterpret_cast(epochWord); + } + int32_t const prevBufRead = (epochRead + kNumLamportBuffers - 1) % kNumLamportBuffers; + int32_t const kvLen = p.kvLens[0]; + int32_t const rowLen = max(0, kvLen - p.nextN + row + 1); + // Identity launch (every row has at most kTopK tokens; the same decision on every rank): no exchange, and + // neither the workspace nor the epoch words are touched, so identity launches never shorten the reuse chain of + // the Lamport buffers (see crossRankTopkClusterMerge.h). + bool const exchange = kvLen - p.nextN + p.numRows > kTopK; + // Thread 0 advances this CTA's epoch word (the CTAs of the last row also those of the rows this launch does not + // have, so every word counts every exchange launch), after a block barrier that follows every read of the word in + // this CTA. Nothing else of this launch reads the epoch words, and the next launch reads them only after its grid + // dependency wait. + auto advanceEpoch = [&]() + { + if (exchange && threadIdx.x == 0) + { + int32_t const rows = row == static_cast(gridDim.y) - 1 ? kMaxRows - row : 1; + for (int32_t r = 0; r < rows; ++r) + { + epochWord[r * kSourcesPerRank] = epochRead + 1; + } + } + }; + + if (rowLen <= kTopK) + { + // Every rank agrees on this branch: identity selection, no exchange for this row. + if (src == 0) + { + for (int32_t j = threadIdx.x; j < kTopK; j += kThreads) + { + outRow[j] = j < rowLen ? j : -1; + } + } + if (exchange) + { + fetchResetHeader(p, s, prevBufRead, resetOffset); + resetSlots(p, s, prevBufRead); + markPhase(p, 9); + } + __syncthreads(); + advanceEpoch(); + clusterInitWait(); + } + else + { + int32_t const localLen = min(localLength(rowLen, rank, world), p.localCapacity); + float const* rowLogits = p.logits + static_cast(row) * p.logitsStride; + + // ---- Round 1: this source's local top keys --------------------------------------------------------- + int32_t const n = loadSource(s, rowLogits, localLen, src, true); + if (threadIdx.x == 0) + { + epochShared = epochRead; + } + // The reset warps start fetching the headers of their share of the previous exchange launch's buffer (an + // exchange row implies an exchange launch); the values are first used by the reset after the send. + fetchResetHeader(p, s, prevBufRead, resetOffset); + __syncthreads(); + markPhase(p, 1); + advanceEpoch(); + uint32_t const cap = static_cast(roundCap(world)); + uint32_t const need = static_cast(roundNeed(world)); + SelectOut sel{0u, 0u, 0u, static_cast(n), true, false, 0}; + uint32_t selLo = 0u; + int32_t passInfo = 0; + if (n > static_cast(cap)) + { + // The cut lies among the keys of one sign (see kPositiveKey); the digits span that sign's key range. + SignRange const r = signRange(readStats(s.stats), need); + selLo = r.lo; + SmemKeys const keys{s.chunk, nullptr}; + sel = blockSelect(s, keys, n, r.lo, r.hi, need, cap, r.lo, r.bits, debugSlots(p, 32), r.aboveHi); + passInfo = sel.passes + (r.negative ? 10 : 0); + } + markPhase(p, 2); + int32_t const sendBuf = epochShared % kNumLamportBuffers; + uint32_t const sent = sendSource(s, sel, n, selLo, uint64_t{kEmptyWord}, cap, + p.mcWorkspace + round1Offset(sendBuf, rank * kSourcesPerRank + src, row), src, rank, world, + reinterpret_cast(s.mkey), debugSlots(p, 0)); + putDebug(debugSlots(p, 0), 46, sent); + putDebug(debugSlots(p, 0), 47, passInfo); + markPhase(p, 3); + + // The reset warps reset the previous exchange launch's buffer, whose candidates every rank has consumed (a + // peer writes it again only two exchange launches later, after this launch completed on this rank), while + // the poll and header warps wait for this launch's candidates. + resetSlots(p, s, (epochShared + kNumLamportBuffers - 1) % kNumLamportBuffers); + int32_t const buf = epochShared % kNumLamportBuffers; + if (threadIdx.x == kFirstResetWarp * 32) + { + int64_t* const d = debugSlots(p, 0); + if (d != nullptr) + { + d[9] = static_cast(globalTimerNs()); + } + } + + // ---- Merge: the row's cluster ----------------------------------------------------------------------- + WaitCtx const wait{p.state, p.timeoutNs, rank}; + if (clusterMergeRow(p, s, rec, tally, buf, row, rowLen, outRow, wait) && src == 0) + { + // A source withheld keys above the threshold: crossRankTopk's merge, with its second round, on CTA 0. + mergeRow(p, s, segSlots, segBegin, buf, row, rowLen, localLen, rowLogits, outRow, wait); + int64_t* const dbg = debugSlots(p, 0); + putDebug(dbg, 63, dbg != nullptr ? dbg[63] + 100 : 0); + } + markPhase(p, 7); + } + markPhase(p, 8); +} + +#endif + +} // namespace + +size_t cpTopkSmemBytes(int32_t localCapacity, int32_t worldSize) +{ + return smemBytes(localCapacity, worldSize); +} + +int32_t cpTopkMaxLocalCapacity(int32_t worldSize) +{ + static int32_t cached[kMaxRanks + 1] = {}; + if (worldSize < 1 || worldSize > kMaxRanks) + { + return 0; + } + if (cached[worldSize] == 0) + { + int device = 0; + cudaGetDevice(&device); + int maxSmemOptin = 0; + cudaDeviceGetAttribute(&maxSmemOptin, cudaDevAttrMaxSharedMemoryPerBlockOptin, device); + int32_t cap = 0; + for (int32_t c = 1 << 22; c >= kPageTokens * kSourcesPerRank; c -= kPageTokens * kSourcesPerRank) + { + if (smemBytes(c, worldSize) + kStaticSmemBytes <= static_cast(maxSmemOptin)) + { + cap = c; + break; + } + } + cached[worldSize] = cap; + } + return cached[worldSize]; +} + +int32_t cpTopkMaxActiveClusters(int32_t worldSize) +{ + // Queried once per world size and device (the value + 1; 0 = not queried yet). + constexpr int32_t kMaxDevices = 64; + static int32_t cached[kMaxDevices][kMaxRanks + 1] = {}; + if (worldSize < 1 || worldSize > kMaxRanks) + { + return 0; + } + int device = 0; + cudaGetDevice(&device); + if (device >= 0 && device < kMaxDevices && cached[device][worldSize] > 0) + { + return cached[device][worldSize] - 1; + } + int32_t const capacity = cpTopkMaxLocalCapacity(worldSize); + if (capacity == 0) + { + return 0; + } + size_t const smem = smemBytes(capacity, worldSize); + if (cudaFuncSetAttribute(cpTopkZ9Kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(smem)) + != cudaSuccess) + { + (void) cudaGetLastError(); + return 0; + } + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kSourcesPerRank, kMaxRows); + config.blockDim = dim3(kThreads); + config.dynamicSmemBytes = smem; + cudaLaunchAttribute attr{}; + attr.id = cudaLaunchAttributeClusterDimension; + attr.val.clusterDim.x = kSourcesPerRank; + attr.val.clusterDim.y = 1; + attr.val.clusterDim.z = 1; + config.attrs = &attr; + config.numAttrs = 1; + int clusters = 0; + if (cudaOccupancyMaxActiveClusters(&clusters, cpTopkZ9Kernel, &config) != cudaSuccess) + { + (void) cudaGetLastError(); + return 0; + } + if (device >= 0 && device < kMaxDevices) + { + cached[device][worldSize] = clusters + 1; + } + return clusters; +} + +int64_t cpTopkDefaultTimeoutMs() +{ + static int64_t const timeoutMs = []() + { + char const* env = std::getenv("TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS"); + if (env == nullptr || env[0] == '\0') + { + return kDefaultTimeoutMs; + } + char* end = nullptr; + long long const v = std::strtoll(env, &end, 10); + if (end == env || *end != '\0' || v <= 0) + { + throw std::invalid_argument(std::string("glm_moe_dsa_cross_rank_topk_cluster_merge: bad " + "TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS=") + + env); + } + return static_cast(v); + }(); + return timeoutMs; +} + +void invokeCpTopk(CpTopkParams const& inParams, cudaStream_t stream) +{ + if (inParams.numRows <= 0) + { + return; + } + CpTopkParams params = inParams; + if (params.timeoutNs == 0) + { + params.timeoutNs = static_cast(cpTopkDefaultTimeoutMs()) * 1000000ull; + } + if (params.numRows > kMaxRows || params.worldSize <= 0 || params.worldSize > kMaxRanks || params.rank < 0 + || params.rank >= params.worldSize) + { + throw std::invalid_argument("glm_moe_dsa_cross_rank_topk_cluster_merge: unsupported rows/world/rank"); + } + size_t const smem = smemBytes(params.localCapacity, params.worldSize); + // The attribute is per function and device; raise it only when a larger footprint is requested. + static thread_local size_t configuredSmem[64] = {}; + int device = 0; + cudaGetDevice(&device); + cudaError_t err = cudaSuccess; + if (device >= 64 || configuredSmem[device] < smem) + { + err = cudaFuncSetAttribute(cpTopkZ9Kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(smem)); + if (err != cudaSuccess) + { + throw std::runtime_error( + std::string("glm_moe_dsa_cross_rank_topk_cluster_merge: smem attribute: ") + cudaGetErrorString(err)); + } + if (device < 64) + { + configuredSmem[device] = smem; + } + } + // Programmatic dependent launch as for the other TensorRT-LLM kernels (on unless TRTLLM_ENABLE_PDL=0). + static bool const enablePdl = []() + { + char const* env = std::getenv("TRTLLM_ENABLE_PDL"); + return !(env != nullptr && env[0] == '0' && env[1] == '\0'); + }(); + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kSourcesPerRank, params.numRows); + config.blockDim = dim3(kThreads); + config.dynamicSmemBytes = smem; + config.stream = stream; + cudaLaunchAttribute attrs[2]; + attrs[0].id = cudaLaunchAttributeClusterDimension; + attrs[0].val.clusterDim.x = kSourcesPerRank; + attrs[0].val.clusterDim.y = 1; + attrs[0].val.clusterDim.z = 1; + attrs[1].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[1].val.programmaticStreamSerializationAllowed = enablePdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 2; + err = cudaLaunchKernelEx(&config, cpTopkZ9Kernel, params); + if (err == cudaSuccess) + { + err = cudaGetLastError(); + } + if (err != cudaSuccess) + { + throw std::runtime_error( + std::string("glm_moe_dsa_cross_rank_topk_cluster_merge: launch: ") + cudaGetErrorString(err)); + } +} + +} // namespace kernels::glm_moe_dsa::cross_rank_topk_cluster_merge + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.h new file mode 100644 index 000000000000..0f6d53a52e92 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.h @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::cross_rank_topk_cluster_merge +{ + +// Context-partitioned DSA indexer top-k with a cluster merge. +// +// It selects exactly what the kernel of glmMoeDsa/crossRankTopk.h selects, in the same output order, with the +// same context partition, exchange sources, round-1 / round-2 protocol and Lamport workspace layout: +// - global 64-token page p belongs to rank p % worldSize; within a rank, each verify row is handled by +// kSourcesPerRank CTAs, CTA s owning the 8-token block (s + page) % kSourcesPerRank of every local page +// (exchange source q = rank * kSourcesPerRank + s); +// - each source selects its local top keys with crossRankTopk's rule and broadcasts them as (order-preserving key, +// global token) pairs in source order, plus a header (count, exclusive bound of the withheld keys; this kernel adds +// the key statistics of the keys sent), through the multicast Lamport workspace. +// What differs is how the row is merged and how a launch ends: +// - the kSourcesPerRank CTAs of one row form a thread-block cluster. CTA s merges the candidates of sources +// [s * worldSize, (s + 1) * worldSize) (at 8 ranks: the 8 sources of rank s). The cluster runs crossRankTopk's +// merge select (the same digits and early-stop rules) on the cluster-summed histograms, and every CTA writes its +// selected candidates directly into the output row at crossRankTopk's positions (keys above the threshold in +// candidate order, then ties in candidate order, then -1). Each round-1 slot header carries its source's key +// statistics, so every CTA derives the row's statistics from the headers it polls; the CTAs exchange only the +// select's histogram totals, with st.async stores into each other's shared memory completed on transaction +// barriers, and derive every CTA's output counts from the per-CTA tallies of the select. The only cluster-wide +// barrier orders the barriers' initialization before the first exchange. A row in which a source withheld keys +// above the threshold (identical decision on every rank) is merged instead by CTA 0 with crossRankTopk's single-CTA +// merge, including its second round; +// - every CTA keeps its own epoch word (one per (row, source CTA) slot): its thread 0 reads the word after the grid +// dependency wait (only the previous exchange launch of this op writes it), the value is published to the CTA at +// the block barrier that follows the key loads, and the CTA advances the word with a plain store after that +// barrier (nothing else in the launch reads it; the next launch reads it after its own grid dependency wait); the +// CTAs of the last row also advance the words of the rows the launch does not have, so every word counts the +// exchange launches. There is no arrival counter and no end-of-kernel fence. +// +// Rows of at most kTopK valid tokens produce the identity selection followed by -1, without an exchange. A launch whose +// rows all have at most kTopK valid tokens (the same on every rank: the lengths come from the shared KV length) is an +// identity launch: it writes neither the workspace nor the epoch words. Every other launch is an exchange launch. +// Exchange launches rotate kNumLamportBuffers buffers by the epoch, which counts exchange launches; in exchange launch +// x every CTA resets its share of its own rank's copy of the buffer of exchange launch x - 1 between its send and its +// merge (the headers of its share are fetched once the epoch is known, after the grid dependency wait). Every rank's +// merges of x - 1 have read their copy of it, and a peer writes it again only in exchange launch x + 2, which starts +// after the peer's merges of x + 1 received this rank's sources of x + 1, i.e. after this rank completed launch x +// (identity launches in between neither write nor wait, so they do not shorten this chain). Every global memory +// access of the kernel follows its grid dependency wait, which orders it after the completion (and memory +// visibility) of the preceding kernels of the stream, including the previous launch of this op; the kernel never +// triggers its dependents early. +// +// A merge that waits longer than the timeout (CpTopkParams::timeoutNs, %globaltimer) for a peer's words records the +// wait in the state's error word and aborts the kernel with a trap: the exchange cannot complete correctly any more, +// and a late peer write could land in a buffer this rank reuses, so the run fails instead of continuing. + +//! Selected indices per verify row (DSA index_topk). +constexpr int32_t kTopK = 2048; +//! Ranks of the context-parallel group. +constexpr int32_t kMaxRanks = 8; +//! Verify rows per call. +constexpr int32_t kMaxRows = 8; +//! Tokens per indexer K page; the context is partitioned by whole pages. +constexpr int32_t kPageTokens = 64; +//! CTAs (exchange sources) per rank and verify row; also the cluster size (one cluster per row). +constexpr int32_t kSourcesPerRank = 8; +//! Exchange sources per row. +constexpr int32_t kMaxSources = kMaxRanks * kSourcesPerRank; +//! Round-1 slots per (source, row). +constexpr int32_t kSourceCapacity = 256; +//! Keys each saturated source aims to send in round 1 at 8 ranks (crossRankTopk's rule: three times the even share of +//! kTopK; refining stops at the first bin edge with at most four times the even share of keys at or above it). +constexpr int32_t kSourceBudget = 96; +//! Round-2 slots per (source, row). +constexpr int32_t kRound2Capacity = 2048; +//! Threads per CTA. +constexpr int32_t kThreads = 1024; +//! Lamport buffers rotated by the epoch. +constexpr int32_t kNumLamportBuffers = 3; +//! 32-bit word that marks an empty Lamport slot; never a valid key, token index, count or bound. +constexpr uint32_t kEmptyWord = 0xFFFFFFFFu; + +//! 32-bit words of one (source, row) slot: a 32-byte header then two words per candidate, so candidates can be written +//! and read with 16-byte vector accesses. Round-1 header: count, exclusive bound of the withheld keys, then the key +//! statistics of the keys sent (largest key, smallest key + 1, keys >= +0.0, largest key below +0.0, smallest key +//! >= +0.0 plus 1; the "+ 1" maps the "none" value 0xFFFFFFFF to 0), then 0. Round-2 header: count, bound. +constexpr int64_t kSlotHeaderWords = 8; +constexpr int64_t kRound1SlotWords = kSlotHeaderWords + 2 * int64_t{kSourceCapacity}; +constexpr int64_t kRound2SlotWords = kSlotHeaderWords + 2 * int64_t{kRound2Capacity}; +//! 32-bit words of one Lamport buffer (round-1 region, then round-2 region). +constexpr int64_t kBufferWords = int64_t{kMaxSources} * kMaxRows * (kRound1SlotWords + kRound2SlotWords); +//! 32-bit words of the whole exchange workspace; every word must start as kEmptyWord. +constexpr int64_t kWorkspaceWords = kNumLamportBuffers * kBufferWords; +//! int32 words of the per-rank device state, zero-initialized: [0, 2) unused, [2] second rounds taken, [3] error word +//! (nonzero = the wait that timed out, written right before the kernel aborts), then one epoch word per +//! (row, source CTA) slot: state[kEpochBase + row * kSourcesPerRank + s]. +constexpr int32_t kEpochBase = 4; +constexpr int32_t kStateWords = kEpochBase + kMaxRows * kSourcesPerRank; +//! Default wait for a peer's Lamport words before the kernel aborts +//! (TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS overrides it). +constexpr int64_t kDefaultTimeoutMs = 120000; + +//! Values of the error word. +enum CpTopkWait : int32_t +{ + kWaitRound1Header = 1, + kWaitRound1Candidates = 2, + kWaitRound2Header = 3, + kWaitRound2Candidates = 4 +}; + +struct CpTopkParams +{ + //! [numRows, logitsStride] fp32 logits of this rank's local columns. + float const* logits; + int64_t logitsStride; + //! Local columns the logits view holds (each row's local length is clamped to it). + int32_t localCapacity; + //! [1] int32 global indexer KV length of the request; row r has kvLen - nextN + r + 1 valid tokens. + int32_t const* kvLens; + //! [numRows, kTopK] int32 output: global token indices, -1 for unused slots. + int32_t* out; + int32_t numRows; + int32_t nextN; + int32_t rank; + int32_t worldSize; + //! Multicast address of the exchange workspace (kWorkspaceWords words, identical layout on every rank). + uint32_t* mcWorkspace; + //! This rank's unicast address of the same workspace. + uint32_t* ucWorkspace; + //! [kStateWords] int32 per-rank device state, zero-initialized. + int32_t* state; + //! Optional [numRows, kSourcesPerRank, kDebugSlots] int64 phase timestamps (diagnosis only; nullptr = off). + int64_t* debugTimes = nullptr; + //! Longest wait for a peer's words before the kernel aborts (ns); 0 = the default (kDefaultTimeoutMs, or + //! TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS). + uint64_t timeoutNs = 0; +}; + +//! Timestamps per CTA in CpTopkParams::debugTimes: [0, 16) %globaltimer (ns) and [16, 32) clock64 per phase (0 start, +//! 10 grid dependency wait returned, 1 keys loaded, 2 local select, 3 sent, 9 previous buffer reset (%globaltimer of +//! the first reset warp only), 4 candidates polled, 5 cluster statistics agreed, 6 merge select, 7 row written, 8 end), +//! [32, 42) local select (keys, then clock64 at the start of / after the histogram of / after the bin search of each +//! pass), [42, 45) send (start, keys taken, stores issued), 46 round-1 keys sent by this source, 47 local select passes +//! (+10 when the cut was searched among the negative keys), [48, 58) merge select (keys = this CTA's candidates; per +//! pass clock64 at its start / after the group totals arrived / after the bin was decided), clock64 of the merge: 45 +//! candidates polled, 61 statistics combined, 58 emission positions scanned, 60 row written; 62 candidates of the row +//! (all CTAs), 63 merge select passes (+10 likewise; +100 when the row took crossRankTopk's single-CTA merge, which +//! then reuses 45 and 58-61 as crossRankTopk does); clock64 and values of thread 0. +constexpr int32_t kDebugSlots = 64; + +//! Effective wait timeout of a launch with params.timeoutNs == 0 (ms). +int64_t cpTopkDefaultTimeoutMs(); + +//! Dynamic shared memory the kernel needs (bytes). +size_t cpTopkSmemBytes(int32_t localCapacity, int32_t worldSize); + +//! Largest local capacity the kernel accepts for a world size. +int32_t cpTopkMaxLocalCapacity(int32_t worldSize); + +//! Clusters of the kernel's launch configuration (largest local capacity) that can be resident at once on this +//! device (cudaOccupancyMaxActiveClusters, queried once per world size and device); 0 when the query fails. No row +//! waits for another row of its rank, but a row's cluster waits for the same row's sources on every peer rank, and +//! the ranks may make their rows resident in different orders; the caller therefore requires kMaxRows co-resident +//! clusters, so every cluster of a launch can be resident at once (other kernels sharing the SMs are finite and do +//! not wait for this one). +int32_t cpTopkMaxActiveClusters(int32_t worldSize); + +void invokeCpTopk(CpTopkParams const& params, cudaStream_t stream); + +} // namespace kernels::glm_moe_dsa::cross_rank_topk_cluster_merge + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.cu new file mode 100644 index 000000000000..685a2da67a97 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.cu @@ -0,0 +1,2757 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.h" + +#include + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::cross_rank_topk_global_emit +{ +namespace +{ + +constexpr int32_t kBins = 2048; +static_assert(kBins >= kTopK, "the output row is staged in the histogram"); +constexpr int32_t kWarps = kThreads / 32; +//! Per-warp key statistics kept in shared memory (see KeyStats). +constexpr int32_t kNumKeyStats = 5; +//! Tokens of each page one source holds. +constexpr int32_t kBlockTokens = kPageTokens / kSourcesPerRank; +//! CTAs of one row's cluster. +constexpr int32_t kClusterCtas = kSourcesPerRank; +//! Histogram bins per group of the cluster bin search (one warp lane per bin). +constexpr int32_t kGroupBins = 32; +constexpr int32_t kMaxGroups = kBins / kGroupBins; +//! Static shared memory of the kernel (round-2 segment tables and the cluster exchange records), kept out of the +//! dynamic budget. +constexpr size_t kStaticSmemBytes = 8192; + +// Records the CTAs of a row's cluster send each other with st.async (static, so every CTA has them at the same +// address). Exchange x of a row completes on barrier bar[x % 2], phase x / 2; the record sets alternate by pass +// parity and are written again only after every CTA has completed the exchange in between. +struct ClusterRecords +{ + //! Transaction barriers of the exchanges (arrival count 1: this CTA's expect-tx). + uint64_t bar[2]; + //! [pass parity][source CTA][group] histogram totals per group of kGroupBins bins. + uint32_t groups[2][kClusterCtas][kMaxGroups]; + //! [pass parity][source CTA][bin] counts of the selected group's bins, ascending. + uint32_t bins[2][kClusterCtas][kGroupBins]; +}; + +// Scalar slots in shared memory. +enum ScalarSlot : int32_t +{ + kSlotBin = 0, + kSlotAbove, + kSlotBinCount, + kSlotTotal, + kSlotCounter2, + kSlotSrcCount, // [kMaxSources] + kSlotSrcBound = kSlotSrcCount + kMaxSources, // [kMaxSources] + kSlotSrcLen = kSlotSrcBound + kMaxSources, // [kMaxSources] + kSlotSrcOffset = kSlotSrcLen + kMaxSources, // [kMaxSources + 1] + kSlotCountBefore = kSlotSrcOffset + kMaxSources + 1, // candidates of the cluster CTAs before this one + kSlotCountMine, // candidates of this cluster CTA + kSlotCountTotal, // candidates of the row + kSlotPositivesBefore, // candidates >= +0.0 of the cluster CTAs before this one + kNumScalarSlots +}; + +__host__ __device__ inline int32_t numSources(int32_t world) +{ + return world * kSourcesPerRank; +} + +// Even share of kTopK per source. +__host__ __device__ inline int32_t evenShare(int32_t world) +{ + return (kTopK + numSources(world) - 1) / numSources(world); +} + +// Most keys a source sends in round 1 (an even number). +__host__ __device__ inline int32_t roundCap(int32_t world) +{ + return min(kSourceCapacity, 4 * evenShare(world)); +} + +// Keys one source can hold: one block of every local page. +__host__ __device__ inline int32_t chunkCapacity(int32_t localCapacity) +{ + int32_t const pages = (localCapacity + kPageTokens - 1) / kPageTokens; + return pages * kBlockTokens; +} + +__host__ __device__ inline int32_t mergeCapacity(int32_t world) +{ + return numSources(world) * roundCap(world); +} + +__host__ __device__ inline size_t smemBytes(int32_t localCapacity, int32_t world) +{ + size_t words = 2 * size_t{kBins} + 2 * size_t{kWarps} + size_t{kNumKeyStats} * kWarps + size_t{kNumScalarSlots}; + words = (words + 3) / 4 * 4; + words += static_cast(chunkCapacity(localCapacity)) + 2 * static_cast(mergeCapacity(world)); + return words * sizeof(uint32_t); +} + +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 900) +// The kernel targets SM90+ (multicast Lamport exchange, thread-block clusters). +__global__ void __launch_bounds__(kThreads, 1) cpTopkZ11Kernel(CpTopkParams p) +{ + __trap(); +} +#else + +//! Bits of one radix-select digit (kBins bins). +constexpr int32_t kDigitBits = 11; +//! Order-preserving key of +0.0: the keys of non-negative values are at least this, those of negative values below. +//! Real indexer logits are mostly negative, and the cut of a row (and of many sources) often lies among small +//! negative values while the largest keys are positive: one key range across zero spans ~2^31 key units, where a +//! radix digit has no resolution. The selects therefore work on one sign at a time: with fewer than `need` keys at +//! or above kPositiveKey these are all taken and the cut is searched among the negative keys only, else among the +//! non-negative keys only; each range is narrow enough for one or two passes. +constexpr uint32_t kPositiveKey = 0x80000000u; +//! Independent 16-byte candidate loads one merge thread issues before it waits on any of them. +constexpr int32_t kPollBatch = 4; +//! Polls of a still-empty Lamport word between two reads of the global timer. +constexpr int32_t kPollsPerTimerRead = 256; +//! Warps of the cluster merge: warps [0, kPollWarps) poll the round-1 candidates of this CTA's sources (one 16-byte +//! vector per thread: at most 520 vectors per CTA, at world sizes 5 to 7), the next kHeaderWarps warps poll the +//! round-1 headers of every source (one per thread), and the remaining kResetWarps warps reset the previous exchange +//! launch's buffer meanwhile. +constexpr int32_t kPollWarps = 17; +constexpr int32_t kHeaderWarps = 2; +constexpr int32_t kFirstResetWarp = kPollWarps + kHeaderWarps; +constexpr int32_t kResetWarps = kWarps - kFirstResetWarp; +static_assert(kHeaderWarps * 32 >= kMaxSources, "one header per header-warp thread"); + +// Keys a saturated source aims to send in round 1 (kSourceBudget at 8 ranks). +__device__ __forceinline__ int32_t roundNeed(int32_t world) +{ + return min(kSourceCapacity, 3 * evenShare(world)); +} + +// Tokens of `rank` among the global tokens [0, n). +__host__ __device__ __forceinline__ int32_t localLength(int32_t n, int32_t rank, int32_t world) +{ + if (n <= 0) + { + return 0; + } + int32_t const fullPages = n / kPageTokens; + int32_t const rem = n % kPageTokens; + int32_t const ownedFull = (fullPages - rank + world - 1) / world; + int32_t len = ownedFull * kPageTokens; + if (rem > 0 && fullPages % world == rank) + { + len += rem; + } + return len; +} + +// Source `src` of a rank holds one block of kBlockTokens consecutive tokens of every local page: block +// (src + page) % kSourcesPerRank. A fully selected page is thus spread over all sources of its rank. +// Keys of source `src` among `localLen` local columns (only the last local page can be partial). +__host__ __device__ __forceinline__ int32_t sourceLength(int32_t localLen, int32_t src) +{ + int32_t const pages = (localLen + kPageTokens - 1) / kPageTokens; + if (pages == 0) + { + return 0; + } + int32_t const last = pages - 1; + int32_t const tail = localLen - last * kPageTokens - ((src + last) % kSourcesPerRank) * kBlockTokens; + return last * kBlockTokens + min(max(tail, 0), kBlockTokens); +} + +// Global token of a local column under the page-interleaved partition. +__device__ __forceinline__ uint32_t localToGlobal(int32_t col, int32_t rank, int32_t world) +{ + int32_t const page = col / kPageTokens; + return static_cast((page * world + rank) * kPageTokens + (col % kPageTokens)); +} + +struct SmemLayout +{ + uint32_t* hist; // [2 * kBins] radix histograms of alternate passes; the first one stages the output row + uint32_t* warpSums; // [2 * kWarps] + uint32_t* stats; // [kNumKeyStats * kWarps] per-warp key statistics + uint32_t* scal; // [kNumScalarSlots] + uint32_t* chunk; // [chunkCapacity] order-preserving keys of this source + uint32_t* mkey; // [mergeCapacity] staged round-1 pairs, then the merge candidates (merge CTA) + uint32_t* midx; // [mergeCapacity] +}; + +__device__ inline SmemLayout carveSmem(uint32_t* base, int32_t localCapacity, int32_t world) +{ + SmemLayout s; + uint32_t* p = base; + s.hist = p; + p += 2 * kBins; + s.warpSums = p; + p += 2 * kWarps; + s.stats = p; + p += kNumKeyStats * kWarps; + s.scal = p; + p += kNumScalarSlots; + p = base + ((p - base) + 3) / 4 * 4; + s.chunk = p; + p += chunkCapacity(localCapacity); + s.mkey = p; + p += mergeCapacity(world); + s.midx = p; + return s; +} + +__device__ __forceinline__ uint32_t orderedKey(float v) +{ + uint32_t const b = __float_as_uint(v); + uint32_t const k = (b & 0x80000000u) ? ~b : (b | 0x80000000u); + // NaN keys above +inf are clamped so that no key, and no exclusive bound key + 1, equals the empty marker. + return k < kEmptyWord - 1u ? k : kEmptyWord - 2u; +} + +// Bits of x (0 for 0): the digit range of keys in [base, base + x]. +__device__ __forceinline__ int32_t bitLength(uint32_t x) +{ + return x == 0u ? 0 : 32 - __clz(x); +} + +// Statistics of a set of keys: extremes overall and per sign, and the count of non-negative keys. +struct KeyStats +{ + uint32_t maxKey = 0u; // largest key (0 when none) + uint32_t minKey = kEmptyWord; // smallest key (kEmptyWord when none) + uint32_t positives = 0u; // keys >= kPositiveKey + uint32_t maxNeg = 0u; // largest key below kPositiveKey (0 when none) + uint32_t minPos = kEmptyWord; // smallest key >= kPositiveKey (kEmptyWord when none) + + __device__ __forceinline__ void add(uint32_t k) + { + bool const pos = k >= kPositiveKey; + maxKey = max(maxKey, k); + minKey = min(minKey, k); + positives += pos ? 1u : 0u; + maxNeg = pos ? maxNeg : max(maxNeg, k); + minPos = pos ? min(minPos, k) : minPos; + } +}; + +// Reduce a thread's statistics over its warp; lane 0 stores them in the per-warp array (read with readStats after +// the next block barrier). +__device__ __forceinline__ void storeWarpStats(KeyStats const& t, uint32_t* stats) +{ + uint32_t const v0 = __reduce_max_sync(0xFFFFFFFFu, t.maxKey); + uint32_t const v1 = __reduce_min_sync(0xFFFFFFFFu, t.minKey); + uint32_t const v2 = __reduce_add_sync(0xFFFFFFFFu, t.positives); + uint32_t const v3 = __reduce_max_sync(0xFFFFFFFFu, t.maxNeg); + uint32_t const v4 = __reduce_min_sync(0xFFFFFFFFu, t.minPos); + if (threadIdx.x % 32 == 0) + { + int32_t const w = threadIdx.x / 32; + stats[w] = v0; + stats[kWarps + w] = v1; + stats[2 * kWarps + w] = v2; + stats[3 * kWarps + w] = v3; + stats[4 * kWarps + w] = v4; + } +} + +// Block statistics from the per-warp array (every warp reduces it; no barrier). +__device__ __forceinline__ KeyStats readStats(uint32_t const* stats) +{ + int32_t const lane = threadIdx.x % 32; + KeyStats b; + b.maxKey = __reduce_max_sync(0xFFFFFFFFu, stats[lane]); + b.minKey = __reduce_min_sync(0xFFFFFFFFu, stats[kWarps + lane]); + b.positives = __reduce_add_sync(0xFFFFFFFFu, stats[2 * kWarps + lane]); + b.maxNeg = __reduce_max_sync(0xFFFFFFFFu, stats[3 * kWarps + lane]); + b.minPos = __reduce_min_sync(0xFFFFFFFFu, stats[4 * kWarps + lane]); + return b; +} + +// Digit setup of a select of the need-th largest key: over the non-negative keys when there are at least `need` of +// them, else over the negative keys with every non-negative key counted as taken (`aboveHi`). +struct SignRange +{ + uint32_t lo; // smallest key of the range (also the digit base) + uint64_t hi; // exclusive end of the histogrammed range + int32_t bits; // digit range of the range + uint32_t aboveHi; // keys at or above hi, all taken + bool negative; // the cut is among the negative keys +}; + +__device__ __forceinline__ SignRange signRange(KeyStats const& st, uint32_t need) +{ + if (st.positives >= need || st.positives == 0u) + { + uint32_t const lo = st.positives >= need ? st.minPos : st.minKey; + return SignRange{lo, uint64_t{kEmptyWord}, bitLength(st.maxKey - lo), 0u, st.positives == 0u}; + } + return SignRange{st.minKey, uint64_t{kPositiveKey}, bitLength(st.maxNeg - st.minKey), st.positives, true}; +} + +__device__ __forceinline__ int64_t round1Offset(int32_t buf, int32_t src, int32_t row) +{ + return int64_t{buf} * kBufferWords + (int64_t{src} * kMaxRows + row) * kRound1SlotWords; +} + +__device__ __forceinline__ int64_t round2Offset(int32_t buf, int32_t src, int32_t row) +{ + return int64_t{buf} * kBufferWords + int64_t{kMaxSources} * kMaxRows * kRound1SlotWords + + (int64_t{src} * kMaxRows + row) * kRound2SlotWords; +} + +__device__ __forceinline__ uint2 loadVolatile2(uint32_t const* ptr) +{ + uint2 v; + asm volatile("ld.volatile.global.v2.u32 {%0, %1}, [%2];" : "=r"(v.x), "=r"(v.y) : "l"(ptr) : "memory"); + return v; +} + +__device__ __forceinline__ uint4 loadVolatile4(uint32_t const* ptr) +{ + uint4 v; + asm volatile("ld.volatile.global.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) + : "l"(ptr) + : "memory"); + return v; +} + +__device__ __forceinline__ void store2(uint32_t* ptr, uint32_t a, uint32_t b) +{ + asm volatile("st.global.v2.u32 [%0], {%1, %2};" ::"l"(ptr), "r"(a), "r"(b) : "memory"); +} + +__device__ __forceinline__ void store4(uint32_t* ptr, uint4 v) +{ + asm volatile("st.global.v4.u32 [%0], {%1, %2, %3, %4};" ::"l"(ptr), "r"(v.x), "r"(v.y), "r"(v.z), "r"(v.w) + : "memory"); +} + +__device__ __forceinline__ bool isEmpty(uint2 v) +{ + return v.x == kEmptyWord || v.y == kEmptyWord; +} + +// Whether the first pair of a 16-byte candidate vector, and the second one when `second`, were written. +__device__ __forceinline__ bool vectorReady(uint4 v, bool second) +{ + return v.x != kEmptyWord && v.y != kEmptyWord && (!second || (v.z != kEmptyWord && v.w != kEmptyWord)); +} + +__device__ __forceinline__ uint64_t globalTimerNs() +{ + uint64_t t; + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); + return t; +} + +// What a polling thread needs to give up: the error word, the timeout and this rank (for the report). +struct WaitCtx +{ + int32_t* state; + uint64_t timeoutNs; + int32_t rank; +}; + +// A peer did not deliver within the timeout. The selection cannot be completed correctly any more, and the peer's +// late writes could land in a buffer this rank resets or reuses, so record which wait failed and abort the kernel: +// the resulting CUDA error fails the run instead of letting any rank continue with a partial selection. +__device__ __noinline__ void abortOnTimeout(WaitCtx const& w, int32_t what) +{ + bool const first = atomicExch(&w.state[3], what) == 0; + __threadfence_system(); + if (first) + { + printf( + "[glm_moe_dsa] crossRankTopkGlobalEmit: rank %d, row %d: wait %d for a peer exceeded %llu ms; aborting the " + "kernel\n", + w.rank, static_cast(blockIdx.y), what, static_cast(w.timeoutNs / 1000000ull)); + } + __trap(); +} + +// Spin until both words of a Lamport pair were written; aborts the kernel after the timeout. +__device__ uint2 pollPair(uint32_t const* ptr, WaitCtx const& w, int32_t what) +{ + uint2 v = loadVolatile2(ptr); + uint64_t start = 0; + for (int32_t polls = 1; isEmpty(v); ++polls) + { + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, what); + } + } + v = loadVolatile2(ptr); + } + return v; +} + +// Spin until a 16-byte candidate vector was written (see vectorReady); aborts the kernel after the timeout. +__device__ uint4 pollVector(uint32_t const* ptr, bool second, WaitCtx const& w, int32_t what) +{ + uint4 v = loadVolatile4(ptr); + uint64_t start = 0; + for (int32_t polls = 1; !vectorReady(v, second); ++polls) + { + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, what); + } + } + v = loadVolatile4(ptr); + } + return v; +} + +// Block-wide inclusive scan of one value per thread. Returns the inclusive prefix; `total` gets the block sum. +__device__ uint32_t blockInclusiveScan(uint32_t v, uint32_t* warpSums, uint32_t& total) +{ + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + uint32_t x = v; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + if (lane >= o) + { + x += y; + } + } + if (lane == 31) + { + warpSums[warp] = x; + } + __syncthreads(); + if (warp == 0) + { + uint32_t w = warpSums[lane]; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, w, o); + if (lane >= o) + { + w += y; + } + } + warpSums[kWarps + lane] = w; + } + __syncthreads(); + total = warpSums[2 * kWarps - 1]; + uint32_t const before = warp > 0 ? warpSums[kWarps + warp - 1] : 0u; + return x + before; +} + +// Exclusive block-wide scan with a single barrier: every warp scans the per-warp sums itself. Returns this +// thread's exclusive prefix; `total` gets the block sum. warpSums must not be rewritten before the next barrier. +__device__ uint32_t blockExclusiveScan(uint32_t v, uint32_t* warpSums, uint32_t& total) +{ + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + uint32_t x = v; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + if (lane >= o) + { + x += y; + } + } + if (lane == 31) + { + warpSums[warp] = x; + } + __syncthreads(); + uint32_t const w = warpSums[lane]; + uint32_t y = w; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const z = __shfl_up_sync(0xFFFFFFFFu, y, o); + if (lane >= o) + { + y += z; + } + } + total = __shfl_sync(0xFFFFFFFFu, y, 31); + return __shfl_sync(0xFFFFFFFFu, y - w, warp) + x - v; +} + +// Find, counting from the top bin, the bin of `hist` that holds the target-th largest key, with two barriers: +// per-warp sums, then each warp scans them and only the warp holding the target scans its own bins. Returns the +// total count (every thread); when target <= total, the bin, the count above it and its own count go to the +// scalar slots. +__device__ uint32_t findBin(SmemLayout const& s, uint32_t const* hist, int32_t nbins, uint32_t target) +{ + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + int32_t const perThread = (nbins + kThreads - 1) / kThreads; + int32_t const top = nbins - 1 - perThread * static_cast(threadIdx.x); + uint32_t mine = 0; + for (int32_t i = 0; i < perThread && top - i >= 0; ++i) + { + mine += hist[top - i]; + } + uint32_t const warpTotal = __reduce_add_sync(0xFFFFFFFFu, mine); + if (lane == 0) + { + s.warpSums[warp] = warpTotal; + } + __syncthreads(); + uint32_t const w = s.warpSums[lane]; + uint32_t x = w; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + if (lane >= o) + { + x += y; + } + } + uint32_t const total = __shfl_sync(0xFFFFFFFFu, x, 31); + uint32_t const warpBefore = __shfl_sync(0xFFFFFFFFu, x - w, warp); + if (warpBefore < target && target <= warpBefore + warpTotal) + { + uint32_t t = mine; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, t, o); + if (lane >= o) + { + t += y; + } + } + uint32_t acc = warpBefore + t - mine; + if (acc < target && target <= acc + mine) + { + for (int32_t i = 0; i < perThread && top - i >= 0; ++i) + { + uint32_t const c = hist[top - i]; + if (acc + c >= target) + { + s.scal[kSlotBin] = static_cast(top - i); + s.scal[kSlotAbove] = acc; + s.scal[kSlotBinCount] = c; + break; + } + acc += c; + } + } + } + __syncthreads(); + return total; +} + +struct SelectOut +{ + uint32_t edge; // lower edge of the selected bin (the exact key after the last digit) + uint32_t above; // predicate keys strictly above the selected bin + uint32_t binCount; // predicate keys in the selected bin + uint32_t total; // predicate keys + bool all; // take every predicate key (fewer than needed, or all fit the capacity) + bool full; // exact mode: the keys at or above `edge` are exactly the `need` largest + int32_t passes; // radix passes run +}; + +// Candidate keys and token indices held in shared memory (16-byte aligned, readable in groups of four up to the +// next multiple of four). +struct SmemKeys +{ + static constexpr bool kVector = true; + uint32_t const* keys; + uint32_t const* idx; + + __device__ uint4 keys4(int32_t j) const + { + return *reinterpret_cast(keys + j); + } + + __device__ uint4 index4(int32_t j) const + { + return *reinterpret_cast(idx + j); + } +}; + +// Merge candidates in shared memory followed by round-2 candidates read from their global Lamport slots. +struct MergeKeys +{ + static constexpr bool kVector = false; + uint32_t const* smemKeys; + uint32_t const* smemIdx; + int32_t smemCount; + uint32_t const* const* slots; // round-2 slot of each segment + uint32_t const* segBegin; // [numSegments + 1] candidate offsets + int32_t numSegments; + + __device__ uint32_t const* pair(int32_t k) const + { + // Last segment whose begin is <= k. + int32_t lo = 0; + int32_t hi = numSegments - 1; + while (lo < hi) + { + int32_t const mid = (lo + hi + 1) / 2; + if (static_cast(segBegin[mid]) <= k) + { + lo = mid; + } + else + { + hi = mid - 1; + } + } + return slots[lo] + kSlotHeaderWords + 2 * (k - static_cast(segBegin[lo])); + } + + __device__ uint32_t operator()(int32_t j) const + { + return j < smemCount ? smemKeys[j] : pair(j - smemCount)[0]; + } + + __device__ uint32_t index(int32_t j) const + { + return j < smemCount ? smemIdx[j] : pair(j - smemCount)[1]; + } +}; + +// Debug slots of this CTA starting at `offset` (nullptr without a debug buffer). +__device__ __forceinline__ int64_t* debugSlots(CpTopkParams const& p, int32_t offset) +{ + if (p.debugTimes == nullptr) + { + return nullptr; + } + return p.debugTimes + (static_cast(blockIdx.y) * kSourcesPerRank + blockIdx.x) * kDebugSlots + offset; +} + +// clock64 of thread 0 into a debug slot (no-op without a debug buffer). +__device__ __forceinline__ void stamp(int64_t* dbg, int32_t slot) +{ + if (dbg != nullptr && threadIdx.x == 0) + { + dbg[slot] = clock64(); + } +} + +// A value into a debug slot (thread 0; no-op without a debug buffer). +__device__ __forceinline__ void putDebug(int64_t* dbg, int32_t slot, int64_t value) +{ + if (dbg != nullptr && threadIdx.x == 0) + { + dbg[slot] = value; + } +} + +// Phase timestamp for diagnosis (thread 0 of the CTA; no-op without a debug buffer). +__device__ __forceinline__ void markPhase(CpTopkParams const& p, int32_t phase) +{ + if (p.debugTimes != nullptr && threadIdx.x == 0) + { + uint64_t t; + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); + int64_t* d = p.debugTimes + (static_cast(blockIdx.y) * kSourcesPerRank + blockIdx.x) * kDebugSlots; + d[phase] = static_cast(t); + d[16 + phase] = static_cast(clock64()); + } +} + +// ---- Bridge outputs (topkBridgeKernel's values, written at the emission) -------------------------------------------- + +// Row scalars of the bridge in an exchange row, in static shared memory, from warp kFirstResetWarp: +// - the row's request index before the bias (lane kReqLane's asynchronous copy, started right after the grid +// dependency wait and completed before the block barrier that follows the key loads); +// - the layer ids and the accepted count (asynchronous copies started right after the grid dependency wait and +// completed with the warp's cp.async.wait_all in resetSlots); +// - the row's pool row and copy row (stored by lane 0 after resetSlots); +// the last two groups are read after the block barrier that follows the candidate poll. +struct BridgeShared +{ + int64_t acc64; + int32_t* poolRow; + int32_t* copyRow; + int32_t acc32; + int32_t reqRaw; + int32_t layer[kMaxGroupSize]; +}; + +// Lane of warp kFirstResetWarp that copies the request index. +constexpr int32_t kReqLane = 30; + +// Request of an exchange row from its staged index. +__device__ __forceinline__ int32_t sharedRequest(CpTopkBridgeParams const& b, BridgeShared const& bs) +{ + return bs.reqRaw - b.reqIdxBias; +} + +// Pool base of token `tok` read from the block table (the bridge's rule: -1 for tok < 0 or a page beyond the table). +__device__ __forceinline__ int32_t directBase(CpTopkBridgeParams const& b, int32_t req, int32_t tok) +{ + if (tok < 0) + { + return -1; + } + int32_t const page = tok / kPageTokens; + if (page >= b.maxBlocksPerReq) + { + return -1; + } + return b.blockTable[req * b.btStride0 + page * b.btStride1]; +} + +// Copy row of verify row `row` (-1: the row is not copied). Copy mode 0: rows [0, copyRows) to copyRowStart + row. +// Copy mode 1: request i = row / copyNextN < copyRows copies its last accepted row i * copyNextN + clamp(acc - 1, 0, +// copyNextN - 1) (copyNextN - 1 without accepted counts) to copyRowStart + i; that row is the only one of request i +// that can match, so this is the bridge's selection seen from the source row. +__device__ __forceinline__ int32_t bridgeCopyRow(CpTopkBridgeParams const& b, int32_t row, int64_t acc) +{ + if (b.copyDst == nullptr) + { + return -1; + } + if (b.copyMode == 0) + { + return row < b.copyRows ? b.copyRowStart + row : -1; + } + int32_t const i = row / b.copyNextN; + if (i >= b.copyRows) + { + return -1; + } + int64_t off = b.copyNextN - 1; + if (b.numAccepted != nullptr) + { + off = acc - 1; + off = off < 0 ? 0 : (off > b.copyNextN - 1 ? b.copyNextN - 1 : off); + } + int32_t const srcRow = static_cast(static_cast(i) * b.copyNextN + off); + return srcRow == row ? b.copyRowStart + i : -1; +} + +// Whether verify row `row` needs an accepted count (copy mode 1 with counts, a request below copyRows). +__device__ __forceinline__ bool needsAccepted(CpTopkBridgeParams const& b, int32_t row) +{ + return b.copyDst != nullptr && b.copyMode == 1 && b.numAccepted != nullptr && row / b.copyNextN < b.copyRows; +} + +// Accepted count staged in shared memory (BridgeShared) in the dtype of the counts. +__device__ __forceinline__ int64_t sharedAccepted(CpTopkBridgeParams const& b, BridgeShared const& bs) +{ + return b.numAcceptedIs64 ? bs.acc64 : static_cast(bs.acc32); +} + +// Accepted count of the request of verify row `row`, read from global memory (0 when not needed). +__device__ __forceinline__ int64_t loadAccepted(CpTopkBridgeParams const& b, int32_t row) +{ + if (!needsAccepted(b, row)) + { + return 0; + } + int32_t const i = row / b.copyNextN; + return b.numAcceptedIs64 ? static_cast(b.numAccepted)[i] + : static_cast(static_cast(b.numAccepted)[i]); +} + +// Destinations of one verify row's bridge outputs: the pool row of group member 0 (member g at g * strideG further) +// and the copy row (nullptr: not copied). +struct BridgeRowOut +{ + int32_t* pool; + int64_t strideG; + int32_t* copy; +}; + +__device__ __forceinline__ BridgeRowOut bridgeRowOut(CpTopkBridgeParams const& b, int32_t row, int32_t copyRow) +{ + return BridgeRowOut{ + b.out + row * b.outStride0, b.outStrideG, copyRow >= 0 ? b.copyDst + copyRow * b.copyStride0 : nullptr}; +} + +// The bridge outputs of output position `pos`: for every group member g, the bridge's remapOne(tok, layer g) = +// base * strideFactor + (tok % kPageTokens + layer[g] * kPageTokens), or -1 for tok < 0 or base < 0, with its int32 +// wraparound arithmetic (evaluated in uint32, where the grouping of the sum does not change the bits); then the copy. +template +__device__ __forceinline__ void writeBridge( + CpTopkBridgeParams const& b, BridgeRowOut const& o, uint32_t pos, int32_t tok, int32_t base, LayerAt const& layerAt) +{ + bool const valid = tok >= 0 && base >= 0; + uint32_t const common = static_cast(base) * static_cast(b.strideFactor) + + (static_cast(tok) & static_cast(kPageTokens - 1)); + int32_t* dst = o.pool + pos; + for (int32_t g = 0; g < b.groupSize; ++g) + { + *dst = valid ? static_cast(common + static_cast(layerAt(g)) * kPageTokens) : -1; + dst += o.strideG; + } + if (o.copy != nullptr) + { + o.copy[pos] = tok; + } +} + +// Warp kFirstResetWarp of an exchange row: start the asynchronous copies of the row's request index, layer ids (or +// store the single layer id) and its request's accepted count into `bs` (cp.async, generic proxy; see BridgeShared). +__device__ __forceinline__ void fetchRowScalars(CpTopkParams const& p, BridgeShared& bs, int32_t row) +{ + CpTopkBridgeParams const& b = p.bridge; + int32_t const lane = static_cast(threadIdx.x) % 32; + if (lane < b.groupSize) + { + if (b.layerIds != nullptr) + { + uint32_t const dst = static_cast(__cvta_generic_to_shared(&bs.layer[lane])); + asm volatile("cp.async.ca.shared.global [%0], [%1], 4;" ::"r"(dst), "l"(b.layerIds + lane) : "memory"); + } + else + { + bs.layer[lane] = b.layerId; + } + } + static_assert(kMaxGroupSize < kReqLane && kReqLane < 31, "the request and accepted-count lanes hold no layer id"); + if (lane == kReqLane) + { + // A group of its own, completed (cp.async.wait_group 0) before the block barrier that follows the key loads. + uint32_t const dst = static_cast(__cvta_generic_to_shared(&bs.reqRaw)); + asm volatile("cp.async.ca.shared.global [%0], [%1], 4;" ::"r"(dst), "l"(b.reqIdx + row) : "memory"); + asm volatile("cp.async.commit_group;" ::: "memory"); + } + if (lane == 31 && needsAccepted(b, row)) + { + int32_t const i = row / b.copyNextN; + if (b.numAcceptedIs64) + { + uint32_t const dst = static_cast(__cvta_generic_to_shared(&bs.acc64)); + asm volatile("cp.async.ca.shared.global [%0], [%1], 8;" ::"r"(dst), + "l"(static_cast(b.numAccepted) + i) + : "memory"); + } + else + { + uint32_t const dst = static_cast(__cvta_generic_to_shared(&bs.acc32)); + asm volatile("cp.async.ca.shared.global [%0], [%1], 4;" ::"r"(dst), + "l"(static_cast(b.numAccepted) + i) + : "memory"); + } + } +} + +// Start the copy of candidate token `tok`'s pool base (its block-table entry, 4-byte cp.async) into `dst`, or store +// -1 for tok < 0 or a page beyond the table (the bridge's rule). Only the calling thread reads `dst`, after its +// cp.async.wait_all. +__device__ __forceinline__ void fetchBase(CpTopkBridgeParams const& b, int32_t req, int32_t tok, uint32_t* dst) +{ + int32_t const page = tok / kPageTokens; + if (tok < 0 || page >= b.maxBlocksPerReq) + { + *dst = 0xFFFFFFFFu; + return; + } + uint32_t const addr = static_cast(__cvta_generic_to_shared(dst)); + asm volatile("cp.async.ca.shared.global [%0], [%1], 4;" ::"r"(addr), + "l"(b.blockTable + req * b.btStride0 + page * b.btStride1) + : "memory"); +} + +// Cluster-merge emission of one thread: its (at most two) candidates at pos0 / pos1 and its padding position posPad +// (kTopK: none), ids and bridge outputs (same values as writeBridge): the id row outRow, the pool row of group member +// g at poolRow + g * strideG (kGroup members; 0: groupSize, at most kMaxGroupSize) and the copy row (nullptr: none), +// with the pool bases fetched at the poll and the layer ids in shared memory. +// +// It is a called function (not inlined) so that its instructions are at one address: warp kFirstResetWarp's lane 0 +// runs it once into shared scratch while it waits for the poll (warmEmission), so the emission does not fetch them +// from memory on the critical path. The emission runs once per launch, at its end; when the L2 has been streamed since +// the previous launch (as by a layer's weights in the served step), its instructions would otherwise come from DRAM. +template +__device__ __noinline__ void emitCandidates(int32_t* outRow, int32_t* poolRow, int32_t* copyRow, int64_t strideG, + uint32_t strideFactor, int32_t groupSize, int32_t const* layer, uint32_t pos0, uint32_t pos1, uint32_t posPad, + uint2 tokens, uint2 bases) +{ + uint32_t const want = static_cast(kTopK); + auto emit = [&](uint32_t pos, int32_t tok, int32_t base) + { + outRow[pos] = tok; + bool const valid = tok >= 0 && base >= 0; + uint32_t const common = static_cast(base) * strideFactor + + (static_cast(tok) & static_cast(kPageTokens - 1)); + int32_t* const dst = poolRow + pos; +#pragma unroll + for (int32_t g = 0; g < (kGroup > 0 ? kGroup : kMaxGroupSize); ++g) + { + if (kGroup > 0 || g < groupSize) + { + dst[g * strideG] + = valid ? static_cast(common + static_cast(layer[g]) * kPageTokens) : -1; + } + } + if (copyRow != nullptr) + { + copyRow[pos] = tok; + } + }; + if (pos0 < want) + { + emit(pos0, static_cast(tokens.x), static_cast(bases.x)); + } + if (pos1 < want) + { + emit(pos1, static_cast(tokens.y), static_cast(bases.y)); + } + if (posPad < want) + { + emit(posPad, -1, -1); + } +} + +// emitCandidates for a run-time group size; the served sizes (per-layer remaps and groups of four) are unrolled. +__device__ __forceinline__ void emitCandidatesFor(int32_t groupSize, int32_t* outRow, int32_t* poolRow, + int32_t* copyRow, int64_t strideG, uint32_t strideFactor, int32_t const* layer, uint32_t pos0, uint32_t pos1, + uint32_t posPad, uint2 tokens, uint2 bases) +{ + switch (groupSize) + { + case 1: + emitCandidates<1>( + outRow, poolRow, copyRow, strideG, strideFactor, groupSize, layer, pos0, pos1, posPad, tokens, bases); + break; + case 4: + emitCandidates<4>( + outRow, poolRow, copyRow, strideG, strideFactor, groupSize, layer, pos0, pos1, posPad, tokens, bases); + break; + default: + emitCandidates<0>( + outRow, poolRow, copyRow, strideG, strideFactor, groupSize, layer, pos0, pos1, posPad, tokens, bases); + break; + } +} + +//! Words of the shared scratch of warmEmission: ids [0, 4), pool rows [4, 36) (stride 4), copy row [36, 40). +constexpr int32_t kWarmScratchWords = 40; + +// One thread runs the emission of this launch's group size once into `scratch` (three positions, every store taken +// once, no global access), so that its instructions are cached on the SM before the cluster emission runs. +__device__ __forceinline__ void warmEmission(int32_t groupSize, int32_t* scratch, int32_t const* layer) +{ + emitCandidatesFor(groupSize, scratch, scratch + 4, scratch + 36, 4, 0u, layer, 0u, 1u, 2u, make_uint2(0u, 0u), + make_uint2(~0u, ~0u)); +} + +// Histogram update of one key of a radix pass. +__device__ __forceinline__ void histogramKey(uint32_t* hist, bool inRange, uint32_t k, uint32_t lo, uint64_t hi, + uint32_t base, uint32_t prefMask, uint32_t prefVal, int32_t shift, uint32_t binMask) +{ + uint32_t const d = k - base; + if (inRange && k >= lo && static_cast(k) < hi && (d & prefMask) == prefVal) + { + atomicAdd(&hist[(d >> shift) & binMask], 1u); + } +} + +// Block radix select of the need-th largest of the n keys at or above lo, of which `aboveHi` (< need) lie at or +// above hi and are counted as taken without being histogrammed. Every key in [lo, hi) is at least `base`, and +// key - base < 2^topBits; digits of up to kDigitBits bits of key - base are refined from the top. With cap > 0 the +// select stops at the first bin edge whose keys at or above it number at most `cap`. With cap = 0 it runs to the +// exact key, or stops early when the selected bin is wholly taken (`full`). Alternate passes use alternate +// histograms, so the next one is cleared while the current one is searched. Counts in the result include aboveHi. +template +__device__ SelectOut blockSelect(SmemLayout const& s, KeyAt const& keyAt, int32_t n, uint32_t lo, uint64_t hi, + uint32_t need, uint32_t cap, uint32_t base, int32_t topBits, int64_t* dbg = nullptr, uint32_t aboveHi = 0u) +{ + if (dbg != nullptr && threadIdx.x == 0) + { + dbg[0] = n; + } + SelectOut out{lo, 0u, 0u, 0u, false, false, 0}; + uint32_t prefMask = 0; + uint32_t prefVal = 0; + uint32_t above = aboveHi; + int32_t remaining = max(topBits, 1); + uint32_t* hist = s.hist; + uint32_t* histNext = s.hist + kBins; + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, remaining)); b += kThreads) + { + hist[b] = 0; + } + __syncthreads(); +#pragma unroll 1 + for (int32_t pass = 0; remaining > 0; ++pass) + { + int32_t const width = min(kDigitBits, remaining); + int32_t const shift = remaining - width; + int32_t const nbins = 1 << width; + uint32_t const binMask = static_cast(nbins - 1); + out.passes = pass + 1; + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[1 + 3 * pass] = clock64(); + } + if constexpr (KeyAt::kVector) + { + for (int32_t j0 = 4 * static_cast(threadIdx.x); j0 < n; j0 += 4 * kThreads) + { + uint4 const q = keyAt.keys4(j0); + histogramKey(hist, true, q.x, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, j0 + 1 < n, q.y, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, j0 + 2 < n, q.z, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, j0 + 3 < n, q.w, lo, hi, base, prefMask, prefVal, shift, binMask); + } + } + else + { + for (int32_t j = threadIdx.x; j < n; j += kThreads) + { + histogramKey(hist, true, keyAt(j), lo, hi, base, prefMask, prefVal, shift, binMask); + } + } + __syncthreads(); + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[2 + 3 * pass] = clock64(); + } + if (shift > 0) + { + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, shift)); b += kThreads) + { + histNext[b] = 0; + } + } + // Find the bin holding the (need - above)-th largest key, counting from the top bin. + uint32_t const target = need - above; + uint32_t const total = findBin(s, hist, nbins, target); + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[3 + 3 * pass] = clock64(); + } + if (pass == 0) + { + out.total = total + aboveHi; + if (total + aboveHi <= cap || target > total) + { + out.all = true; + out.above = total + aboveHi; + return out; + } + } + // The scalars are next written after two more barriers (the next histogram and scan). + uint32_t const bin = s.scal[kSlotBin]; + above += s.scal[kSlotAbove]; + out.binCount = s.scal[kSlotBinCount]; + prefVal |= bin << shift; + prefMask |= binMask << shift; + out.edge = base + prefVal; + out.above = above; + remaining = shift; + uint32_t* const t = hist; + hist = histNext; + histNext = t; + if (cap > 0 && above + out.binCount <= cap) + { + return out; + } + if (cap == 0 && remaining > 0 && above + out.binCount == need) + { + out.full = true; + return out; + } + } + return out; +} + +// Send one source's selected keys (keys in [lo, hi) of the chunk) to its slot; returns the count sent. +// Takes every predicate key at or above the edge when they fit `cap`. Otherwise (more than `cap` keys tie at the +// exact edge) it takes every key above the edge, plus, with kPartialTies, the first keys equal to it in source +// order up to `cap` (any subset of equal keys is a valid tie-aware choice). Round 1 sends no partial ties, so the +// keys a second round requests from a source (at most the edge) were never sent before. +// The header carries the count and an exclusive upper bound of the keys left unsent (0 when none); in round 1 +// (!kPartialTies) also the key statistics of the keys sent (see kSlotHeaderWords). +// With `stage` (round 1, at most kSourceCapacity keys), the pairs are gathered in shared memory first and written +// with coalesced 16-byte stores; otherwise each pair is stored where it is found. +template +__device__ uint32_t sendSource(SmemLayout const& s, SelectOut const& sel, int32_t n, uint32_t lo, uint64_t hi, + uint32_t cap, uint32_t* mcSlot, int32_t src, int32_t rank, int32_t world, uint2* stage, int64_t* dbg = nullptr) +{ + bool const strict = !sel.all && sel.above + sel.binCount > cap; + uint32_t const threshold = sel.all ? lo : sel.edge; + // Keys equal to the edge that fit after the keys above it (sel.above < need <= cap when strict). + uint32_t const tieBudget = kPartialTies && strict ? cap - sel.above : 0u; + uint32_t const sent = sel.all ? sel.total : (strict ? sel.above + tieBudget : sel.above + sel.binCount); + uint32_t const bound = sel.all ? lo : (strict ? sel.edge + 1u : sel.edge); + stamp(dbg, 42); + // One page block (kBlockTokens keys) per thread; positions from a block scan, so the slot holds the keys in + // source order: first those taken outright, then (strict only) the admitted keys equal to the edge. + int32_t const groups = (n + kBlockTokens - 1) / kBlockTokens; + uint32_t takenMain = 0; + uint32_t takenTies = 0; + uint32_t const tieBase = strict ? sel.above : 0u; + for (int32_t g0 = 0; g0 < groups; g0 += kThreads) + { + int32_t const g = g0 + static_cast(threadIdx.x); + uint32_t k[kBlockTokens]; + uint32_t mask = 0; + uint32_t tieMask = 0; + if (g < groups) + { + uint4 const a = *reinterpret_cast(s.chunk + kBlockTokens * g); + uint4 const b = *reinterpret_cast(s.chunk + kBlockTokens * g + 4); + k[0] = a.x; + k[1] = a.y; + k[2] = a.z; + k[3] = a.w; + k[4] = b.x; + k[5] = b.y; + k[6] = b.z; + k[7] = b.w; +#pragma unroll + for (int32_t i = 0; i < kBlockTokens; ++i) + { + bool const in = kBlockTokens * g + i < n && k[i] >= lo && static_cast(k[i]) < hi; + bool const take = in && (strict ? k[i] > threshold : k[i] >= threshold); + bool const tie = kPartialTies && in && strict && k[i] == threshold; + mask |= (take ? 1u : 0u) << i; + tieMask |= (tie ? 1u : 0u) << i; + } + } + // At most kThreads * kBlockTokens keys per pass: both counts fit 16 bits. + uint32_t const c = static_cast(__popc(mask)) | (static_cast(__popc(tieMask)) << 16); + uint32_t chunkTaken; + uint32_t const before = blockExclusiveScan(c, s.warpSums, chunkTaken); + uint32_t pos = takenMain + (before & 0xFFFFu); + uint32_t tiePos = takenTies + (before >> 16); + takenMain += chunkTaken & 0xFFFFu; + takenTies += chunkTaken >> 16; + if ((mask | tieMask) != 0u) + { + int32_t const colBase = g * kPageTokens + ((src + g) % kSourcesPerRank) * kBlockTokens; +#pragma unroll + for (int32_t i = 0; i < kBlockTokens; ++i) + { + uint32_t slot = kEmptyWord; + if ((mask >> i) & 1u) + { + slot = pos++; + } + else if constexpr (kPartialTies) + { + if ((tieMask >> i) & 1u) + { + slot = tiePos < tieBudget ? tieBase + tiePos : kEmptyWord; + ++tiePos; + } + } + if (slot != kEmptyWord) + { + uint32_t const token = localToGlobal(colBase + i, rank, world); + if (stage != nullptr) + { + stage[slot] = make_uint2(k[i], token); + } + else + { + store2(mcSlot + kSlotHeaderWords + 2 * static_cast(slot), k[i], token); + } + } + } + } + // The staged pairs are complete, and the scan's warp sums may be rewritten. + __syncthreads(); + } + stamp(dbg, 43); + if (stage != nullptr) + { + // Two pairs per 16-byte store; an odd count leaves the last half of the last vector empty. + int32_t const vectors = static_cast(sent + 1u) / 2; + for (int32_t v = threadIdx.x; v < vectors; v += kThreads) + { + uint2 const a = stage[2 * v]; + uint2 const b + = 2 * v + 1 < static_cast(sent) ? stage[2 * v + 1] : make_uint2(kEmptyWord, kEmptyWord); + store4(mcSlot + kSlotHeaderWords + 4 * static_cast(v), make_uint4(a.x, a.y, b.x, b.y)); + } + } + // Receivers check every candidate word, so the header needs no ordering against the candidates. + if constexpr (kPartialTies) + { + if (threadIdx.x == kThreads - 1) + { + store2(mcSlot, sent, bound); + } + } + else + { + // The statistics of the keys sent, from the staged pairs (round 1 always stages), by the last warp, which + // stores no vector (at most kSourceCapacity / 2), while the first warps store the vectors. + static_assert(kSourceCapacity / 2 <= kThreads - 32, "the last warp stores no candidate vector"); + if (threadIdx.x >= kThreads - 32) + { + int32_t const lane = threadIdx.x % 32; + KeyStats t; +#pragma unroll + for (int32_t j = 0; j < kSourceCapacity / 32; ++j) + { + int32_t const i = lane + 32 * j; + if (i < static_cast(sent)) + { + t.add(stage[i].x); + } + } + uint32_t const maxKey = __reduce_max_sync(0xFFFFFFFFu, t.maxKey); + uint32_t const minKey = __reduce_min_sync(0xFFFFFFFFu, t.minKey); + uint32_t const positives = __reduce_add_sync(0xFFFFFFFFu, t.positives); + uint32_t const maxNeg = __reduce_max_sync(0xFFFFFFFFu, t.maxNeg); + uint32_t const minPos = __reduce_min_sync(0xFFFFFFFFu, t.minPos); + if (lane == 31) + { + store4(mcSlot, make_uint4(sent, bound, maxKey, minKey + 1u)); + store4(mcSlot + 4, make_uint4(positives, maxNeg, minPos + 1u, 0u)); + } + } + } + stamp(dbg, 44); + return sent; +} + +// Load one source's keys (one block of kBlockTokens tokens of every local page) from the local logits into the +// chunk; returns the number of keys. Consecutive threads read consecutive keys (whole 32-byte blocks per eight +// threads), and each thread issues kLoadBatch loads before it uses any. With `withStats`, the per-warp key +// statistics are stored for readStats after the next block barrier. +__device__ int32_t loadSource( + SmemLayout const& s, float const* rowLogits, int32_t localLen, int32_t src, bool withStats) +{ + constexpr int32_t kLoadBatch = 8; + int32_t const n = sourceLength(localLen, src); + KeyStats st; + for (int32_t j0 = threadIdx.x; j0 < n; j0 += kLoadBatch * kThreads) + { + float v[kLoadBatch]; +#pragma unroll + for (int32_t i = 0; i < kLoadBatch; ++i) + { + int32_t const j = j0 + i * kThreads; + int32_t const page = j / kBlockTokens; + int32_t const col = page * kPageTokens + ((src + page) % kSourcesPerRank) * kBlockTokens + j % kBlockTokens; + v[i] = j < n ? rowLogits[col] : 0.F; + } +#pragma unroll + for (int32_t i = 0; i < kLoadBatch; ++i) + { + int32_t const j = j0 + i * kThreads; + if (j < n) + { + uint32_t const k = orderedKey(v[i]); + s.chunk[j] = k; + st.add(k); + } + } + } + if (withStats) + { + storeWarpStats(st, s.stats); + } + return n; +} + +// Whether source q withheld keys that may lie above the merged k-th key. +__device__ __forceinline__ bool sourceOverflows(SmemLayout const& s, int32_t q, uint32_t kth) +{ + uint32_t const cnt = s.scal[kSlotSrcCount + q]; + uint64_t const bound = s.scal[kSlotSrcBound + q]; + return cnt < s.scal[kSlotSrcLen + q] && bound > uint64_t{kth} + 1u; +} + +// Emit the top-kTopK of the merge candidates. Keys above `kth` are all taken; ties at `kth` are taken in candidate +// order when only some of them fit (deterministic on every rank), otherwise all of them. With `takeAll`, every +// candidate is taken and the row is padded with -1. The row is assembled in shared memory (the first histogram) +// and written with coalesced stores, together with the bridge outputs of verify row `verifyRow` (pool bases read from +// the block table; the row scalars from `bs`). +template +__device__ bool emitSelection(SmemLayout const& s, KeyAt const& keys, int32_t total, uint32_t kth, bool takeAll, + int32_t* outRow, int32_t overflowSources, CpTopkParams const& p, BridgeShared const& bs, int32_t verifyRow, + int64_t* dbg = nullptr) +{ + uint32_t const want = static_cast(kTopK); + // Positions from one block scan over contiguous candidate ranges (no contended counter); the order is the + // candidate order, identical on every rank. Shared-memory candidates are read four at a time. + int32_t per = (total + kThreads - 1) / kThreads; + if constexpr (KeyAt::kVector) + { + per = (per + 3) / 4 * 4; + } + int32_t const j0 = min(total, static_cast(threadIdx.x) * per); + int32_t const j1 = min(total, j0 + per); + uint32_t cGt = 0; + uint32_t cEq = 0; + if constexpr (KeyAt::kVector) + { + for (int32_t j = j0; j < j1; j += 4) + { + uint4 const q = keys.keys4(j); + uint32_t const kk[4] = {q.x, q.y, q.z, q.w}; +#pragma unroll + for (int32_t i = 0; i < 4; ++i) + { + bool const in = j + i < j1; + cGt += (in && (takeAll || kk[i] > kth)) ? 1u : 0u; + cEq += (in && !takeAll && kk[i] == kth) ? 1u : 0u; + } + } + } + else + { + for (int32_t j = j0; j < j1; ++j) + { + uint32_t const k = keys(j); + cGt += (takeAll || k > kth) ? 1u : 0u; + cEq += (!takeAll && k == kth) ? 1u : 0u; + } + } + // The first `overflowSources` threads flag a source that withheld keys above the threshold (the flag slot is + // zero on entry); with a flag set, nothing is written and the caller runs the second round. + if (static_cast(threadIdx.x) < overflowSources && sourceOverflows(s, threadIdx.x, kth)) + { + atomicOr(&s.scal[kSlotCounter2], 1u); + } + uint32_t allGt; + uint32_t allEq; + uint32_t posGt; + uint32_t posEq; + if (total < 0x10000) + { + // Both counts fit 16 bits: one scan of the packed pair. + uint32_t packedTotal; + uint32_t const packed = cGt | (cEq << 16); + uint32_t const before = blockExclusiveScan(packed, s.warpSums, packedTotal); + allGt = packedTotal & 0xFFFFu; + allEq = packedTotal >> 16; + posGt = before & 0xFFFFu; + posEq = allGt + (before >> 16); + } + else + { + posGt = blockInclusiveScan(cGt, s.warpSums, allGt) - cGt; + __syncthreads(); + posEq = blockInclusiveScan(cEq, s.warpSums, allEq) - cEq; + posEq += allGt; + } + stamp(dbg, 59); + if (overflowSources > 0 && s.scal[kSlotCounter2] != 0u) + { + return true; + } + uint32_t* row = s.hist; + auto place = [&](uint32_t k, uint32_t idx) + { + if (takeAll || k > kth) + { + if (posGt < want) + { + row[posGt] = idx; + } + ++posGt; + } + else if (k == kth) + { + if (posEq < want) + { + row[posEq] = idx; + } + ++posEq; + } + }; + if constexpr (KeyAt::kVector) + { + for (int32_t j = j0; j < j1; j += 4) + { + uint4 const q = keys.keys4(j); + uint4 const x = keys.index4(j); + uint32_t const kk[4] = {q.x, q.y, q.z, q.w}; + uint32_t const xx[4] = {x.x, x.y, x.z, x.w}; +#pragma unroll + for (int32_t i = 0; i < 4; ++i) + { + if (j + i < j1) + { + place(kk[i], xx[i]); + } + } + } + } + else + { + for (int32_t j = j0; j < j1; ++j) + { + place(keys(j), keys.index(j)); + } + } + __syncthreads(); + stamp(dbg, 60); + uint32_t const selected = min(allGt + allEq, want); + BridgeRowOut const bridgeOut{bs.poolRow, p.bridge.outStrideG, bs.copyRow}; + int32_t const req = sharedRequest(p.bridge, bs); + for (uint32_t j = threadIdx.x; j < want; j += kThreads) + { + int32_t const tok = j < selected ? static_cast(row[j]) : -1; + outRow[j] = tok; + writeBridge( + p.bridge, bridgeOut, j, tok, directBase(p.bridge, req, tok), [&](int32_t g) { return bs.layer[g]; }); + } + stamp(dbg, 61); + return false; +} + +// Threshold of an exact (cap = 0) merge select: keys above `kth` are taken, ties at `kth` while they fit. +__device__ __forceinline__ uint32_t mergeThreshold(SelectOut const& m, bool& takeAll) +{ + takeAll = m.all || (m.full && m.edge == 0u); + if (takeAll) + { + return 0u; + } + // A wholly taken bin: the keys at or above its edge are exactly kTopK. + return m.full ? m.edge - 1u : m.edge; +} + +// Reset of the previous exchange launch's buffer on this rank's copy. Its 2 * kMaxSources * kMaxRows slots (round 1, +// then round 2) are spread over the kResetWarps reset warps (the warps from kFirstResetWarp on) of every CTA of the +// launch, at most ten consecutive slots per warp, one per lane: each lane fetches its slot's header once the epoch is +// known (fetchResetHeaders, after the grid dependency wait and the first block barrier; an asynchronous copy into +// shared memory that completes during the local select and the send) and the warp resets the written slots while the +// poll warps wait for the candidates (resetSlots). A slot was written iff its header was; the header's count bounds +// the candidates to reset. +constexpr int32_t kResetSlots = 2 * kMaxSources * kMaxRows; + +// This CTA's index among the launch's clearers, and their number (from the grid; recomputed where used). +__device__ __forceinline__ int32_t resetClearer() +{ + return static_cast(blockIdx.y) * kSourcesPerRank + static_cast(blockIdx.x); +} + +__device__ __forceinline__ int32_t resetClearers() +{ + return static_cast(gridDim.y) * kSourcesPerRank; +} + +__device__ __forceinline__ int32_t resetSlotsPerWarp(int32_t numClearers) +{ + int32_t const warps = numClearers * kResetWarps; + return (kResetSlots + warps - 1) / warps; +} + +// First slot of this (reset) warp. +__device__ __forceinline__ int32_t resetFirstSlot(int32_t clearer, int32_t numClearers) +{ + int32_t const warp = static_cast(threadIdx.x) / 32 - kFirstResetWarp; + return (clearer * kResetWarps + warp) * resetSlotsPerWarp(numClearers); +} + +__device__ __forceinline__ int64_t resetSlotOffset(int32_t buf, int32_t slot) +{ + int32_t const round1Slots = kMaxSources * kMaxRows; + bool const second = slot >= round1Slots; + int32_t const qr = second ? slot - round1Slots : slot; + return second ? round2Offset(buf, qr / kMaxRows, qr % kMaxRows) : round1Offset(buf, qr / kMaxRows, qr % kMaxRows); +} + +__device__ __forceinline__ uint32_t* resetSlotBase(CpTopkParams const& p, int32_t buf, int32_t slot) +{ + return p.ucWorkspace + resetSlotOffset(buf, slot); +} + +//! Most slots one reset warp resets (one row: the fewest reset warps). +constexpr int32_t kMaxResetSlotsPerWarp + = (kResetSlots + kSourcesPerRank * kResetWarps - 1) / (kSourcesPerRank * kResetWarps); +static_assert(kMaxResetSlotsPerWarp <= 32, "one slot header per lane"); + +// Shared-memory home of the reset warps' slot headers (16 bytes per slot: count, bound and two further header words), +// in the merge candidate area beyond the round-1 staging of the send; it is free from the first block barrier until +// the reset. +constexpr int32_t kResetHeaderOffset = 2 * kSourceCapacity; +static_assert(kResetHeaderOffset + 4 * kMaxResetSlotsPerWarp * kResetWarps <= kSourcesPerRank * kSourceCapacity, + "the reset headers fit the smallest merge candidate area"); + +__device__ __forceinline__ uint4* resetHeaderHome(SmemLayout const& s) +{ + int32_t const warp = static_cast(threadIdx.x) / 32 - kFirstResetWarp; + return reinterpret_cast(s.mkey + kResetHeaderOffset) + warp * kMaxResetSlotsPerWarp; +} + +// Word offset, within one Lamport buffer, of the slot whose header this lane fetches for the reset; -1 for the lanes +// and warps without one. Arithmetic on the grid only (no memory access). +__device__ __forceinline__ int32_t resetHeaderOffset() +{ + int32_t const numClearers = resetClearers(); + int32_t const lane = threadIdx.x % 32; + if (static_cast(threadIdx.x) / 32 < kFirstResetWarp || lane >= resetSlotsPerWarp(numClearers)) + { + return -1; + } + int32_t const slot = resetFirstSlot(resetClearer(), numClearers) + lane; + if (slot >= kResetSlots) + { + return -1; + } + static_assert(kBufferWords <= int64_t{1} << 31, "slot offsets within a buffer fit int32"); + return static_cast(resetSlotOffset(0, slot)); +} + +// Starts the copy of this lane's reset slot header of buffer `buf` (offset from resetHeaderOffset) into the warp's +// home with an asynchronous copy (cp.async, generic proxy, completed in resetSlots): a pending copy holds no register +// and no block barrier waits for it. Lanes without a slot do nothing. +__device__ __forceinline__ void fetchResetHeader( + CpTopkParams const& p, SmemLayout const& s, int32_t buf, int32_t offset) +{ + if (offset >= 0) + { + uint32_t const dst = static_cast( + __cvta_generic_to_shared(resetHeaderHome(s) + static_cast(threadIdx.x % 32))); + uint32_t const* const src = p.ucWorkspace + int64_t{buf} * kBufferWords + offset; + asm volatile("cp.async.cg.shared.global [%0], [%1], 16;" ::"r"(dst), "l"(src) : "memory"); + } +} + +// Reset this reset warp's slots of buffer `buf` (headers fetched by fetchResetHeaders); other warps return. +__device__ void resetSlots(CpTopkParams const& p, SmemLayout const& s, int32_t buf) +{ + if (static_cast(threadIdx.x) / 32 < kFirstResetWarp) + { + return; + } + int32_t const clearer = resetClearer(); + int32_t const numClearers = resetClearers(); + // Each lane completes its own header copy; the warp barrier then makes every lane's header visible to the warp. + asm volatile("cp.async.wait_all;" ::: "memory"); + __syncwarp(); + int32_t const per = resetSlotsPerWarp(numClearers); + int32_t const lane = threadIdx.x % 32; + int32_t const first = resetFirstSlot(clearer, numClearers); + uint4 const* const headers = resetHeaderHome(s); + uint4 const empty = make_uint4(kEmptyWord, kEmptyWord, kEmptyWord, kEmptyWord); + for (int32_t i = 0; i < per && first + i < kResetSlots; ++i) + { + uint4 const h = headers[i]; + if (h.x == kEmptyWord && h.y == kEmptyWord) + { + continue; + } + int32_t const slot = first + i; + uint32_t* base = resetSlotBase(p, buf, slot); + uint32_t const capacity = slot >= kMaxSources * kMaxRows ? kRound2Capacity : kSourceCapacity; + uint32_t const count = h.x == kEmptyWord ? capacity : min(h.x, capacity); + int32_t const vectors = static_cast(count + 1u) / 2; + for (int32_t v = lane; v < vectors; v += 32) + { + store4(base + kSlotHeaderWords + 4 * static_cast(v), empty); + } + if (lane < 2) + { + store4(base + 4 * lane, empty); + } + } +} + +// Merge of one row on this rank (one CTA): gathers every source's round-1 candidates, selects the exact +// top-kTopK, runs the second round when a source withheld keys above the threshold, and writes the row and its bridge +// outputs. +__device__ void mergeRow(CpTopkParams const& p, SmemLayout const& s, uint32_t const** segSlots, uint32_t* segBegin, + int32_t buf, int32_t row, int32_t rowLen, int32_t localLen, float const* rowLogits, int32_t* outRow, + WaitCtx const& wait, BridgeShared const& bs) +{ + int32_t const world = p.worldSize; + int32_t const rank = p.rank; + int32_t const sources = numSources(world); + uint32_t const cap = static_cast(roundCap(world)); + __syncthreads(); + if (threadIdx.x == 0) + { + s.scal[kSlotCounter2] = 0u; + } + if (static_cast(threadIdx.x) < sources) + { + // Keys each source holds, computed while its header is in flight. + int32_t const q = threadIdx.x; + s.scal[kSlotSrcLen + q] = static_cast( + sourceLength(min(localLength(rowLen, q / kSourcesPerRank, world), p.localCapacity), q % kSourcesPerRank)); + uint2 const h = pollPair(p.ucWorkspace + round1Offset(buf, threadIdx.x, row), wait, kWaitRound1Header); + s.scal[kSlotSrcCount + threadIdx.x] = min(h.x, cap); + s.scal[kSlotSrcBound + threadIdx.x] = h.y; + } + __syncthreads(); + if (threadIdx.x < 32) + { + // Exclusive prefix of the source counts (at most 64 sources: two per lane). + uint32_t const a = static_cast(threadIdx.x) < sources ? s.scal[kSlotSrcCount + threadIdx.x] : 0u; + uint32_t const b + = static_cast(threadIdx.x) + 32 < sources ? s.scal[kSlotSrcCount + threadIdx.x + 32] : 0u; + uint32_t x = a; + uint32_t z = b; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, x, o); + uint32_t const w = __shfl_up_sync(0xFFFFFFFFu, z, o); + if (static_cast(threadIdx.x) >= o) + { + x += y; + z += w; + } + } + uint32_t const totalA = __shfl_sync(0xFFFFFFFFu, x, 31); + s.scal[kSlotSrcOffset + threadIdx.x] = x - a; + s.scal[kSlotSrcOffset + 32 + threadIdx.x] = totalA + z - b; + if (threadIdx.x == 31) + { + s.scal[kSlotSrcOffset + kMaxSources] = totalA + z; + } + } + __syncthreads(); + int32_t const total = static_cast(s.scal[kSlotSrcOffset + kMaxSources]); + markPhase(p, 4); + + // Candidates: padded (source, vector) positions; issue a batch of independent 16-byte loads, then re-poll the + // ones not yet written. The key statistics are tracked for the merge select. + int32_t const vectorsPerSource = static_cast(cap) / 2; + int32_t const padded = sources * vectorsPerSource; + KeyStats st; +#pragma unroll 1 + for (int32_t v0 = 0; v0 < padded; v0 += kThreads * kPollBatch) + { + uint4 v[kPollBatch]; +#pragma unroll + for (int32_t i = 0; i < kPollBatch; ++i) + { + int32_t const jv = v0 + i * kThreads + static_cast(threadIdx.x); + int32_t const q = jv / vectorsPerSource; + int32_t const kv = jv % vectorsPerSource; + v[i] = make_uint4(0u, 0u, 0u, 0u); + if (jv < padded && 2 * kv < static_cast(s.scal[kSlotSrcCount + q])) + { + v[i] = loadVolatile4(p.ucWorkspace + round1Offset(buf, q, row) + kSlotHeaderWords + 4 * kv); + } + } +#pragma unroll + for (int32_t i = 0; i < kPollBatch; ++i) + { + int32_t const jv = v0 + i * kThreads + static_cast(threadIdx.x); + int32_t const q = jv / vectorsPerSource; + int32_t const kv = jv % vectorsPerSource; + if (jv < padded && 2 * kv < static_cast(s.scal[kSlotSrcCount + q])) + { + bool const second = 2 * kv + 1 < static_cast(s.scal[kSlotSrcCount + q]); + if (!vectorReady(v[i], second)) + { + v[i] = pollVector(p.ucWorkspace + round1Offset(buf, q, row) + kSlotHeaderWords + 4 * kv, second, + wait, kWaitRound1Candidates); + } + int32_t const j = static_cast(s.scal[kSlotSrcOffset + q]) + 2 * kv; + s.mkey[j] = v[i].x; + s.midx[j] = v[i].y; + st.add(v[i].x); + if (second) + { + s.mkey[j + 1] = v[i].z; + s.midx[j + 1] = v[i].w; + st.add(v[i].z); + } + } + } + } + stamp(debugSlots(p, 0), 45); + storeWarpStats(st, s.stats); + __syncthreads(); + markPhase(p, 5); + + // Digits of the merge select span the candidates of the sign the cut lies in (see kPositiveKey). + SignRange const r = total > 0 ? signRange(readStats(s.stats), static_cast(kTopK)) + : SignRange{0u, uint64_t{kEmptyWord}, 32, 0u, false}; + SmemKeys const merged{s.mkey, s.midx}; + SelectOut const m = blockSelect( + s, merged, total, r.lo, r.hi, static_cast(kTopK), 0u, r.lo, r.bits, debugSlots(p, 48), r.aboveHi); + putDebug(debugSlots(p, 0), 62, total); + putDebug(debugSlots(p, 0), 63, m.passes + (r.negative ? 10 : 0)); + bool takeAll = false; + uint32_t const kth = mergeThreshold(m, takeAll); + markPhase(p, 6); + stamp(debugSlots(p, 0), 58); + if (!emitSelection(s, merged, total, kth, takeAll, outRow, sources, p, bs, row, debugSlots(p, 0))) + { + return; + } + + // ---- Round 2 (rare; every rank takes it): withheld keys in [kth, bound) of overflowing sources ---------- + __syncthreads(); + // 1. This rank's overflowing sources send their top-kRound2Capacity withheld keys. + for (int32_t c = 0; c < kSourcesPerRank; ++c) + { + int32_t const q = rank * kSourcesPerRank + c; + if (!sourceOverflows(s, q, kth)) + { + continue; + } + uint64_t const b = s.scal[kSlotSrcBound + q]; + int32_t const nq = loadSource(s, rowLogits, localLen, c, false); + __syncthreads(); + SelectOut const sel2 = blockSelect( + s, SmemKeys{s.chunk, nullptr}, nq, kth, b, static_cast(kRound2Capacity), kRound2Capacity, 0u, 32); + (void) sendSource( + s, sel2, nq, kth, b, kRound2Capacity, p.mcWorkspace + round2Offset(buf, q, row), c, rank, world, nullptr); + __syncthreads(); + } + // 2. Gather the round-2 segments of every overflowing source (all ranks). + if (threadIdx.x == 0) + { + int32_t segs = 0; + uint32_t off = 0; + for (int32_t q = 0; q < sources; ++q) + { + if (!sourceOverflows(s, q, kth)) + { + continue; + } + uint32_t const* slot = p.ucWorkspace + round2Offset(buf, q, row); + uint2 const h = pollPair(slot, wait, kWaitRound2Header); + segSlots[segs] = slot; + segBegin[segs] = off; + off += min(h.x, static_cast(kRound2Capacity)); + ++segs; + } + segBegin[segs] = off; + s.scal[kSlotTotal] = static_cast(segs); + } + __syncthreads(); + int32_t const segs = static_cast(s.scal[kSlotTotal]); + int32_t const total2 = static_cast(segBegin[segs]); + MergeKeys const keys{s.mkey, s.midx, total, segSlots, segBegin, segs}; + for (int32_t k = threadIdx.x; k < total2; k += kThreads) + { + (void) pollPair(keys.pair(k), wait, kWaitRound2Candidates); + } + __syncthreads(); + SelectOut const m2 + = blockSelect(s, keys, total + total2, 0u, uint64_t{kEmptyWord}, static_cast(kTopK), 0u, 0u, 32); + bool takeAll2 = false; + uint32_t const kth2 = mergeThreshold(m2, takeAll2); + (void) emitSelection(s, keys, total + total2, kth2, takeAll2, outRow, 0, p, bs, row); + if (threadIdx.x == 0) + { + atomicAdd(&p.state[2], 1); + } +} + +// ---- Thread-block cluster primitives (one cluster of kClusterCtas CTAs per row; cluster rank = blockIdx.x) ---------- + +__device__ __forceinline__ uint32_t smemAddr(void const* ptr) +{ + return static_cast(__cvta_generic_to_shared(ptr)); +} + +// Shared-memory window address of `ptr` (this CTA's shared memory) in CTA `rank` of the cluster. +__device__ __forceinline__ uint32_t clusterAddr(void const* ptr, int32_t rank) +{ + uint32_t remote; + asm volatile("mapa.shared::cluster.u32 %0, %1, %2;" : "=r"(remote) : "r"(smemAddr(ptr)), "r"(rank)); + return remote; +} + +// Initialize the exchange barriers and announce it to the cluster (relaxed arrive; clusterInitWait completes it +// before the first exchange). Touches shared memory only. +__device__ __forceinline__ void clusterInit(ClusterRecords& rec) +{ + if (threadIdx.x == 0) + { + asm volatile("mbarrier.init.shared::cta.b64 [%0], 1;" ::"r"(smemAddr(&rec.bar[0])) : "memory"); + asm volatile("mbarrier.init.shared::cta.b64 [%0], 1;" ::"r"(smemAddr(&rec.bar[1])) : "memory"); + asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory"); + } + asm volatile("barrier.cluster.arrive.relaxed.aligned;" ::: "memory"); +} + +__device__ __forceinline__ void clusterInitWait() +{ + asm volatile("barrier.cluster.wait.aligned;" ::: "memory"); +} + +// This CTA's single arrival on exchange barrier `bar`, expecting `bytes` from the cluster's st.async stores. +__device__ __forceinline__ void expectBytes(uint64_t* bar, uint32_t bytes) +{ + asm volatile( + "{\n\t.reg .b64 st;\n\tmbarrier.arrive.expect_tx.shared::cta.b64 st, [%0], %1;\n\t}" ::"r"(smemAddr(bar)), + "r"(bytes) + : "memory"); +} + +// Wait until the phase of exchange barrier `bar` with parity `parity` completed (every expected byte arrived). +__device__ __forceinline__ void waitExchange(uint64_t* bar, uint32_t parity) +{ + uint32_t done; + do + { + asm volatile( + "{\n\t.reg .pred p;\n\tmbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 p, [%1], %2;\n\t" + "selp.u32 %0, 1, 0, p;\n\t}" + : "=r"(done) + : "r"(smemAddr(bar)), "r"(parity) + : "memory"); + } while (done == 0u); +} + +// Store 16 / 8 bytes at `ptr` (a record of this CTA's layout) in CTA `rank`, completing them on its barrier `bar`. +__device__ __forceinline__ void sendTo(int32_t rank, void const* ptr, uint64_t const* bar, uint4 v) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.v4.b32 [%0], {%1, %2, %3, %4}, [%5];" ::"r"( + clusterAddr(ptr, rank)), + "r"(v.x), "r"(v.y), "r"(v.z), "r"(v.w), "r"(clusterAddr(bar, rank)) + : "memory"); +} + +__device__ __forceinline__ void sendTo(int32_t rank, void const* ptr, uint64_t const* bar, uint2 v) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.v2.b32 [%0], {%1, %2}, [%3];" ::"r"( + clusterAddr(ptr, rank)), + "r"(v.x), "r"(v.y), "r"(clusterAddr(bar, rank)) + : "memory"); +} + +// Exchange sequence of a row: per pass x = 2 * pass group totals and x = 2 * pass + 1 the selected group's bins. +// Every CTA sends the records of exchange x + 1 only after its own exchange x completed, so a barrier phase (and an +// alternating record set) is reused only after every CTA consumed the previous one. +__device__ __forceinline__ uint64_t* exchangeBar(ClusterRecords& rec, uint32_t x) +{ + return &rec.bar[x & 1u]; +} + +__device__ __forceinline__ uint32_t exchangeParity(uint32_t x) +{ + return (x >> 1) & 1u; +} + +// Inclusive scan over the lanes of a warp. +__device__ __forceinline__ uint32_t warpInclusiveScan(uint32_t v) +{ + int32_t const lane = threadIdx.x % 32; +#pragma unroll + for (int32_t o = 1; o < 32; o <<= 1) + { + uint32_t const y = __shfl_up_sync(0xFFFFFFFFu, v, o); + if (lane >= o) + { + v += y; + } + } + return v; +} + +// Tallies of the cluster select over the candidates of the cluster CTAs before this one (in candidate order), kept +// by warp 0 and published in shared memory: those strictly above the selected bins of all passes so far, and those +// in the selected bin of the last pass. +struct CtaTally +{ + uint32_t aboveBefore; + uint32_t binBefore; +}; + +// Cluster radix select of the need-th largest key among the candidates of every CTA of the cluster (this thread +// holds k0 and k1; kEmptyWord is no candidate, as it lies above every range). It computes exactly what blockSelect +// with cap = 0 computes over the union of the candidates: the same digits (from `base` and topBits), the same bins +// and counts from the cluster-summed histograms, and the same stop rules. Per pass: every CTA histograms its own +// candidates and sends its totals per group of kGroupBins bins to every CTA (st.async, two groups per warp); warp 0 +// of every CTA finds, from the top, the group holding the target, sends its own counts of that group's bins to every +// CTA, and finds the bin from the eight CTAs' counts. From the same records, warp 0 also tallies the candidates of +// the CTAs before this one above the selected bins and in the last selected bin (`tally`), which give this CTA's +// emission offsets without another exchange. +__device__ SelectOut clusterSelect(SmemLayout const& s, ClusterRecords& rec, CtaTally& tally, uint32_t k0, uint32_t k1, + uint32_t lo, uint64_t hi, uint32_t need, uint32_t base, int32_t topBits, uint32_t aboveHi, int64_t* dbg) +{ + int32_t const me = blockIdx.x; + int32_t const lane = threadIdx.x % 32; + int32_t const warp = threadIdx.x / 32; + SelectOut out{lo, 0u, 0u, 0u, false, false, 0}; + uint32_t prefMask = 0; + uint32_t prefVal = 0; + uint32_t above = aboveHi; + int32_t remaining = max(topBits, 1); + uint32_t* hist = s.hist; + uint32_t* histNext = s.hist + kBins; + // Warp 0: the running tallies of the CTAs before this one. + uint32_t aboveBefore = 0u; + uint32_t binBefore = 0u; + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, remaining)); b += kThreads) + { + hist[b] = 0; + } + __syncthreads(); +#pragma unroll 1 + for (int32_t pass = 0; remaining > 0; ++pass) + { + int32_t const width = min(kDigitBits, remaining); + int32_t const shift = remaining - width; + int32_t const nbins = 1 << width; + uint32_t const binMask = static_cast(nbins - 1); + int32_t const groups = (nbins + kGroupBins - 1) / kGroupBins; + int32_t const pairs = (groups + 1) / 2; + uint32_t const xg = 2u * pass; + uint32_t const xb = xg + 1u; + out.passes = pass + 1; + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[1 + 3 * pass] = clock64(); + } + if (threadIdx.x == 0) + { + expectBytes(exchangeBar(rec, xg), kClusterCtas * pairs * 8); + } + // The next pass's histogram: its last reads (the previous pass's bins sent by warp 0) are complete. + if (shift > 0) + { + for (int32_t b = threadIdx.x; b < (1 << min(kDigitBits, shift)); b += kThreads) + { + histNext[b] = 0; + } + } + histogramKey(hist, true, k0, lo, hi, base, prefMask, prefVal, shift, binMask); + histogramKey(hist, true, k1, lo, hi, base, prefMask, prefVal, shift, binMask); + __syncthreads(); + uint32_t(*const groupRec)[kMaxGroups] = rec.groups[pass & 1]; + uint32_t(*const binRec)[kGroupBins] = rec.bins[pass & 1]; + if (warp < pairs) + { + int32_t const b0 = 2 * warp * kGroupBins + lane; + int32_t const b1 = b0 + kGroupBins; + uint32_t const t0 = __reduce_add_sync(0xFFFFFFFFu, b0 < nbins ? hist[b0] : 0u); + uint32_t const t1 = __reduce_add_sync(0xFFFFFFFFu, b1 < nbins ? hist[b1] : 0u); + if (lane < kClusterCtas) + { + sendTo(lane, &groupRec[me][2 * warp], exchangeBar(rec, xg), make_uint2(t0, t1)); + } + } + uint32_t const target = need - above; + if (warp == 0) + { + waitExchange(exchangeBar(rec, xg), exchangeParity(xg)); + if (dbg != nullptr && lane == 0 && pass < 3) + { + dbg[2 + 3 * pass] = clock64(); + } + // Lane l sums the groups at positions 2l and 2l + 1 from the top over the cluster. + int32_t const ga = groups - 1 - 2 * lane; + int32_t const gb = ga - 1; + uint32_t a = 0; + uint32_t b = 0; + uint32_t beforeA = 0; + uint32_t beforeB = 0; +#pragma unroll + for (int32_t c = 0; c < kClusterCtas; ++c) + { + uint32_t const x = ga >= 0 ? groupRec[c][ga] : 0u; + uint32_t const y = gb >= 0 ? groupRec[c][gb] : 0u; + a += x; + b += y; + beforeA += c < me ? x : 0u; + beforeB += c < me ? y : 0u; + } + uint32_t const incl = warpInclusiveScan(a + b); + uint32_t const before = incl - a - b; + uint32_t const total = __shfl_sync(0xFFFFFFFFu, incl, 31); + bool const holds = before < target && target <= incl; + uint32_t const holder = __ballot_sync(0xFFFFFFFFu, holds); + if (lane == 0) + { + s.scal[kSlotTotal] = total; + } + if (holder != 0u) + { + int32_t const hl = __ffs(holder) - 1; + bool const first = target <= before + a; + int32_t const gSel = __shfl_sync(0xFFFFFFFFu, first ? ga : gb, hl); + uint32_t const gAbove = __shfl_sync(0xFFFFFFFFu, first ? before : before + a, hl); + if (lane == 0) + { + expectBytes(exchangeBar(rec, xb), kClusterCtas * kGroupBins * 4); + } + // This CTA's counts of the selected group's bins to every CTA: lane l sends vector l % 8 to CTAs + // l / 8 and l / 8 + 4. + int32_t const v = lane % 8; + int32_t const b0 = gSel * kGroupBins + 4 * v; + uint4 const q = make_uint4(b0 < nbins ? hist[b0] : 0u, b0 + 1 < nbins ? hist[b0 + 1] : 0u, + b0 + 2 < nbins ? hist[b0 + 2] : 0u, b0 + 3 < nbins ? hist[b0 + 3] : 0u); + sendTo(lane / 8, &binRec[me][4 * v], exchangeBar(rec, xb), q); + sendTo(lane / 8 + 4, &binRec[me][4 * v], exchangeBar(rec, xb), q); + // While the bins travel: the earlier CTAs' candidates in the groups above the selected one (both + // groups of the lanes before the holder, and the holder's first group when it selected the second). + bool const selFirst = __shfl_sync(0xFFFFFFFFu, first, hl); + aboveBefore += __reduce_add_sync( + 0xFFFFFFFFu, lane < hl ? beforeA + beforeB : (lane == hl && !selFirst ? beforeA : 0u)); + waitExchange(exchangeBar(rec, xb), exchangeParity(xb)); + // Bins of the selected group from the top: lane l holds bin 32 * gSel + 31 - l. + int32_t const bin = gSel * kGroupBins + kGroupBins - 1 - lane; + uint32_t c = 0; + uint32_t cBefore = 0; +#pragma unroll + for (int32_t d = 0; d < kClusterCtas; ++d) + { + uint32_t const x = binRec[d][kGroupBins - 1 - lane]; + c += x; + cBefore += d < me ? x : 0u; + } + uint32_t const y = warpInclusiveScan(c); + uint32_t const acc = gAbove + y - c; + bool const found = acc < target && target <= acc + c; + if (found) + { + s.scal[kSlotBin] = static_cast(bin); + s.scal[kSlotAbove] = acc; + s.scal[kSlotBinCount] = c; + } + // The earlier CTAs' candidates in the bins above the selected one (lanes before the finder) and in + // the selected bin (the finder's). + int32_t const finder = __ffs(__ballot_sync(0xFFFFFFFFu, found)) - 1; + aboveBefore += __reduce_add_sync(0xFFFFFFFFu, lane < finder ? cBefore : 0u); + binBefore = __shfl_sync(0xFFFFFFFFu, cBefore, finder); + } + else if (lane == 0) + { + // Every key is taken (pass 0 only, the same decision in every CTA): the bin exchange still + // completes, with no bytes, so that the exchange sequence stays aligned. + expectBytes(exchangeBar(rec, xb), 0u); + waitExchange(exchangeBar(rec, xb), exchangeParity(xb)); + } + if (lane == 0) + { + tally.aboveBefore = aboveBefore; + tally.binBefore = binBefore; + } + } + __syncthreads(); + if (dbg != nullptr && threadIdx.x == 0 && pass < 3) + { + dbg[3 + 3 * pass] = clock64(); + } + uint32_t const total = s.scal[kSlotTotal]; + if (pass == 0) + { + out.total = total + aboveHi; + if (total + aboveHi == 0u || target > total) + { + out.all = true; + out.above = total + aboveHi; + return out; + } + } + // The scalars are next written by warp 0 after the next histogram's block barrier. + uint32_t const bin = s.scal[kSlotBin]; + above += s.scal[kSlotAbove]; + out.binCount = s.scal[kSlotBinCount]; + prefVal |= bin << shift; + prefMask |= binMask << shift; + out.edge = base + prefVal; + out.above = above; + remaining = shift; + uint32_t* const t = hist; + hist = histNext; + histNext = t; + if (remaining > 0 && above + out.binCount == need) + { + out.full = true; + return out; + } + } + return out; +} + +// Spin until the header of a round-1 slot was written and, when the slot's count covers candidate vector kv, that +// vector too; returns the header's count (clamped to `cap`) and bound. Both are polled together, so neither waits +// for the other; aborts the kernel after the timeout. +__device__ uint2 pollSlotVector(uint32_t const* slot, int32_t kv, uint32_t cap, uint4& v, WaitCtx const& w) +{ + uint32_t const* vec = slot + kSlotHeaderWords + 4 * kv; + uint2 h = loadVolatile2(slot); + v = loadVolatile4(vec); + uint64_t start = 0; + for (int32_t polls = 1;; ++polls) + { + bool const headerReady = !isEmpty(h); + uint32_t const cnt = headerReady ? min(h.x, cap) : 0u; + if (headerReady && (2u * kv >= cnt || vectorReady(v, 2u * kv + 1u < cnt))) + { + return make_uint2(cnt, h.y); + } + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, headerReady ? kWaitRound1Candidates : kWaitRound1Header); + } + } + if (!headerReady) + { + h = loadVolatile2(slot); + } + v = loadVolatile4(vec); + } +} + +// Spin until the whole 32-byte round-1 header of a slot was written; aborts the kernel after the timeout. +__device__ void pollHeader(uint32_t const* slot, uint4& a, uint4& b, WaitCtx const& w) +{ + a = loadVolatile4(slot); + b = loadVolatile4(slot + 4); + uint64_t start = 0; + for (int32_t polls = 1; !(vectorReady(a, true) && vectorReady(b, true)); ++polls) + { + if (polls % kPollsPerTimerRead == 0) + { + uint64_t const now = globalTimerNs(); + if (start == 0) + { + start = now; + } + else if (now - start > w.timeoutNs) + { + abortOnTimeout(w, kWaitRound1Header); + } + } + a = loadVolatile4(slot); + b = loadVolatile4(slot + 4); + } +} + +// Merge of one row by its cluster. CTA c gathers the round-1 candidates of sources [c * world, (c + 1) * world) (one +// 16-byte vector, two candidate slots, per thread of the poll warps, kept in registers: thread order is crossRankTopk's +// candidate order) while its header warps read every source's header (counts, bounds and the key statistics of the keys +// sent, so every CTA has the row's statistics and can decide every source's overflow without an exchange). The cluster +// runs crossRankTopk's merge select on the summed histograms (clusterSelect), whose records also give every CTA's +// number of keys above the threshold and of ties, and every CTA writes its selected candidates at crossRankTopk's +// positions: keys above the threshold in candidate order, then ties in candidate order while they fit, then -1, each +// position with its bridge outputs (pool bases fetched per candidate during the cluster select). Returns true, writing +// nothing, when a source withheld keys that may lie above the threshold (the same decision on every CTA and rank); the +// row is then merged by mergeRow. +__device__ bool clusterMergeRow(CpTopkParams const& p, SmemLayout const& s, ClusterRecords& rec, CtaTally& tally, + int32_t buf, int32_t row, int32_t rowLen, int32_t* outRow, WaitCtx const& wait, BridgeShared const& bs) +{ + int32_t const world = p.worldSize; + int32_t const sources = numSources(world); + int32_t const me = blockIdx.x; + int32_t const tid = threadIdx.x; + int32_t const lane = tid % 32; + int32_t const warp = tid / 32; + uint32_t const cap = static_cast(roundCap(world)); + int32_t const vectorsPerSource = static_cast(cap) / 2; + int32_t const q0 = me * world; + int64_t* const dbg = debugSlots(p, 0); + // This thread's candidates (kEmptyWord: none; never a key). + uint32_t k0 = kEmptyWord; + uint32_t k1 = kEmptyWord; + if (tid == 0) + { + s.scal[kSlotCounter2] = 0u; + } + if (tid < world * vectorsPerSource) + { + int32_t const q = q0 + tid / vectorsPerSource; + int32_t const kv = tid % vectorsPerSource; + uint4 v; + uint2 const h = pollSlotVector(p.ucWorkspace + round1Offset(buf, q, row), kv, cap, v, wait); + if (2u * kv < h.x) + { + k0 = v.x; + if (2u * kv + 1u < h.x) + { + k1 = v.z; + } + } + // The tokens wait in shared memory until the emission (s.midx is free in the cluster merge). Their pool bases + // are fetched now, while the cluster selects: copies of the candidates' block-table entries into this + // thread's words beyond kBaseOffsetWords, completed before the block barrier of the emission prefix. + *reinterpret_cast(s.midx + 2 * tid) = make_uint2(v.y, v.w); + if (k0 != kEmptyWord) + { + int32_t const req = sharedRequest(p.bridge, bs); + uint32_t* const bases = s.midx + kBaseOffsetWords + 2 * tid; + fetchBase(p.bridge, req, static_cast(v.y), bases); + if (k1 != kEmptyWord) + { + fetchBase(p.bridge, req, static_cast(v.w), bases + 1); + } + } + } + else if (warp >= kPollWarps && warp < kFirstResetWarp) + { + // Header warps: thread h reads source h's header; per source its count (clamped to the round-1 capacity), + // bound and length, and per header warp the reduced key statistics. + int32_t const q = tid - kPollWarps * 32; + KeyStats st; + if (q < sources) + { + uint4 a; + uint4 b; + pollHeader(p.ucWorkspace + round1Offset(buf, q, row), a, b, wait); + s.scal[kSlotSrcCount + q] = min(a.x, cap); + s.scal[kSlotSrcBound + q] = a.y; + s.scal[kSlotSrcLen + q] = static_cast(sourceLength( + min(localLength(rowLen, q / kSourcesPerRank, world), p.localCapacity), q % kSourcesPerRank)); + st.maxKey = a.z; + st.minKey = a.w - 1u; + st.positives = b.x; + st.maxNeg = b.y; + st.minPos = b.z - 1u; + } + uint32_t const cnt = q < sources ? s.scal[kSlotSrcCount + q] : 0u; + // Candidates and keys >= +0.0 of the cluster CTAs before this one (CTA c merges sources + // [c * world, (c + 1) * world)), of this one and of the row. + int32_t const c = q < sources ? q / world : kClusterCtas; + uint32_t const nBefore = __reduce_add_sync(0xFFFFFFFFu, c < me ? cnt : 0u); + uint32_t const nMine = __reduce_add_sync(0xFFFFFFFFu, c == me ? cnt : 0u); + uint32_t const nAll = __reduce_add_sync(0xFFFFFFFFu, cnt); + uint32_t const pBefore = __reduce_add_sync(0xFFFFFFFFu, c < me ? st.positives : 0u); + if (lane == 0) + { + atomicAdd(&s.scal[kSlotCountBefore], nBefore); + atomicAdd(&s.scal[kSlotCountMine], nMine); + atomicAdd(&s.scal[kSlotCountTotal], nAll); + atomicAdd(&s.scal[kSlotPositivesBefore], pBefore); + } + uint32_t const v0 = __reduce_max_sync(0xFFFFFFFFu, st.maxKey); + uint32_t const v1 = __reduce_min_sync(0xFFFFFFFFu, st.minKey); + uint32_t const v2 = __reduce_add_sync(0xFFFFFFFFu, st.positives); + uint32_t const v3 = __reduce_max_sync(0xFFFFFFFFu, st.maxNeg); + uint32_t const v4 = __reduce_min_sync(0xFFFFFFFFu, st.minPos); + if (lane == 0) + { + // In the scan scratch (s.stats may still be read by the send's last warp; s.warpSums is next written + // by the emission scan, after the reads below). + uint32_t* const out = s.warpSums + kNumKeyStats * (warp - kPollWarps); + out[0] = v0; + out[1] = v1; + out[2] = v2; + out[3] = v3; + out[4] = v4; + } + } + stamp(dbg, 45); + __syncthreads(); + markPhase(p, 4); + + // Statistics of the row's candidates from every source's header (identical in every CTA and rank). + KeyStats g; +#pragma unroll + for (int32_t w = 0; w < kHeaderWarps; ++w) + { + uint32_t const* const in = s.warpSums + kNumKeyStats * w; + g.maxKey = max(g.maxKey, in[0]); + g.minKey = min(g.minKey, in[1]); + g.positives += in[2]; + g.maxNeg = max(g.maxNeg, in[3]); + g.minPos = min(g.minPos, in[4]); + } + uint32_t const total = s.scal[kSlotCountTotal]; + stamp(dbg, 61); + markPhase(p, 5); + + SignRange const r + = total > 0u ? signRange(g, static_cast(kTopK)) : SignRange{0u, uint64_t{kEmptyWord}, 32, 0u, false}; + clusterInitWait(); + SelectOut const m = clusterSelect( + s, rec, tally, k0, k1, r.lo, r.hi, static_cast(kTopK), r.lo, r.bits, r.aboveHi, debugSlots(p, 48)); + putDebug(debugSlots(p, 48), 0, s.scal[kSlotCountMine]); + putDebug(dbg, 62, total); + putDebug(dbg, 63, m.passes + (r.negative ? 10 : 0)); + bool takeAll = false; + uint32_t const kth = mergeThreshold(m, takeAll); + markPhase(p, 6); + + // Keys above the threshold and ties of the cluster CTAs before this one and of the row (identical in every CTA), + // from the select's tallies; with the threshold below a wholly taken bin the ties fall beyond kTopK and are taken + // as keys above it. + uint32_t gtBefore; + uint32_t eqBefore; + uint32_t allGt; + uint32_t allEq; + if (takeAll) + { + gtBefore = s.scal[kSlotCountBefore]; + eqBefore = 0u; + allGt = total; + allEq = 0u; + } + else + { + uint32_t const aboveBefore = tally.aboveBefore + (r.aboveHi > 0u ? s.scal[kSlotPositivesBefore] : 0u); + gtBefore = m.full ? aboveBefore + tally.binBefore : aboveBefore; + eqBefore = m.full ? 0u : tally.binBefore; + allGt = m.full ? m.above + m.binCount : m.above; + allEq = m.full ? 0u : m.binCount; + } + // This CTA's positions, scanned in thread order; a source of the row that withheld keys above the threshold + // sends the row to the second round. + bool const gt0 = k0 != kEmptyWord && (takeAll || k0 > kth); + bool const eq0 = k0 != kEmptyWord && !takeAll && k0 == kth; + bool const gt1 = k1 != kEmptyWord && (takeAll || k1 > kth); + bool const eq1 = k1 != kEmptyWord && !takeAll && k1 == kth; + if (tid < sources && sourceOverflows(s, tid, kth)) + { + atomicOr(&s.scal[kSlotCounter2], 1u); + } + // Exclusive prefix in thread order (k0 before k1): within the warp from ballots, across warps from the warp + // totals (one block barrier). + uint32_t const below = (1u << lane) - 1u; + uint32_t const bGt0 = __ballot_sync(0xFFFFFFFFu, gt0); + uint32_t const bGt1 = __ballot_sync(0xFFFFFFFFu, gt1); + uint32_t const bEq0 = __ballot_sync(0xFFFFFFFFu, eq0); + uint32_t const bEq1 = __ballot_sync(0xFFFFFFFFu, eq1); + if (lane == 0) + { + s.warpSums[warp] = static_cast(__popc(bGt0) + __popc(bGt1)) + + (static_cast(__popc(bEq0) + __popc(bEq1)) << 16); + } + // The pool-base copies are complete before the barrier (a second-round row reuses s.midx). + asm volatile("cp.async.wait_all;" ::: "memory"); + __syncthreads(); + uint32_t const warpsBefore = __reduce_add_sync(0xFFFFFFFFu, lane < warp ? s.warpSums[lane] : 0u); + uint32_t const before = warpsBefore + static_cast(__popc(bGt0 & below) + __popc(bGt1 & below)) + + (static_cast(__popc(bEq0 & below) + __popc(bEq1 & below)) << 16); + stamp(dbg, 58); + if (s.scal[kSlotCounter2] != 0u) + { + return true; + } + uint32_t const want = static_cast(kTopK); + uint32_t posGt = gtBefore + (before & 0xFFFFu); + uint32_t posEq = allGt + eqBefore + (before >> 16); + uint2 tokens = make_uint2(0u, 0u); + uint2 tokenBases = make_uint2(0u, 0u); + if (k0 != kEmptyWord) + { + tokens = *reinterpret_cast(s.midx + 2 * tid); + tokenBases = *reinterpret_cast(s.midx + kBaseOffsetWords + 2 * tid); + } + // Output positions of this thread's two candidates (want: not written) and of its share of the -1 padding (at + // most one position per thread of the cluster). + static_assert(kClusterCtas * kThreads >= kTopK, "one padding position per thread"); + uint32_t const pos0 = gt0 ? posGt : (eq0 ? posEq : want); + posGt += gt0 ? 1u : 0u; + posEq += eq0 ? 1u : 0u; + uint32_t const pos1 = gt1 ? posGt : (eq1 ? posEq : want); + uint32_t const selected = min(allGt + allEq, want); + uint32_t const posPad = selected + static_cast(me * kThreads + tid); + emitCandidatesFor(p.bridge.groupSize, outRow, bs.poolRow, bs.copyRow, p.bridge.outStrideG, + static_cast(p.bridge.strideFactor), bs.layer, pos0, pos1, posPad, tokens, tokenBases); + stamp(dbg, 60); + return false; +} + +__global__ void __launch_bounds__(kThreads, 1) cpTopkZ11Kernel(CpTopkParams p) +{ + markPhase(p, 0); + extern __shared__ __align__(16) uint32_t smemRaw[]; + __shared__ uint32_t const* segSlots[kMaxSources]; + __shared__ uint32_t segBegin[kMaxSources + 1]; + __shared__ __align__(16) ClusterRecords rec; + __shared__ CtaTally tally; + __shared__ int32_t epochShared; + __shared__ __align__(16) BridgeShared bridgeRow; + __shared__ int32_t emitScratch[kWarmScratchWords]; + int32_t const world = p.worldSize; + int32_t const rank = p.rank; + int32_t const src = blockIdx.x; + int32_t const row = blockIdx.y; + SmemLayout const s = carveSmem(smemRaw, p.localCapacity, world); + int32_t* outRow = p.out + static_cast(row) * kTopK; + clusterInit(rec); + if (threadIdx.x <= kSlotPositivesBefore - kSlotCountBefore) + { + s.scal[kSlotCountBefore + threadIdx.x] = 0u; + } + int32_t const resetOffset = resetHeaderOffset(); + // Every global read (lengths, logits, the epoch word, the workspace, the bridge inputs) comes after the grid + // dependency wait, so a programmatic early launch is safe whatever the preceding kernels write. + asm volatile("griddepcontrol.wait;" ::: "memory"); + markPhase(p, 10); + // Thread 0 and the reset warps read this CTA's epoch word, written only by the previous exchange launch of this + // op, before the key loads; nothing waits for the value until the key loads are done. Thread 0 publishes it to the + // CTA at the block barrier that follows the key loads, and the reset warps start the copies of the previous + // buffer's headers right before that barrier (slot offsets computed before the wait, from the grid only). + int32_t* const epochWord = p.state + kEpochBase + row * kSourcesPerRank + src; + int32_t epochRead = 0; + if (threadIdx.x == 0 || static_cast(threadIdx.x) >= kFirstResetWarp * 32) + { + epochRead = *reinterpret_cast(epochWord); + } + int32_t const prevBufRead = (epochRead + kNumLamportBuffers - 1) % kNumLamportBuffers; + int32_t const kvLen = p.kvLens[0]; + int32_t const rowLen = max(0, kvLen - p.nextN + row + 1); + // Identity launch (every row has at most kTopK tokens; the same decision on every rank): no exchange, and + // neither the workspace nor the epoch words are touched, so identity launches never shorten the reuse chain of + // the Lamport buffers (see crossRankTopkGlobalEmit.h). + bool const exchange = kvLen - p.nextN + p.numRows > kTopK; + // Thread 0 advances this CTA's epoch word (the CTAs of the last row also those of the rows this launch does not + // have, so every word counts every exchange launch), after a block barrier that follows every read of the word in + // this CTA. Nothing else of this launch reads the epoch words, and the next launch reads them only after its grid + // dependency wait. + auto advanceEpoch = [&]() + { + if (exchange && threadIdx.x == 0) + { + int32_t const rows = row == static_cast(gridDim.y) - 1 ? kMaxRows - row : 1; + for (int32_t r = 0; r < rows; ++r) + { + epochWord[r * kSourcesPerRank] = epochRead + 1; + } + } + }; + + if (rowLen <= kTopK) + { + // Every rank agrees on this branch: identity selection, no exchange for this row. Each CTA of the row writes + // kTopK / kClusterCtas positions with their bridge outputs (bridge inputs read directly). + constexpr int32_t kIdentityPerCta = kTopK / kClusterCtas; + if (static_cast(threadIdx.x) < kIdentityPerCta) + { + CpTopkBridgeParams const& b = p.bridge; + int32_t const pos = src * kIdentityPerCta + static_cast(threadIdx.x); + int32_t const tok = pos < rowLen ? pos : -1; + outRow[pos] = tok; + int32_t const req = b.reqIdx[row] - b.reqIdxBias; + BridgeRowOut const bridgeOut = bridgeRowOut(b, row, bridgeCopyRow(b, row, loadAccepted(b, row))); + writeBridge(b, bridgeOut, static_cast(pos), tok, directBase(b, req, tok), + [&](int32_t g) { return b.layerIds != nullptr ? b.layerIds[g] : b.layerId; }); + } + if (exchange) + { + fetchResetHeader(p, s, prevBufRead, resetOffset); + resetSlots(p, s, prevBufRead); + markPhase(p, 9); + } + __syncthreads(); + advanceEpoch(); + clusterInitWait(); + } + else + { + int32_t const localLen = min(localLength(rowLen, rank, world), p.localCapacity); + float const* rowLogits = p.logits + static_cast(row) * p.logitsStride; + // The first reset warp starts the copies of the row's request index, layer ids and accepted count (completed + // with the reset). + if (static_cast(threadIdx.x) / 32 == kFirstResetWarp) + { + fetchRowScalars(p, bridgeRow, row); + } + + // ---- Round 1: this source's local top keys --------------------------------------------------------- + int32_t const n = loadSource(s, rowLogits, localLen, src, true); + if (threadIdx.x == 0) + { + epochShared = epochRead; + } + // The request index copy is complete before the barrier (only its group is committed at this point). + if (static_cast(threadIdx.x) == kFirstResetWarp * 32 + kReqLane) + { + asm volatile("cp.async.wait_group 0;" ::: "memory"); + } + // The reset warps start fetching the headers of their share of the previous exchange launch's buffer (an + // exchange row implies an exchange launch); the values are first used by the reset after the send. + fetchResetHeader(p, s, prevBufRead, resetOffset); + __syncthreads(); + markPhase(p, 1); + advanceEpoch(); + uint32_t const cap = static_cast(roundCap(world)); + uint32_t const need = static_cast(roundNeed(world)); + SelectOut sel{0u, 0u, 0u, static_cast(n), true, false, 0}; + uint32_t selLo = 0u; + int32_t passInfo = 0; + if (n > static_cast(cap)) + { + // The cut lies among the keys of one sign (see kPositiveKey); the digits span that sign's key range. + SignRange const r = signRange(readStats(s.stats), need); + selLo = r.lo; + SmemKeys const keys{s.chunk, nullptr}; + sel = blockSelect(s, keys, n, r.lo, r.hi, need, cap, r.lo, r.bits, debugSlots(p, 32), r.aboveHi); + passInfo = sel.passes + (r.negative ? 10 : 0); + } + markPhase(p, 2); + int32_t const sendBuf = epochShared % kNumLamportBuffers; + uint32_t const sent = sendSource(s, sel, n, selLo, uint64_t{kEmptyWord}, cap, + p.mcWorkspace + round1Offset(sendBuf, rank * kSourcesPerRank + src, row), src, rank, world, + reinterpret_cast(s.mkey), debugSlots(p, 0)); + putDebug(debugSlots(p, 0), 46, sent); + putDebug(debugSlots(p, 0), 47, passInfo); + markPhase(p, 3); + + // The reset warps reset the previous exchange launch's buffer, whose candidates every rank has consumed (a + // peer writes it again only two exchange launches later, after this launch completed on this rank), while + // the poll and header warps wait for this launch's candidates. + resetSlots(p, s, (epochShared + kNumLamportBuffers - 1) % kNumLamportBuffers); + int32_t const buf = epochShared % kNumLamportBuffers; + if (threadIdx.x == kFirstResetWarp * 32) + { + int64_t* const d = debugSlots(p, 0); + if (d != nullptr) + { + d[9] = static_cast(globalTimerNs()); + } + // The row's pool and copy rows (the accepted count is visible after resetSlots' warp barrier), read by + // the emission after the block barrier that follows the candidate poll. + CpTopkBridgeParams const& b = p.bridge; + int32_t const copyRow = bridgeCopyRow(b, row, sharedAccepted(b, bridgeRow)); + bridgeRow.poolRow = b.out + row * b.outStride0; + bridgeRow.copyRow = copyRow >= 0 ? b.copyDst + copyRow * b.copyStride0 : nullptr; + // The emission's instructions, run once now while this warp waits for the poll (see emitCandidates); + // the layer ids are complete after resetSlots. + warmEmission(b.groupSize, emitScratch, bridgeRow.layer); + } + + // ---- Merge: the row's cluster ----------------------------------------------------------------------- + WaitCtx const wait{p.state, p.timeoutNs, rank}; + if (clusterMergeRow(p, s, rec, tally, buf, row, rowLen, outRow, wait, bridgeRow) && src == 0) + { + // A source withheld keys above the threshold: crossRankTopk's merge, with its second round, on CTA 0. + mergeRow(p, s, segSlots, segBegin, buf, row, rowLen, localLen, rowLogits, outRow, wait, bridgeRow); + int64_t* const dbg = debugSlots(p, 0); + putDebug(dbg, 63, dbg != nullptr ? dbg[63] + 100 : 0); + } + markPhase(p, 7); + } + markPhase(p, 8); +} + +#endif + +} // namespace + +size_t cpTopkSmemBytes(int32_t localCapacity, int32_t worldSize) +{ + return smemBytes(localCapacity, worldSize); +} + +int32_t cpTopkMaxLocalCapacity(int32_t worldSize) +{ + static int32_t cached[kMaxRanks + 1] = {}; + if (worldSize < 1 || worldSize > kMaxRanks) + { + return 0; + } + if (cached[worldSize] == 0) + { + int device = 0; + cudaGetDevice(&device); + int maxSmemOptin = 0; + cudaDeviceGetAttribute(&maxSmemOptin, cudaDevAttrMaxSharedMemoryPerBlockOptin, device); + int32_t cap = 0; + for (int32_t c = 1 << 22; c >= kPageTokens * kSourcesPerRank; c -= kPageTokens * kSourcesPerRank) + { + if (smemBytes(c, worldSize) + kStaticSmemBytes <= static_cast(maxSmemOptin)) + { + cap = c; + break; + } + } + cached[worldSize] = cap; + } + return cached[worldSize]; +} + +int32_t cpTopkMaxActiveClusters(int32_t worldSize) +{ + // Queried once per world size and device (the value + 1; 0 = not queried yet). + constexpr int32_t kMaxDevices = 64; + static int32_t cached[kMaxDevices][kMaxRanks + 1] = {}; + if (worldSize < 1 || worldSize > kMaxRanks) + { + return 0; + } + int device = 0; + cudaGetDevice(&device); + if (device >= 0 && device < kMaxDevices && cached[device][worldSize] > 0) + { + return cached[device][worldSize] - 1; + } + int32_t const capacity = cpTopkMaxLocalCapacity(worldSize); + if (capacity == 0) + { + return 0; + } + size_t const smem = smemBytes(capacity, worldSize); + if (cudaFuncSetAttribute(cpTopkZ11Kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(smem)) + != cudaSuccess) + { + (void) cudaGetLastError(); + return 0; + } + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kSourcesPerRank, kMaxRows); + config.blockDim = dim3(kThreads); + config.dynamicSmemBytes = smem; + cudaLaunchAttribute attr{}; + attr.id = cudaLaunchAttributeClusterDimension; + attr.val.clusterDim.x = kSourcesPerRank; + attr.val.clusterDim.y = 1; + attr.val.clusterDim.z = 1; + config.attrs = &attr; + config.numAttrs = 1; + int clusters = 0; + if (cudaOccupancyMaxActiveClusters(&clusters, cpTopkZ11Kernel, &config) != cudaSuccess) + { + (void) cudaGetLastError(); + return 0; + } + if (device >= 0 && device < kMaxDevices) + { + cached[device][worldSize] = clusters + 1; + } + return clusters; +} + +int64_t cpTopkDefaultTimeoutMs() +{ + static int64_t const timeoutMs = []() + { + char const* env = std::getenv("TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS"); + if (env == nullptr || env[0] == '\0') + { + return kDefaultTimeoutMs; + } + char* end = nullptr; + long long const v = std::strtoll(env, &end, 10); + if (end == env || *end != '\0' || v <= 0) + { + throw std::invalid_argument( + std::string( + "glm_moe_dsa_cross_rank_topk_global_emit: bad TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS=") + + env); + } + return static_cast(v); + }(); + return timeoutMs; +} + +void invokeCpTopk(CpTopkParams const& inParams, cudaStream_t stream) +{ + if (inParams.numRows <= 0) + { + return; + } + CpTopkParams params = inParams; + if (params.timeoutNs == 0) + { + params.timeoutNs = static_cast(cpTopkDefaultTimeoutMs()) * 1000000ull; + } + if (params.numRows > kMaxRows || params.worldSize <= 0 || params.worldSize > kMaxRanks || params.rank < 0 + || params.rank >= params.worldSize) + { + throw std::invalid_argument("glm_moe_dsa_cross_rank_topk_global_emit: unsupported rows/world/rank"); + } + CpTopkBridgeParams const& b = params.bridge; + if (b.out == nullptr || b.reqIdx == nullptr || b.blockTable == nullptr || b.groupSize < 1 + || b.groupSize > kMaxGroupSize || b.maxBlocksPerReq < 0 || (b.copyMode != 0 && b.copyMode != 1) + || b.copyNextN < 1 || b.copyRows < 0 + || (b.copyDst != nullptr + && (b.copyMode == 0 ? b.copyRows > params.numRows + : static_cast(b.copyRows) * b.copyNextN > params.numRows))) + { + throw std::invalid_argument("glm_moe_dsa_cross_rank_topk_global_emit: bad bridge parameters"); + } + // The cluster merge stages two tokens per polled candidate vector below kBaseOffsetWords and their pool bases + // beyond it. + int32_t const pollThreads = params.worldSize * (roundCap(params.worldSize) / 2); + if (2 * pollThreads > kBaseOffsetWords || kBaseOffsetWords + 2 * pollThreads > mergeCapacity(params.worldSize)) + { + throw std::invalid_argument( + "glm_moe_dsa_cross_rank_topk_global_emit: the candidate staging exceeds the merge area"); + } + size_t const smem = smemBytes(params.localCapacity, params.worldSize); + // The attribute is per function and device; raise it only when a larger footprint is requested. + static thread_local size_t configuredSmem[64] = {}; + int device = 0; + cudaGetDevice(&device); + cudaError_t err = cudaSuccess; + if (device >= 64 || configuredSmem[device] < smem) + { + err = cudaFuncSetAttribute( + cpTopkZ11Kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(smem)); + if (err != cudaSuccess) + { + throw std::runtime_error( + std::string("glm_moe_dsa_cross_rank_topk_global_emit: smem attribute: ") + cudaGetErrorString(err)); + } + if (device < 64) + { + configuredSmem[device] = smem; + } + } + // Programmatic dependent launch as for the other TensorRT-LLM kernels (on unless TRTLLM_ENABLE_PDL=0). + static bool const enablePdl = []() + { + char const* env = std::getenv("TRTLLM_ENABLE_PDL"); + return !(env != nullptr && env[0] == '0' && env[1] == '\0'); + }(); + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kSourcesPerRank, params.numRows); + config.blockDim = dim3(kThreads); + config.dynamicSmemBytes = smem; + config.stream = stream; + cudaLaunchAttribute attrs[2]; + attrs[0].id = cudaLaunchAttributeClusterDimension; + attrs[0].val.clusterDim.x = kSourcesPerRank; + attrs[0].val.clusterDim.y = 1; + attrs[0].val.clusterDim.z = 1; + attrs[1].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[1].val.programmaticStreamSerializationAllowed = enablePdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 2; + err = cudaLaunchKernelEx(&config, cpTopkZ11Kernel, params); + if (err == cudaSuccess) + { + err = cudaGetLastError(); + } + if (err != cudaSuccess) + { + throw std::runtime_error( + std::string("glm_moe_dsa_cross_rank_topk_global_emit: launch: ") + cudaGetErrorString(err)); + } +} + +} // namespace kernels::glm_moe_dsa::cross_rank_topk_global_emit + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.h new file mode 100644 index 000000000000..c51b6fff00fe --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.h @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::cross_rank_topk_global_emit +{ + +// Context-partitioned DSA indexer top-k with a cluster merge that also writes the top-k bridge's outputs. +// +// It is the cluster-merge kernel (glmMoeDsa/crossRankTopkClusterMerge.h; the selection, exchange, workspace, epoch, +// reset and timeout below are that kernel's, unchanged) whose emission also writes, at every output position, what the +// top-k index bridge (glmMoeDsa/ropeKvAppend.h, topkBridgeKernel) writes from the selected row: +// - the global KV-pool rows of every layer of the remap group: pool[g][row][pos] = remapOne(tok, layer g), i.e. +// -1 for tok < 0 or a page beyond the request's block table, else +// blockTable[req, tok / 64] * strideFactor + tok % 64 + layerId[g] * 64 (int32 arithmetic, as the bridge); +// - the native top-k copy row of this row, if any: copy mode 0 copies rows [0, copyRows) to copy rows +// copyRowStart + row; copy mode 1 copies, for request i < copyRows, its last accepted row +// i * copyNextN + clamp(numAccepted[i] - 1, 0, copyNextN - 1) (i * copyNextN + copyNextN - 1 without +// numAccepted) to copy row copyRowStart + i. +// In the cluster merge, the pool bases come off the critical path: right after the candidate poll, every poll thread +// starts 4-byte cp.async copies of its (at most two) candidates' block-table entries blockTable[req, tok / 64] into +// its own shared-memory words, which complete while the cluster selects the threshold; the emission then only adds +// and stores. The emission is a called function that one thread of the CTA first runs into shared scratch while it +// waits for the poll, so its instructions are cached on the SM when the emission runs. The row's request index, layer +// ids and accepted count are copied into shared memory by one reset warp right after the grid dependency wait. +// Identity rows and the CTA-0 second round read the block table directly. Every bridge input (request indices, block +// table, layer ids, accepted counts) is read after the grid dependency wait, like every other global access of the +// kernel. +// +// The merge selects exactly what the kernel of glmMoeDsa/crossRankTopk.h selects, in the same output order, with the +// same context partition, exchange sources, round-1 / round-2 protocol and Lamport workspace layout: +// - global 64-token page p belongs to rank p % worldSize; within a rank, each verify row is handled by +// kSourcesPerRank CTAs, CTA s owning the 8-token block (s + page) % kSourcesPerRank of every local page +// (exchange source q = rank * kSourcesPerRank + s); +// - each source selects its local top keys with crossRankTopk's rule and broadcasts them as (order-preserving key, +// global token) pairs in source order, plus a header (count, exclusive bound of the withheld keys; the cluster +// merge adds the key statistics of the keys sent), through the multicast Lamport workspace. +// What differs is how the row is merged and how a launch ends: +// - the kSourcesPerRank CTAs of one row form a thread-block cluster. CTA s merges the candidates of sources +// [s * worldSize, (s + 1) * worldSize) (at 8 ranks: the 8 sources of rank s). The cluster runs crossRankTopk's +// merge select (the same digits and early-stop rules) on the cluster-summed histograms, and every CTA writes its +// selected candidates directly into the output row at crossRankTopk's positions (keys above the threshold in +// candidate order, then ties in candidate order, then -1). Each round-1 slot header carries its source's key +// statistics, so every CTA derives the row's statistics from the headers it polls; the CTAs exchange only the +// select's histogram totals, with st.async stores into each other's shared memory completed on transaction +// barriers, and derive every CTA's output counts from the per-CTA tallies of the select. The only cluster-wide +// barrier orders the barriers' initialization before the first exchange. A row in which a source withheld keys +// above the threshold (identical decision on every rank) is merged instead by CTA 0 with crossRankTopk's single-CTA +// merge, including its second round; +// - every CTA keeps its own epoch word (one per (row, source CTA) slot): its thread 0 reads the word after the grid +// dependency wait (only the previous exchange launch of this op writes it), the value is published to the CTA at +// the block barrier that follows the key loads, and the CTA advances the word with a plain store after that +// barrier (nothing else in the launch reads it; the next launch reads it after its own grid dependency wait); the +// CTAs of the last row also advance the words of the rows the launch does not have, so every word counts the +// exchange launches. There is no arrival counter and no end-of-kernel fence. +// +// Rows of at most kTopK valid tokens produce the identity selection followed by -1, without an exchange. A launch whose +// rows all have at most kTopK valid tokens (the same on every rank: the lengths come from the shared KV length) is an +// identity launch: it writes neither the workspace nor the epoch words. Every other launch is an exchange launch. +// Exchange launches rotate kNumLamportBuffers buffers by the epoch, which counts exchange launches; in exchange launch +// x every CTA resets its share of its own rank's copy of the buffer of exchange launch x - 1 between its send and its +// merge (the headers of its share are fetched once the epoch is known, after the grid dependency wait). Every rank's +// merges of x - 1 have read their copy of it, and a peer writes it again only in exchange launch x + 2, which starts +// after the peer's merges of x + 1 received this rank's sources of x + 1, i.e. after this rank completed launch x +// (identity launches in between neither write nor wait, so they do not shorten this chain). Every global memory +// access of the kernel follows its grid dependency wait, which orders it after the completion (and memory +// visibility) of the preceding kernels of the stream, including the previous launch of this op; the kernel never +// triggers its dependents early. +// +// A merge that waits longer than the timeout (CpTopkParams::timeoutNs, %globaltimer) for a peer's words records the +// wait in the state's error word and aborts the kernel with a trap: the exchange cannot complete correctly any more, +// and a late peer write could land in a buffer this rank reuses, so the run fails instead of continuing. + +//! Selected indices per verify row (DSA index_topk). +constexpr int32_t kTopK = 2048; +//! Ranks of the context-parallel group. +constexpr int32_t kMaxRanks = 8; +//! Verify rows per call. +constexpr int32_t kMaxRows = 8; +//! Tokens per indexer K page; the context is partitioned by whole pages. +constexpr int32_t kPageTokens = 64; +//! CTAs (exchange sources) per rank and verify row; also the cluster size (one cluster per row). +constexpr int32_t kSourcesPerRank = 8; +//! Exchange sources per row. +constexpr int32_t kMaxSources = kMaxRanks * kSourcesPerRank; +//! Round-1 slots per (source, row). +constexpr int32_t kSourceCapacity = 256; +//! Keys each saturated source aims to send in round 1 at 8 ranks (crossRankTopk's rule: three times the even share of +//! kTopK; refining stops at the first bin edge with at most four times the even share of keys at or above it). +constexpr int32_t kSourceBudget = 96; +//! Round-2 slots per (source, row). +constexpr int32_t kRound2Capacity = 2048; +//! Threads per CTA. +constexpr int32_t kThreads = 1024; +//! Lamport buffers rotated by the epoch. +constexpr int32_t kNumLamportBuffers = 3; +//! 32-bit word that marks an empty Lamport slot; never a valid key, token index, count or bound. +constexpr uint32_t kEmptyWord = 0xFFFFFFFFu; + +//! 32-bit words of one (source, row) slot: a 32-byte header then two words per candidate, so candidates can be written +//! and read with 16-byte vector accesses. Round-1 header: count, exclusive bound of the withheld keys, then the key +//! statistics of the keys sent (largest key, smallest key + 1, keys >= +0.0, largest key below +0.0, smallest key +//! >= +0.0 plus 1; the "+ 1" maps the "none" value 0xFFFFFFFF to 0), then 0. Round-2 header: count, bound. +constexpr int64_t kSlotHeaderWords = 8; +constexpr int64_t kRound1SlotWords = kSlotHeaderWords + 2 * int64_t{kSourceCapacity}; +constexpr int64_t kRound2SlotWords = kSlotHeaderWords + 2 * int64_t{kRound2Capacity}; +//! 32-bit words of one Lamport buffer (round-1 region, then round-2 region). +constexpr int64_t kBufferWords = int64_t{kMaxSources} * kMaxRows * (kRound1SlotWords + kRound2SlotWords); +//! 32-bit words of the whole exchange workspace; every word must start as kEmptyWord. +constexpr int64_t kWorkspaceWords = kNumLamportBuffers * kBufferWords; +//! int32 words of the per-rank device state, zero-initialized: [0, 2) unused, [2] second rounds taken, [3] error word +//! (nonzero = the wait that timed out, written right before the kernel aborts), then one epoch word per +//! (row, source CTA) slot: state[kEpochBase + row * kSourcesPerRank + s]. +constexpr int32_t kEpochBase = 4; +constexpr int32_t kStateWords = kEpochBase + kMaxRows * kSourcesPerRank; +//! Default wait for a peer's Lamport words before the kernel aborts +//! (TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS overrides it). +constexpr int64_t kDefaultTimeoutMs = 120000; + +//! Values of the error word. +enum CpTopkWait : int32_t +{ + kWaitRound1Header = 1, + kWaitRound1Candidates = 2, + kWaitRound2Header = 3, + kWaitRound2Candidates = 4 +}; + +//! Most layers of one remap group (the bridge's kMaxGroupSize). +constexpr int32_t kMaxGroupSize = 8; +//! Words of the merge area (the token staging of the cluster merge) its candidates' tokens use: at most 520 16-byte +//! candidate vectors per CTA (world sizes 5 to 7), two tokens each. The candidates' pool bases follow them, at the +//! same per-thread offsets. +constexpr int32_t kBaseOffsetWords = 1040; + +//! Outputs of the top-k bridge written at the emission (see the comment at the top). +struct CpTopkBridgeParams +{ + //! [groupSize, numRows, kTopK] int32 pool rows with strides (outStrideG, outStride0, 1). + int32_t* out = nullptr; + int64_t outStrideG = 0; + int64_t outStride0 = 0; + int32_t groupSize = 1; + //! Optional [groupSize] int32 layer ids; when null every member uses layerId. + int32_t const* layerIds = nullptr; + int32_t layerId = 0; + //! [numRows] int32 request index per row, before subtracting reqIdxBias. + int32_t const* reqIdx = nullptr; + int32_t reqIdxBias = 0; + //! int32 block table [requests, maxBlocksPerReq] with strides (btStride0, btStride1), block size kPageTokens. + int32_t const* blockTable = nullptr; + int64_t btStride0 = 0; + int64_t btStride1 = 1; + int32_t maxBlocksPerReq = 0; + int32_t strideFactor = 0; + //! Optional copy destination [>= copyRowStart + copyRows, >= kTopK] int32 with row stride copyStride0. + int32_t* copyDst = nullptr; + int64_t copyStride0 = 0; + int32_t copyRows = 0; + int32_t copyRowStart = 0; + int32_t copyMode = 0; + int32_t copyNextN = 1; + //! Optional [copyRows] accepted counts of copy mode 1, int32 or int64. + void const* numAccepted = nullptr; + bool numAcceptedIs64 = false; +}; + +struct CpTopkParams +{ + //! [numRows, logitsStride] fp32 logits of this rank's local columns. + float const* logits; + int64_t logitsStride; + //! Local columns the logits view holds (each row's local length is clamped to it). + int32_t localCapacity; + //! [1] int32 global indexer KV length of the request; row r has kvLen - nextN + r + 1 valid tokens. + int32_t const* kvLens; + //! [numRows, kTopK] int32 output: global token indices, -1 for unused slots. + int32_t* out; + int32_t numRows; + int32_t nextN; + int32_t rank; + int32_t worldSize; + //! Multicast address of the exchange workspace (kWorkspaceWords words, identical layout on every rank). + uint32_t* mcWorkspace; + //! This rank's unicast address of the same workspace. + uint32_t* ucWorkspace; + //! [kStateWords] int32 per-rank device state, zero-initialized. + int32_t* state; + //! Optional [numRows, kSourcesPerRank, kDebugSlots] int64 phase timestamps (diagnosis only; nullptr = off). + int64_t* debugTimes = nullptr; + //! Longest wait for a peer's words before the kernel aborts (ns); 0 = the default (kDefaultTimeoutMs, or + //! TRTLLM_STAIRCASE_GLM_LL_CROSS_RANK_TOPK_TIMEOUT_MS). + uint64_t timeoutNs = 0; + //! The bridge outputs (out, reqIdx and blockTable are required). + CpTopkBridgeParams bridge; +}; + +//! Timestamps per CTA in CpTopkParams::debugTimes: [0, 16) %globaltimer (ns) and [16, 32) clock64 per phase (0 start, +//! 10 grid dependency wait returned, 1 keys loaded, 2 local select, 3 sent, 9 previous buffer reset (%globaltimer of +//! the first reset warp only), 4 candidates polled, 5 cluster statistics agreed, 6 merge select, 7 row written, 8 end), +//! [32, 42) local select (keys, then clock64 at the start of / after the histogram of / after the bin search of each +//! pass), [42, 45) send (start, keys taken, stores issued), 46 round-1 keys sent by this source, 47 local select passes +//! (+10 when the cut was searched among the negative keys), [48, 58) merge select (keys = this CTA's candidates; per +//! pass clock64 at its start / after the group totals arrived / after the bin was decided), clock64 of the merge: 45 +//! candidates polled, 61 statistics combined, 58 emission positions scanned, 60 row written; 62 candidates of the row +//! (all CTAs), 63 merge select passes (+10 likewise; +100 when the row took crossRankTopk's single-CTA merge, which +//! then reuses 45 and 58-61 as crossRankTopk does); clock64 and values of thread 0. +constexpr int32_t kDebugSlots = 64; + +//! Effective wait timeout of a launch with params.timeoutNs == 0 (ms). +int64_t cpTopkDefaultTimeoutMs(); + +//! Dynamic shared memory the kernel needs (bytes). +size_t cpTopkSmemBytes(int32_t localCapacity, int32_t worldSize); + +//! Largest local capacity the kernel accepts for a world size (shared memory; the cluster merge's limit). +int32_t cpTopkMaxLocalCapacity(int32_t worldSize); + +//! Clusters of the kernel's launch configuration (largest local capacity) that can be resident at once on this +//! device (cudaOccupancyMaxActiveClusters, queried once per world size and device); 0 when the query fails. No row +//! waits for another row of its rank, but a row's cluster waits for the same row's sources on every peer rank, and +//! the ranks may make their rows resident in different orders; the caller therefore requires kMaxRows co-resident +//! clusters, so every cluster of a launch can be resident at once (other kernels sharing the SMs are finite and do +//! not wait for this one). +int32_t cpTopkMaxActiveClusters(int32_t worldSize); + +void invokeCpTopk(CpTopkParams const& params, cudaStream_t stream); + +} // namespace kernels::glm_moe_dsa::cross_rank_topk_global_emit + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/denseMlp.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/denseMlp.cu new file mode 100644 index 000000000000..d2fe7ad69100 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/denseMlp.cu @@ -0,0 +1,453 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Weight-streaming BF16 kernels for the dense (non-MoE) MLP at decode row counts (M <= 8). +// +// Both projections are bound by reading the BF16 weight matrix once from HBM. Each CTA owns a +// 16-row weight tile and its warps split the contraction dimension. Every thread loads 16 bytes of +// two weight rows and of one activation row per step and feeds them to two m16n8k16 tensor-core +// MMAs with the weight on the MMA M side and the (at most 8) activation rows on the MMA N side. +// The contraction index assigned to an MMA k-slot depends only on the thread's position inside its +// quad, so weight and activation fragments use the same permutation of k and the sum is exact up to +// FP32 summation order. Each quad therefore reads 64 contiguous bytes of one weight row per step. +// +// The gate/up kernel places 8 gate rows and the matching 8 up rows in one tile, so each thread ends +// with gate and up of the same output column and applies SwiGLU in registers. +// +// Bytes in flight: a thread holds one chunk of steps in registers, and one lane per quad prefetches +// the quad's row segment of the next chunk into L2 with cp.async.bulk.prefetch, which keeps the HBM +// queue full without spending registers. +// +// Programmatic dependent launch: weights never depend on the preceding kernel, so the first chunk +// (registers) and the L2 prefetch of the next one are issued before griddepcontrol.wait; dependents +// are released right after the wait. The tilings keep each grid within one wave of resident CTAs, +// so early-launched dependent CTAs that wait in griddepcontrol.wait cannot starve the primary grid. + +#include "tensorrt_llm/kernels/glmMoeDsa/denseMlp.h" + +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::dense_mlp +{ + +namespace +{ + +using Bf16 = __nv_bfloat16; + +constexpr int kWarpSize = 32; +//! Weight rows per CTA tile (MMA M). +constexpr int kTileRows = 16; +//! Output columns per gate/up CTA: 8 gate rows + 8 up rows form one tile. +constexpr int kGateUpCols = kTileRows / 2; +//! BF16 values per 16-byte vector load. +constexpr int kVecElems = 8; +//! Threads per MMA quad; a quad covers kQuadThreads * kVecElems contraction elements per step. +constexpr int kQuadThreads = 4; +//! Contraction elements consumed by one warp step (two m16n8k16 MMAs). +constexpr int kKPerStep = kQuadThreads * kVecElems; + +//! Compile-time tiling of one kernel instance. +//! \tparam kWarps warps per CTA; each warp owns a contiguous 1/kWarps slice of the contraction. +//! \tparam kSteps steps per register chunk. +//! \tparam kMinBlocks minimum resident CTAs per SM requested from the compiler (register budget). +//! \tparam kCachedX load activations with ld.global.ca instead of the non-coherent path. +//! \tparam kL2Ahead chunks of each weight row segment prefetched into L2 ahead of the register loads. +template +struct Tiling +{ + static constexpr int kWarps = kWarps_; + static constexpr int kSteps = kSteps_; + static constexpr int kMinBlocks = kMinBlocks_; + static constexpr bool kCachedX = kCachedX_; + static constexpr int kL2Ahead = kL2Ahead_; + static constexpr int kKPerChunk = kKPerStep * kSteps; +}; + +__device__ __forceinline__ void pdlWait() +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + asm volatile("griddepcontrol.wait;" ::: "memory"); +#endif +} + +__device__ __forceinline__ void pdlLaunchDependents() +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + asm volatile("griddepcontrol.launch_dependents;" ::: "memory"); +#endif +} + +//! Streaming weight load: not kept in L1, 256-byte L2 fetch granularity. +__device__ __forceinline__ uint4 loadWeight(Bf16 const* ptr) +{ + uint4 r; + asm volatile("ld.global.nc.L1::no_allocate.L2::256B.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(r.x), "=r"(r.y), "=r"(r.z), "=r"(r.w) + : "l"(ptr)); + return r; +} + +//! Activation load: every CTA reads the same few rows, so keep them in L1. +template +__device__ __forceinline__ uint4 loadActivation(Bf16 const* ptr) +{ + if constexpr (kCached) + { + uint4 r; + asm volatile("ld.global.ca.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(r.x), "=r"(r.y), "=r"(r.z), "=r"(r.w) + : "l"(ptr)); + return r; + } + else + { + return __ldg(reinterpret_cast(ptr)); + } +} + +__device__ __forceinline__ void prefetchL2(void const* ptr, uint32_t bytes) +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + asm volatile("cp.async.bulk.prefetch.L2.global [%0], %1;" ::"l"(ptr), "r"(bytes) : "memory"); +#endif +} + +__device__ __forceinline__ void mmaBf16( + float (&d)[4], uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t b0, uint32_t b1) +{ + asm("mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 {%0, %1, %2, %3}, {%4, %5, %6, %7}, {%8, %9}, " + "{%0, %1, %2, %3};" + : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b0), "r"(b1)); +} + +//! Registers of one chunk: kSteps steps of two weight rows and one activation row. +template +struct Chunk +{ + uint4 w0[kSteps]; + uint4 w1[kSteps]; + uint4 x[kSteps]; +}; + +template +__device__ __forceinline__ void loadChunkWeights(Chunk& chunk, Bf16 const* w0, Bf16 const* w1, int chunkIdx) +{ + int const offset = chunkIdx * kKPerStep * kSteps; +#pragma unroll + for (int u = 0; u < kSteps; ++u) + { + chunk.w0[u] = loadWeight(w0 + offset + u * kKPerStep); + chunk.w1[u] = loadWeight(w1 + offset + u * kKPerStep); + } +} + +template +__device__ __forceinline__ void loadChunkActivations(Chunk& chunk, Bf16 const* x, bool hasRow, int chunkIdx) +{ + int const offset = chunkIdx * kKPerStep * kSteps; +#pragma unroll + for (int u = 0; u < kSteps; ++u) + { + chunk.x[u] = hasRow ? loadActivation(x + offset + u * kKPerStep) : make_uint4(0U, 0U, 0U, 0U); + } +} + +//! Two MMAs per step. MMA A uses contraction elements 0..3 of the thread's 8-element vector, MMA B +//! elements 4..7. Fragment registers: a0/a2 = weight row "group", a1/a3 = weight row "group + 8", +//! b0/b1 = activation row "group". +template +__device__ __forceinline__ void computeChunk(float (&accA)[4], float (&accB)[4], Chunk const& chunk) +{ +#pragma unroll + for (int u = 0; u < kSteps; ++u) + { + mmaBf16(accA, chunk.w0[u].x, chunk.w1[u].x, chunk.w0[u].y, chunk.w1[u].y, chunk.x[u].x, chunk.x[u].y); + mmaBf16(accB, chunk.w0[u].z, chunk.w1[u].z, chunk.w0[u].w, chunk.w1[u].w, chunk.x[u].z, chunk.x[u].w); + } +} + +//! Streams the CTA's two weight rows per thread over the warp's contraction slice and returns the +//! CTA-reduced accumulator fragment in warp 0 (other warps return false). +//! acc[0..1] = tile row "group", activation rows 2 * tig + {0, 1}; acc[2..3] = tile row "group + 8". +template +__device__ __forceinline__ bool streamTile(float (&acc)[4], Bf16 const* __restrict__ x, + Bf16 const* __restrict__ weight, int numRows, int kDim, int row0, int row1) +{ + constexpr int kWarps = T::kWarps; + constexpr int kSteps = T::kSteps; + __shared__ float reduceBuf[kWarps][4][kWarpSize]; + + int const lane = threadIdx.x % kWarpSize; + int const warp = threadIdx.x / kWarpSize; + int const group = lane / kQuadThreads; + int const tig = lane % kQuadThreads; + + int const kPerWarp = kDim / kWarps; + int const kBase = warp * kPerWarp + tig * kVecElems; + Bf16 const* w0 = weight + static_cast(row0) * kDim + kBase; + Bf16 const* w1 = weight + static_cast(row1) * kDim + kBase; + bool const hasRow = group < numRows; + Bf16 const* xRow = x + static_cast(hasRow ? group : 0) * kDim + kBase; + int const numChunks = kPerWarp / T::kKPerChunk; + + // One lane per quad prefetches the quad's contiguous row segments of a chunk into L2. + auto prefetchChunkL2 = [&](int chunkIdx) + { + if constexpr (T::kL2Ahead > 0) + { + if (tig == 0 && chunkIdx < numChunks) + { + uint32_t const bytes = T::kKPerChunk * static_cast(sizeof(Bf16)); + prefetchL2(w0 + chunkIdx * T::kKPerChunk, bytes); + prefetchL2(w1 + chunkIdx * T::kKPerChunk, bytes); + } + } + }; + + float accA[4] = {0.F, 0.F, 0.F, 0.F}; + float accB[4] = {0.F, 0.F, 0.F, 0.F}; + + // Weights do not depend on the preceding kernel: request them before the dependency wait. + Chunk chunk; + loadChunkWeights(chunk, w0, w1, 0); +#pragma unroll + for (int a = 1; a <= T::kL2Ahead; ++a) + { + prefetchChunkL2(a); + } + pdlWait(); + pdlLaunchDependents(); + loadChunkActivations(chunk, xRow, hasRow, 0); + +#pragma unroll 1 + for (int c = 0; c < numChunks; ++c) + { + if (c > 0) + { + loadChunkWeights(chunk, w0, w1, c); + loadChunkActivations(chunk, xRow, hasRow, c); + } + prefetchChunkL2(c + 1 + T::kL2Ahead); + computeChunk(accA, accB, chunk); + } + +#pragma unroll + for (int e = 0; e < 4; ++e) + { + acc[e] = accA[e] + accB[e]; + } + if constexpr (kWarps > 1) + { +#pragma unroll + for (int e = 0; e < 4; ++e) + { + reduceBuf[warp][e][lane] = acc[e]; + } + __syncthreads(); + if (warp != 0) + { + return false; + } + // Fixed order over warps keeps the result deterministic. +#pragma unroll + for (int w = 1; w < kWarps; ++w) + { +#pragma unroll + for (int e = 0; e < 4; ++e) + { + acc[e] += reduceBuf[w][e][lane]; + } + } + } + return true; +} + +__device__ __forceinline__ float roundToBf16(float v) +{ + return __bfloat162float(__float2bfloat16_rn(v)); +} + +template +__global__ void __launch_bounds__(T::kWarps* kWarpSize, T::kMinBlocks) gateUpSwigluKernel( + Bf16 const* __restrict__ x, Bf16 const* __restrict__ weight, Bf16* __restrict__ h, int numRows, int hidden, int inter) +{ + int const lane = threadIdx.x % kWarpSize; + int const group = lane / kQuadThreads; + int const tig = lane % kQuadThreads; + int const col = blockIdx.x * kGateUpCols + group; + + float acc[4]; + if (!streamTile(acc, x, weight, numRows, hidden, col, inter + col)) + { + return; + } +#pragma unroll + for (int b = 0; b < 2; ++b) + { + int const row = 2 * tig + b; + if (row < numRows) + { + // gate and up are rounded to BF16 first, matching a BF16 GEMM output feeding SwiGLU. + float const gate = roundToBf16(acc[b]); + float const up = roundToBf16(acc[2 + b]); + float const sig = 1.F / (1.F + __expf(-gate)); + h[static_cast(row) * inter + col] = __float2bfloat16_rn(gate * sig * up); + } + } +} + +template +__global__ void __launch_bounds__(T::kWarps* kWarpSize, T::kMinBlocks) downKernel( + Bf16 const* __restrict__ h, Bf16 const* __restrict__ weight, Bf16* __restrict__ out, int numRows, int inter, int hidden) +{ + int const lane = threadIdx.x % kWarpSize; + int const group = lane / kQuadThreads; + int const tig = lane % kQuadThreads; + int const n0 = blockIdx.x * kTileRows + group; + + float acc[4]; + if (!streamTile(acc, h, weight, numRows, inter, n0, n0 + kTileRows / 2)) + { + return; + } +#pragma unroll + for (int b = 0; b < 2; ++b) + { + int const row = 2 * tig + b; + if (row < numRows) + { + out[static_cast(row) * hidden + n0] = __float2bfloat16_rn(acc[b]); + out[static_cast(row) * hidden + n0 + kTileRows / 2] = __float2bfloat16_rn(acc[2 + b]); + } + } +} + +template +cudaError_t launchKernel(void (*kernel)(Bf16 const*, Bf16 const*, Bf16*, int, int, int), int gridSize, Bf16 const* in, + Bf16 const* weight, Bf16* out, int numRows, int kDim, int outDim, bool enablePdl, cudaStream_t stream) +{ + cudaLaunchConfig_t config{}; + config.gridDim = dim3(gridSize); + config.blockDim = dim3(T::kWarps * kWarpSize); + config.dynamicSmemBytes = 0; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = enablePdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 1; + return cudaLaunchKernelEx(&config, kernel, in, weight, out, numRows, kDim, outDim); +} + +//! Variant table (warps, steps); the template arguments of each index are in launchVariant. +struct VariantDesc +{ + int warps; + int steps; +}; + +constexpr VariantDesc kVariants[] = { + {4, 8}, // 0: down default. L2 prefetch one chunk ahead + {8, 4}, // 1: gate/up default. Three CTAs/SM register budget, L2 prefetch one chunk ahead + {8, 4}, // 2: as 1 with ld.global.ca activation loads + {4, 8}, // 3: as 0 without L2 prefetch + {8, 8}, // 4: two CTAs/SM, no L2 prefetch +}; +constexpr int kNumVariants = sizeof(kVariants) / sizeof(kVariants[0]); + +bool isContractionSupported(int kDim, int variant) +{ + VariantDesc const& v = kVariants[variant]; + return kDim > 0 && kDim % (v.warps * kKPerStep * v.steps) == 0; +} + +//! Dispatches a variant index to its kernel instance; kGateUp selects the gate/up or the down kernel. +template +cudaError_t launchVariant(int variant, Bf16 const* in, Bf16 const* weight, Bf16* out, int numRows, int kDim, + int outDim, bool enablePdl, cudaStream_t stream) +{ + auto launch = [&](auto tiling) -> cudaError_t + { + using T = decltype(tiling); + if constexpr (kGateUp) + { + return launchKernel(gateUpSwigluKernel, outDim / kGateUpCols, in, weight, out, numRows, kDim, outDim, + enablePdl, stream); + } + else + { + return launchKernel( + downKernel, outDim / kTileRows, in, weight, out, numRows, kDim, outDim, enablePdl, stream); + } + }; + switch (variant) + { + case 0: return launch(Tiling<4, 8, 1, false, 1>{}); + case 1: return launch(Tiling<8, 4, 3, false, 1>{}); + case 2: return launch(Tiling<8, 4, 3, true, 1>{}); + case 3: return launch(Tiling<4, 8, 1, false, 0>{}); + case 4: return launch(Tiling<8, 8, 2, false, 0>{}); + default: return cudaErrorInvalidValue; + } +} + +} // namespace + +int getNumVariants() +{ + return kNumVariants; +} + +bool isGateUpSupported(int numRows, int hidden, int inter, int variant) +{ + return variant >= 0 && variant < kNumVariants && numRows >= 1 && numRows <= kMaxRows && inter > 0 + && inter % kGateUpCols == 0 && isContractionSupported(hidden, variant); +} + +bool isDownSupported(int numRows, int inter, int hidden, int variant) +{ + return variant >= 0 && variant < kNumVariants && numRows >= 1 && numRows <= kMaxRows && hidden > 0 + && hidden % kTileRows == 0 && isContractionSupported(inter, variant); +} + +cudaError_t invokeGateUpSwiglu(Bf16 const* x, Bf16 const* gateUpWeight, Bf16* h, int numRows, int hidden, int inter, + int variant, bool enablePdl, cudaStream_t stream) +{ + if (!isGateUpSupported(numRows, hidden, inter, variant)) + { + return cudaErrorInvalidValue; + } + return launchVariant(variant, x, gateUpWeight, h, numRows, hidden, inter, enablePdl, stream); +} + +cudaError_t invokeDown(Bf16 const* h, Bf16 const* downWeight, Bf16* out, int numRows, int inter, int hidden, + int variant, bool enablePdl, cudaStream_t stream) +{ + if (!isDownSupported(numRows, inter, hidden, variant)) + { + return cudaErrorInvalidValue; + } + return launchVariant(variant, h, downWeight, out, numRows, inter, hidden, enablePdl, stream); +} + +} // namespace kernels::glm_moe_dsa::dense_mlp + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/denseMlp.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/denseMlp.h new file mode 100644 index 000000000000..3d82801ffca0 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/denseMlp.h @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::dense_mlp +{ + +//! Largest number of activation rows (tokens) the small-M dense MLP kernels accept. +constexpr int kMaxRows = 8; + +//! Number of tiling variants compiled for each kernel. Variant 0 is the default. +int getNumVariants(); + +//! Host-side eligibility checks (no device work). +//! \param numRows activation rows M. +//! \param hidden model hidden size (gate/up contraction size, down output size). +//! \param inter rank-local intermediate size (gate/up output size, down contraction size). +bool isGateUpSupported(int numRows, int hidden, int inter, int variant); +bool isDownSupported(int numRows, int inter, int hidden, int variant); + +//! Gate/up projection fused with SwiGLU. +//! For every row m < numRows and column j < inter: +//! gate = bf16(sum_k x[m, k] * W[j, k]), up = bf16(sum_k x[m, k] * W[inter + j, k]), +//! h[m, j] = bf16((gate * sigmoid(gate)) * up), all sums in FP32. +//! \param x activations [numRows, hidden], row-major, BF16. +//! \param gateUpWeight [2 * inter, hidden], row-major, BF16 (rows [0, inter) gate, [inter, 2 * inter) up). +//! \param h output [numRows, inter], row-major, BF16. +cudaError_t invokeGateUpSwiglu(__nv_bfloat16 const* x, __nv_bfloat16 const* gateUpWeight, __nv_bfloat16* h, int numRows, + int hidden, int inter, int variant, bool enablePdl, cudaStream_t stream); + +//! Down projection: out[m, n] = bf16(sum_k h[m, k] * W[n, k]) with FP32 sums. +//! \param h activations [numRows, inter], row-major, BF16. +//! \param downWeight [hidden, inter], row-major, BF16. +//! \param out output [numRows, hidden], row-major, BF16. +cudaError_t invokeDown(__nv_bfloat16 const* h, __nv_bfloat16 const* downWeight, __nv_bfloat16* out, int numRows, + int inter, int hidden, int variant, bool enablePdl, cudaStream_t stream); + +} // namespace kernels::glm_moe_dsa::dense_mlp + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.cu new file mode 100644 index 000000000000..33bfd92f7cef --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.cu @@ -0,0 +1,586 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.h" + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::draft_live_rows +{ +namespace +{ +namespace cg = cooperative_groups; + +// Work decomposition (the post-attention router hop's, on the compact rows). +// +// Every cluster has kSlices CTAs; CTA c owns slice [c * kSlice, (c + 1) * kSlice) of the hidden dimension (96 threads +// x 8 elements), the slice a CTA of the native one-shot kernel owns, so the RMSNorm reduction reproduces the native +// tree. There are kG x kRowSplit clusters (kG = 4 expert groups, kRowSplit = min(R, 3) row groups, R = live rows). +// Cluster (g, h) collects and normalizes the compact rows r with r % kRowSplit == h and computes their logits for +// experts [64 g, 64 g + 64). Its router weights live in registers (loaded before the PDL wait). Each CTA multiplies +// its rows of slice c by the experts' weights of slice c with BF16 MMAs (experts on m16, rows on n8) and pushes the +// partial logits to the CTA that owns the expert; owners add the 8 slices in a fixed order, so the logits are +// identical on every rank and do not depend on R or on the row's position. Clusters never synchronize with each +// other; inside a cluster, slice sums and partial logits travel as st.async stores that complete transactions on the +// consumer's mbarrier. +constexpr int kSlices = 8; +constexpr int kSlice = kHiddenDim / kSlices; +constexpr int kEltsPerVec = 8; +constexpr int kVecPerSlice = kSlice / kEltsPerVec; +constexpr int kNativeWarpsPerSlice = kVecPerSlice / 32; +// Row stride of the normalized-slice tile in 16-byte vectors; 100 = 96 + 4 keeps the 16-byte B-fragment loads of two +// rows in one quarter-warp phase on disjoint banks. +constexpr int kNormedStride = kVecPerSlice + 4; +constexpr int kMmaChains = 4; +constexpr int kExpertGroups = 4; +constexpr uint32_t kLamportSentinel = 0x80000000U; + +static_assert(kHiddenDim % (kSlices * kEltsPerVec * 32) == 0); +static_assert(kVecPerSlice == kNativeWarpsPerSlice * 32); +static_assert(kMaxRows == 8, "the router MMA maps rows onto the n8 dimension"); + +struct HopConfig +{ + static constexpr int kThreads = 384; + static constexpr int kWarps = kThreads / 32; + static constexpr int kExpertsPerCluster = kNumExperts / kExpertGroups; + static constexpr int kExpertTiles = kExpertsPerCluster / 16; + static constexpr int kKGroups = kWarps / kExpertTiles; + static constexpr int kKPerGroup = kSlice / kKGroups; + static constexpr int kPairsPerGroup = kKPerGroup / 32; + static constexpr int kExpertsPerOwner = kExpertsPerCluster / kSlices; + static_assert(kExpertsPerCluster % 16 == 0 && kWarps % kExpertTiles == 0 && kSlice % (kKGroups * 32) == 0); + static_assert(kPairsPerGroup % (kMmaChains / 2) == 0); +}; + +constexpr int kMaxRowsPerCtaAll = kMaxRows / 2; + +struct alignas(16) HopStorage +{ + uint4 gamma[kVecPerSlice]; + float vecSq[kMaxRowsPerCtaAll][kVecPerSlice]; + float warpSq[kMaxRowsPerCtaAll][kNativeWarpsPerSlice]; + float blockSq[kMaxRowsPerCtaAll][kSlices]; + // Completes when all slice sums of this CTA's rows have arrived (st.async complete_tx). + alignas(8) uint64_t normBar; + // Completes when all partial logits of the experts this CTA owns have arrived. + alignas(8) uint64_t routerBar; +}; + +struct alignas(16) RouterStorage +{ + uint4 normed[kMaxRows][kNormedStride]; // local row on the MMA n dimension; unused rows stay zero + float partial[kSlices][HopConfig::kKGroups][HopConfig::kExpertsPerOwner][kMaxRows]; +}; + +union BfVec +{ + uint4 packed; + __nv_bfloat16 elts[kEltsPerVec]; +}; + +__device__ __forceinline__ uint4 loadVolatile(void const* ptr) +{ + uint4 v; + asm volatile("ld.volatile.global.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) + : "l"(ptr) + : "memory"); + return v; +} + +__device__ __forceinline__ uint4 loadConstant(void const* ptr) +{ + uint4 v; + asm volatile("ld.global.nc.v4.u32 {%0, %1, %2, %3}, [%4];" : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) : "l"(ptr)); + return v; +} + +__device__ __forceinline__ bool isDirty(uint4 const& v) +{ + return v.x == kLamportSentinel || v.y == kLamportSentinel || v.z == kLamportSentinel || v.w == kLamportSentinel; +} + +// Lamport payloads must never contain the sentinel word: map bf16 -0.0 to +0.0 (the reduction starts from +0.0, so +// the sum is unchanged). +__device__ __forceinline__ uint4 sanitize(uint4 v) +{ + BfVec b; + b.packed = v; +#pragma unroll + for (int i = 0; i < kEltsPerVec; ++i) + { + if (__bfloat16_as_ushort(b.elts[i]) == 0x8000U) + { + b.elts[i] = __ushort_as_bfloat16(0); + } + } + return b.packed; +} + +__device__ __forceinline__ uint32_t sharedAddr(void const* ptr) +{ + return static_cast(__cvta_generic_to_shared(ptr)); +} + +// Address of the same shared-memory object in CTA `rank` of the cluster. +__device__ __forceinline__ uint32_t clusterAddr(uint32_t addr, uint32_t rank) +{ + uint32_t out; + asm volatile("mapa.shared::cluster.u32 %0, %1, %2;" : "=r"(out) : "r"(addr), "r"(rank)); + return out; +} + +__device__ __forceinline__ void mbarrierInitExpectTx(uint64_t* bar, uint32_t bytes) +{ + asm volatile("mbarrier.init.shared::cta.b64 [%0], 1;" ::"r"(sharedAddr(bar)) : "memory"); + asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;" ::"r"(sharedAddr(bar)), "r"(bytes) + : "memory"); +} + +__device__ __forceinline__ void mbarrierWaitPhase0(uint64_t* bar) +{ + asm volatile( + "{\n" + ".reg .pred done;\n" + "LIVE_ROW_MBAR_WAIT_%=:\n" + "mbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 done, [%0], 0;\n" + "@!done bra LIVE_ROW_MBAR_WAIT_%=;\n" + "}\n" ::"r"(sharedAddr(bar)) + : "memory"); +} + +// Remote shared-memory stores that complete transactions on the destination CTA's mbarrier. +__device__ __forceinline__ void storeAsync(uint32_t remoteAddr, float v, uint32_t remoteBar) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.f32 [%0], %1, [%2];" ::"r"(remoteAddr), "f"(v), + "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void storeAsync(uint32_t remoteAddr, float a, float b, uint32_t remoteBar) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.v2.f32 [%0], {%1, %2}, [%3];" ::"r"(remoteAddr), + "f"(a), "f"(b), "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void mmaBf16( + float (&d)[4], uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t b0, uint32_t b1) +{ + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 {%0, %1, %2, %3}, {%4, %5, %6, %7}, {%8, %9}, " + "{%0, %1, %2, %3};" + : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b0), "r"(b1)); +} + +// R = live (compact) rows; kRowSplit = row groups. +template +__global__ void __launch_bounds__(HopConfig::kThreads, 1) liveRowHopKernel(LiveRowHopParams const params) +{ + static_assert(R >= 1 && R <= kMaxRows); +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + using Cfg = HopConfig; + constexpr int kClusters = kExpertGroups * kRowSplit; + constexpr int kGridCtas = kClusters * kSlices; + // Rows one CTA collects, normalizes and multiplies. + constexpr int kRowsPerCta = (R + kRowSplit - 1) / kRowSplit; + static_assert(kRowsPerCta <= kMaxRowsPerCtaAll); + static_assert(kRowsPerCta * kVecPerSlice <= Cfg::kThreads && kRowsPerCta * kNativeWarpsPerSlice <= Cfg::kWarps); + static_assert(Cfg::kExpertsPerOwner * kRowsPerCta * kSlices <= Cfg::kThreads); + + __shared__ HopStorage hop; + __shared__ RouterStorage rs; + + cg::cluster_group cluster = cg::this_cluster(); + int const slice = static_cast(cluster.block_rank()); + int const clusterId = blockIdx.y; + int const expertGroup = clusterId % kExpertGroups; + int const rowGroup = clusterId / kExpertGroups; + int const tid = threadIdx.x; + int const lane = tid & 31; + int const warp = tid >> 5; + int const sliceBase = slice * kSlice; + + // Compact rows this CTA collects and normalizes: local row j is compact row rowGroup + j * kRowSplit. + int const numRowsCta = (R - rowGroup + kRowSplit - 1) / kRowSplit; + // This thread's item: one 16-byte vector of one row slice. + int const itemRow = tid / kVecPerSlice; + int const itemVec = tid % kVecPerSlice; + bool const hasItem = itemRow < numRowsCta; + int const row = rowGroup + itemRow * kRowSplit; + // The rank's own partial of a row slice is published by exactly one CTA of the grid. + bool const sends = hasItem && itemRow % kExpertGroups == expertGroup; + size_t const colOff = static_cast(sliceBase) + itemVec * kEltsPerVec; + size_t const rowOff = static_cast(row) * kHiddenDim + colOff; + // The cluster that writes the normalized rows and the residual. + bool const writesOutputs = expertGroup == 0; + + // ---- Prologue: constant model weights only; may overlap the producer of `input` under PDL. ---- + if (tid < kVecPerSlice) + { + hop.gamma[tid] = loadConstant(reinterpret_cast(params.gamma) + slice * kVecPerSlice + tid); + } + int const gid = lane >> 2; + int const tig = lane & 3; + uint4 wLo[Cfg::kPairsPerGroup]; + uint4 wHi[Cfg::kPairsPerGroup]; + { + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + auto const* w = reinterpret_cast<__nv_bfloat16 const*>(params.routerWeight); + int const expert = expertGroup * Cfg::kExpertsPerCluster + tile * 16 + gid; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + int const k = sliceBase + kGroup * Cfg::kKPerGroup + p * 32 + tig * kEltsPerVec; + wLo[p] = loadConstant(w + static_cast(expert) * kHiddenDim + k); + wHi[p] = loadConstant(w + static_cast(expert + 8) * kHiddenDim + k); + } + // MMA columns of rows this CTA does not own stay zero. + for (int i = tid; i < kMaxRows * kNormedStride; i += Cfg::kThreads) + { + rs.normed[i / kNormedStride][i % kNormedStride] = make_uint4(0, 0, 0, 0); + } + } + + // Cluster-local completion barriers: every CTA of a cluster has the same rows, so each expects the 8 slice sums + // of its rows and the 8 x kKGroups partial tiles of the experts it owns. A split cluster barrier publishes the + // initialization; its wait precedes the first remote store, long after all CTAs arrived. + if (tid == 0) + { + mbarrierInitExpectTx(&hop.normBar, static_cast(numRowsCta * kSlices * sizeof(float))); + mbarrierInitExpectTx(&hop.routerBar, numRowsCta > 0 ? static_cast(sizeof(rs.partial)) : 0U); + asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory"); + } + asm volatile("barrier.cluster.arrive.release.aligned;" ::: "memory"); + + // ---- Wait for the producer of `input` (and transitively of `residualIn`, `rowIds` and the protocol state). ---- + asm volatile("griddepcontrol.wait;" ::: "memory"); + // Dependents only start their own prologue; they still wait for this grid to complete. + asm volatile("griddepcontrol.launch_dependents;"); + + // The live row's source row (written by an earlier kernel of the stream: a plain load after the wait). + int64_t srcRow = 0; + if (hasItem) + { + srcRow = params.rowIds[row]; + } + uint4 const flags = loadVolatile(params.flags); + srcRow = srcRow < 0 ? 0 : (srcRow >= params.numInputRows ? params.numInputRows - 1 : srcRow); + size_t const srcOff = static_cast(srcRow) * kHiddenDim + colOff; + auto const* input = reinterpret_cast<__nv_bfloat16 const*>(params.input); + auto const* residualIn = reinterpret_cast<__nv_bfloat16 const*>(params.residualIn); + uint4 res = make_uint4(0, 0, 0, 0); + // This rank's own partial, as published (sanitized); it is used from registers, not from the multicast copy. + uint4 own = make_uint4(0, 0, 0, 0); + if (hasItem) + { + res = *reinterpret_cast(residualIn + srcOff); + own = sanitize(*reinterpret_cast(input + srcOff)); + } + uint32_t const cur = flags.x; + uint32_t const clearBytes = flags.y; + char* ucBase = reinterpret_cast(params.ucBuffer); + auto* curUc = reinterpret_cast<__nv_bfloat16*>(ucBase + cur * kLamportBufferBytes); + auto* curMc + = reinterpret_cast<__nv_bfloat16*>(reinterpret_cast(params.mcBuffer) + cur * kLamportBufferBytes); + // Lamport layout of one buffer: [compact row][source rank][kHiddenDim]. + size_t const slotBase = static_cast(row) * kNumRanks * kHiddenDim + colOff; + + // ---- Publish this rank's partial to every rank (one multicast store per vector). ---- + if (sends) + { + *reinterpret_cast(curMc + slotBase + static_cast(params.rank) * kHiddenDim) = own; + } + + // ---- Collect the other ranks' partials. ---- + uint4 vals[kNumRanks]; + if (hasItem) + { +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + vals[s] = s == params.rank ? own : loadVolatile(curUc + slotBase + static_cast(s) * kHiddenDim); + } + } + + // ---- Reset the previous call's buffer to the sentinel (reused two calls from now), off the poll's path. ---- + { + uint32_t const dirty = (cur + kNumLamportBuffers - 1) % kNumLamportBuffers; + auto* dirtyBuf = reinterpret_cast(ucBase + dirty * kLamportBufferBytes); + uint32_t const numVec = clearBytes / sizeof(uint4); + uint4 const sentinel = make_uint4(kLamportSentinel, kLamportSentinel, kLamportSentinel, kLamportSentinel); + uint32_t const ctaLinear = clusterId * kSlices + slice; + for (uint32_t j = ctaLinear * Cfg::kThreads + tid; j < numVec; j += kGridCtas * Cfg::kThreads) + { + dirtyBuf[j] = sentinel; + } + } + + if (hasItem) + { + bool pending = true; + while (pending) + { + pending = false; +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + if (isDirty(vals[s])) + { + pending = true; + vals[s] = loadVolatile(curUc + slotBase + static_cast(s) * kHiddenDim); + } + } + } + } + + // ---- Reduce in rank order, add the residual, and form per-vector sums of squares (native arithmetic). ---- + uint4 resOut = make_uint4(0, 0, 0, 0); + if (hasItem) + { + float acc[kEltsPerVec]; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + acc[e] = 0.F; + } +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + BfVec x; + x.packed = vals[s]; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + acc[e] += __bfloat162float(x.elts[e]); + } + } + BfVec rv; + rv.packed = res; + BfVec y; + float sq = 0.F; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + y.elts[e] = __hadd(__float2bfloat16(acc[e]), rv.elts[e]); + sq += __bfloat162float(__hmul(y.elts[e], y.elts[e])); + } + resOut = y.packed; + hop.vecSq[itemRow][itemVec] = sq; + } + __syncthreads(); + if (tid == 0) + { + // Every thread of this CTA has read the protocol state; CTA 0 advances it only after all CTAs report. + asm volatile("red.release.gpu.global.add.u32 [%0], %1;" ::"l"(params.flags + 2), "r"(1U) : "memory"); + } + + // ---- RMS: warp butterfly over 32 vectors, then (w0 + w2) + w1 per slice, then the 8 slices in order. ---- + if (warp < numRowsCta * kNativeWarpsPerSlice) + { + int const j = warp / kNativeWarpsPerSlice; + int const q = warp % kNativeWarpsPerSlice; + float val = hop.vecSq[j][q * 32 + lane]; +#pragma unroll + for (int mask = 16; mask > 0; mask >>= 1) + { + val = val + __shfl_xor_sync(0xffffffffU, val, mask, 32); + } + if (lane == 0) + { + hop.warpSq[j][q] = val; + } + } + __syncthreads(); + asm volatile("barrier.cluster.wait.acquire.aligned;" ::: "memory"); + if (tid < numRowsCta * kSlices) + { + int const j = tid / kSlices; + int const dst = tid % kSlices; + float const blockSum = (hop.warpSq[j][0] + hop.warpSq[j][2]) + hop.warpSq[j][1]; + storeAsync( + clusterAddr(sharedAddr(&hop.blockSq[j][slice]), dst), blockSum, clusterAddr(sharedAddr(&hop.normBar), dst)); + } + mbarrierWaitPhase0(&hop.normBar); + + if (hasItem) + { + float fullSum = 0.F; +#pragma unroll + for (int c = 0; c < kSlices; ++c) + { + fullSum += hop.blockSq[itemRow][c]; + } + float const rcpRms = rsqrtf(fullSum / static_cast(kHiddenDim) + params.eps); + BfVec y; + y.packed = resOut; + BfVec g; + g.packed = hop.gamma[itemVec]; + BfVec n; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + n.elts[e] = __float2bfloat16(__bfloat162float(y.elts[e]) * rcpRms * __bfloat162float(g.elts[e])); + } + if (writesOutputs) + { + *reinterpret_cast(reinterpret_cast<__nv_bfloat16*>(params.normOut) + rowOff) = n.packed; + *reinterpret_cast(reinterpret_cast<__nv_bfloat16*>(params.residualOut) + rowOff) = y.packed; + } + rs.normed[itemRow][itemVec] = n.packed; + } + + if (numRowsCta > 0) + { + __syncthreads(); + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + // Independent accumulator chains keep the MMA latency off a single dependency chain. + float acc[kMmaChains][4] = {}; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + // One 16-byte load covers two k16 steps; the k-permutation inside a step is the same for both operands. + uint4 const b = rs.normed[gid][kGroup * (Cfg::kKPerGroup / kEltsPerVec) + p * 4 + tig]; + int const chain = (2 * p) % kMmaChains; + mmaBf16(acc[chain], wLo[p].x, wHi[p].x, wLo[p].y, wHi[p].y, b.x, b.y); + mmaBf16(acc[chain + 1], wLo[p].z, wHi[p].z, wLo[p].w, wHi[p].w, b.z, b.w); + } + float sum4[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) + { + sum4[i] = (acc[0][i] + acc[1][i]) + (acc[2][i] + acc[3][i]); + } + // Expert e (cluster-local) is owned by CTA e / kExpertsPerOwner. + int const e0 = tile * 16 + gid; + int const e1 = e0 + 8; + uint32_t const owner0 = e0 / Cfg::kExpertsPerOwner; + uint32_t const owner1 = e1 / Cfg::kExpertsPerOwner; + storeAsync(clusterAddr(sharedAddr(&rs.partial[slice][kGroup][e0 % Cfg::kExpertsPerOwner][tig * 2]), owner0), + sum4[0], sum4[1], clusterAddr(sharedAddr(&hop.routerBar), owner0)); + storeAsync(clusterAddr(sharedAddr(&rs.partial[slice][kGroup][e1 % Cfg::kExpertsPerOwner][tig * 2]), owner1), + sum4[2], sum4[3], clusterAddr(sharedAddr(&hop.routerBar), owner1)); + // Also keeps this CTA resident until every partial addressed to it has landed. + mbarrierWaitPhase0(&hop.routerBar); + + // This CTA owns experts [slice * kExpertsPerOwner, (slice + 1) * kExpertsPerOwner) of the cluster. Lane groups + // of 8 sum over the 8 slices. + if (tid < Cfg::kExpertsPerOwner * numRowsCta * kSlices) + { + int const c = tid % kSlices; + int const ij = tid / kSlices; + int const ei = ij / numRowsCta; + int const j = ij % numRowsCta; + float sum = 0.F; +#pragma unroll + for (int kg = 0; kg < Cfg::kKGroups; ++kg) + { + sum += rs.partial[c][kg][ei][j]; + } +#pragma unroll + for (int mask = kSlices / 2; mask > 0; mask >>= 1) + { + sum = sum + __shfl_xor_sync(0xffffffffU, sum, mask, kSlices); + } + if (c == 0) + { + int const r = rowGroup + j * kRowSplit; + params.routerLogits[r * kNumExperts + expertGroup * Cfg::kExpertsPerCluster + + slice * Cfg::kExpertsPerOwner + ei] + = sum; + } + } + } + + // ---- CTA 0 advances the protocol state once every CTA has read it. ---- + if (tid == 0 && slice == 0 && clusterId == 0) + { + uint32_t arrived = 0; + do + { + asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(arrived) : "l"(params.flags + 2) : "memory"); + } while (arrived < static_cast(kGridCtas)); + // The next call clears exactly the slots this call wrote: compact rows 0..R-1 of every rank. + uint4 const next = make_uint4((cur + 1) % kNumLamportBuffers, + static_cast(R * kNumRanks * kHiddenDim * sizeof(__nv_bfloat16)), 0U, 0U); + asm volatile("st.relaxed.gpu.global.v4.u32 [%0], {%1, %2, %3, %4};" ::"l"(params.flags), "r"(next.x), + "r"(next.y), "r"(next.z), "r"(next.w) + : "memory"); + } +#else + // Clusters, distributed shared memory and PDL need SM90+; the launcher is only reached on supported devices. + __trap(); +#endif +} + +template +cudaError_t launchTyped(LiveRowHopParams const& params) +{ + cudaLaunchAttribute attrs[2]; + attrs[0].id = cudaLaunchAttributeClusterDimension; + attrs[0].val.clusterDim.x = kSlices; + attrs[0].val.clusterDim.y = 1; + attrs[0].val.clusterDim.z = 1; + attrs[1].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[1].val.programmaticStreamSerializationAllowed = params.enablePdl ? 1 : 0; + + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kSlices, kExpertGroups * kRowSplit, 1); + config.blockDim = dim3(HopConfig::kThreads, 1, 1); + config.dynamicSmemBytes = 0; + config.stream = params.stream; + config.attrs = attrs; + config.numAttrs = 2; + return cudaLaunchKernelEx(&config, liveRowHopKernel, params); +} + +} // namespace + +cudaError_t invokeDraftLiveRowHop(LiveRowHopParams const& params) +{ + if (params.rank < 0 || params.rank >= kNumRanks || params.flags == nullptr || params.ucBuffer == nullptr + || params.mcBuffer == nullptr || params.rowIds == nullptr || params.routerWeight == nullptr + || params.routerLogits == nullptr || params.numInputRows < 1 || params.numInputRows > kMaxRows + || params.numRows < 1 || params.numRows > params.numInputRows) + { + return cudaErrorInvalidValue; + } + // 4 expert groups x min(R, 3) row groups (at most 12 clusters of 8 CTAs, each CTA <= 3 row slices). + switch (params.numRows) + { + case 1: return launchTyped<1, 1>(params); + case 2: return launchTyped<2, 2>(params); + case 3: return launchTyped<3, 3>(params); + case 4: return launchTyped<4, 3>(params); + case 5: return launchTyped<5, 3>(params); + case 6: return launchTyped<6, 3>(params); + case 7: return launchTyped<7, 3>(params); + case 8: return launchTyped<8, 3>(params); + default: return cudaErrorInvalidValue; + } +} + +} // namespace kernels::glm_moe_dsa::draft_live_rows + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.h new file mode 100644 index 000000000000..a6c6f40b40f1 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.h @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::draft_live_rows +{ + +//! Live-row post-attention hop (MTP draft step 0). +//! +//! A fork of the router hop of postAttentionHop.h that serves only the ``numRows`` live rows of a call with +//! ``numInputRows`` rows. Compact row ``r`` (0 <= r < numRows) is source row ``rowIds[r]`` of ``input`` and +//! ``residualIn``; for it the kernel computes, on every rank and with identical bits on every rank: +//! sum = bf16(sum over ranks 0..7 of float(partial_rank)) (one-shot Lamport exchange, fixed order) +//! residual = sum + residualIn (bf16 add) +//! normed = bf16(float(residual) * rsqrt(mean(bf16(residual^2)) + eps) * float(gamma)) +//! logits = normed @ routerWeight^T in FP32 ([numRows, kNumExperts]) +//! Every quantity of a row is computed by the same per-row arithmetic as the post-attention hop, so compact row ``r`` +//! equals row ``rowIds[r]`` of a post-attention hop over all ``numInputRows`` rows. +//! +//! ``rowIds`` is read on the device after the kernel's programmatic dependency wait, never before it; it may be +//! written by any earlier kernel of the stream. Ids outside ``[0, numInputRows)`` are clamped into that range. +//! +//! The exchange runs on the post-attention hop's multicast workspace and protocol state, with the same layout +//! (``[row][source rank][kHiddenDim]`` bf16 per Lamport buffer, ``kNumLamportBuffers`` rotating buffers, flags +//! ``{current buffer, bytes to clear in the previous buffer, arrival counter, unused}``), so calls of both kernels +//! may be issued in any order on the stream that issues the group's hops. +struct LiveRowHopParams +{ + void const* input = nullptr; //!< [numInputRows, kHiddenDim] bf16, rank-local partial (o_proj output) + void const* residualIn = nullptr; //!< [numInputRows, kHiddenDim] bf16 + int64_t const* rowIds = nullptr; //!< [numRows] device ids of the live rows in [0, numInputRows) + void const* gamma = nullptr; //!< [kHiddenDim] bf16 RMSNorm weight + void const* routerWeight = nullptr; //!< [kNumExperts, kHiddenDim] bf16 + void* normOut = nullptr; //!< [numRows, kHiddenDim] bf16 + void* residualOut = nullptr; //!< [numRows, kHiddenDim] bf16 + float* routerLogits = nullptr; //!< [numRows, kNumExperts] fp32 + void* ucBuffer = nullptr; //!< local unicast view of the multicast workspace + void* mcBuffer = nullptr; //!< multicast address of the same workspace + uint32_t* flags = nullptr; //!< [4] device-side protocol state of this workspace + int numRows = 0; //!< live rows, 1 <= numRows <= numInputRows + int numInputRows = 0; //!< rows of ``input`` / ``residualIn``, <= kMaxRows + int rank = 0; + float eps = 1e-5F; + bool enablePdl = true; + cudaStream_t stream = nullptr; +}; + +// The post-attention hop's workspace geometry (the kernel runs on that workspace). +constexpr int kHiddenDim = 6144; +constexpr int kNumRanks = 8; +constexpr int kNumExperts = 256; +constexpr int kMaxRows = 8; +constexpr int kNumLamportBuffers = 3; + +//! Bytes of one Lamport buffer: [kMaxRows][kNumRanks][kHiddenDim] bf16. +constexpr size_t kLamportBufferBytes = size_t(kMaxRows) * kNumRanks * kHiddenDim * 2; +//! Bytes of the whole multicast workspace. +constexpr size_t kWorkspaceBytes = kLamportBufferBytes * kNumLamportBuffers; + +//! Launch the live-row hop on ``params.stream``. Returns the launch status. +cudaError_t invokeDraftLiveRowHop(LiveRowHopParams const& params); + +} // namespace kernels::glm_moe_dsa::draft_live_rows + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.cu new file mode 100644 index 000000000000..a4ca68a4859d --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.cu @@ -0,0 +1,2628 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.h" + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::draft_routed_moe +{ + +namespace +{ + +// Ring depths (2 KB chunks per warp; the deeper pair with kFlagDeepRing) and the FC2 intermediate ring (one entry +// per expert of a tile). +constexpr int kFc1Stages = 12; +constexpr int kFc1StagesDeep = 20; +constexpr int kFc2Stages = 8; +constexpr int kFc2StagesDeep = 12; +constexpr int kFc2HStages = 6; +constexpr int kChunkBytes = 16 * kKChunk * 2; // 16 rows x 128 bytes +constexpr int kXRowBytes = kHidden * 2; // 12288 +constexpr int kHEntryBytes = kMaxRows * kInter * 2; // 4096 +constexpr int kStageBytes = kMaxRows * kTopK * 16 * 2; +constexpr float kSumEpsilon = 1e-20f; +constexpr float kBf16MaxFinite = 3.38953139e38f; +// Bytes of one FC2 weight block of an expert (one K chunk of all 6144 rows) and of one L2 prefetch piece. +constexpr int kFc2BlockBytes = kHidden * kKChunk * 2; +constexpr int kPrefetchPieceBytes = 16384; +constexpr int kPrefetchPiecesPerBlock = kFc2BlockBytes / kPrefetchPieceBytes; +static_assert(kFc2BlockBytes % kPrefetchPieceBytes == 0, "prefetch pieces must tile an FC2 block"); +// FC2 intermediate ring: expert s (in the warp's order over its items) has its K chunks at stream positions +// 4s..4s+3 (alone or as the first of a pair) or 4s-3, 4s-1, 4s+1, 4s+3 (second of a pair), so it is last read at +// iteration 4s+7 or earlier, and expert s + kFc2HStages loads into the same entry together with its first chunk, at +// position >= 4(s + kFc2HStages) - 3, issued at the end of iteration (position - kStages + 1). The entry is free when +// that iteration is not earlier than the last read: kStages <= 4 * kFc2HStages - 9. +static_assert(kFc2Stages <= 4 * kFc2HStages - 9 && kFc2StagesDeep <= 4 * kFc2HStages - 9, + "an FC2 intermediate entry is refilled before its last read"); + +// --------------------------------------------------------------------------------------------------------------- +// Arithmetic with the exact instructions of the native kernels. +// --------------------------------------------------------------------------------------------------------------- + +__device__ __forceinline__ float mulFtz(float a, float b) +{ + float r; + asm("mul.ftz.f32 %0, %1, %2;" : "=f"(r) : "f"(a), "f"(b)); + return r; +} + +__device__ __forceinline__ float addFtz(float a, float b) +{ + float r; + asm("add.ftz.f32 %0, %1, %2;" : "=f"(r) : "f"(a), "f"(b)); + return r; +} + +__device__ __forceinline__ float fmaFtz(float a, float b, float c) +{ + float r; + asm("fma.rn.ftz.f32 %0, %1, %2, %3;" : "=f"(r) : "f"(a), "f"(b), "f"(c)); + return r; +} + +__device__ __forceinline__ float tanhApprox(float x) +{ + float r; + asm("tanh.approx.f32 %0, %1;" : "=f"(r) : "f"(x)); + return r; +} + +__device__ __forceinline__ float divApproxFtz(float a, float b) +{ + float r; + asm("div.approx.ftz.f32 %0, %1, %2;" : "=f"(r) : "f"(a), "f"(b)); + return r; +} + +__device__ __forceinline__ float ex2ApproxFtz(float x) +{ + float r; + asm("ex2.approx.ftz.f32 %0, %1;" : "=f"(r) : "f"(x)); + return r; +} + +__device__ __forceinline__ float rcpApproxFtz(float x) +{ + float r; + asm("rcp.approx.ftz.f32 %0, %1;" : "=f"(r) : "f"(x)); + return r; +} + +__device__ __forceinline__ float mulRn(float a, float b) +{ + float r; + asm("mul.rn.f32 %0, %1, %2;" : "=f"(r) : "f"(a), "f"(b)); + return r; +} + +__device__ __forceinline__ float addRn(float a, float b) +{ + float r; + asm("add.rn.f32 %0, %1, %2;" : "=f"(r) : "f"(a), "f"(b)); + return r; +} + +//! FlashInfer ``sigmoid_accurate`` as compiled with ``-use_fast_math``. +__device__ __forceinline__ float routingSigmoid(float x) +{ + return fmaFtz(tanhApprox(mulFtz(x, 0.5f)), 0.5f, 0.5f); +} + +//! Round to BF16 like ``F2FP.SATFINITE.BF16.F32``: round to nearest even, overflow saturates, NaN stays NaN. +__device__ __forceinline__ __nv_bfloat16 bf16SatFinite(float x) +{ + __nv_bfloat16 r = __float2bfloat16_rn(x); + float back = __bfloat162float(r); + if (back == INFINITY || back == -INFINITY) + { + r = __float2bfloat16_rn(x > 0.f ? kBf16MaxFinite : -kBf16MaxFinite); + } + return r; +} + +//! The native gated epilogue (``swiGlu``, no clamp limit, alpha 1, beta 0): ``(up + 0) * (gate * sigmoid(gate))`` +//! with ``sigmoid(g) = rcp.approx(1 + ex2.approx(-(g * log2 e)))``; both inputs are clamped to +-FLT_MAX first. +__device__ __forceinline__ float swiGlu(float up, float gate) +{ + float const g = gate > FLT_MAX ? FLT_MAX : (gate < -FLT_MAX ? -FLT_MAX : gate); + float const u = up > FLT_MAX ? FLT_MAX : (up < -FLT_MAX ? -FLT_MAX : up); + float const t = mulRn(mulRn(g, 1.4426950216293334961f), -1.f); + float const s = rcpApproxFtz(addRn(ex2ApproxFtz(t), 1.f)); + return mulRn(addRn(u, 0.f), mulRn(g, s)); +} + +// --------------------------------------------------------------------------------------------------------------- +// Data movement and MMA. +// --------------------------------------------------------------------------------------------------------------- + +__device__ __forceinline__ uint32_t smemAddr(void const* p) +{ + return static_cast(__cvta_generic_to_shared(p)); +} + +__device__ __forceinline__ void cpAsync16(uint32_t dst, void const* src) +{ + asm volatile("cp.async.cg.shared.global [%0], [%1], 16;" ::"r"(dst), "l"(__cvta_generic_to_global(src)) : "memory"); +} + +__device__ __forceinline__ void cpAsyncCommit() +{ + asm volatile("cp.async.commit_group;" ::: "memory"); +} + +template +__device__ __forceinline__ void cpAsyncWait() +{ + asm volatile("cp.async.wait_group %0;" ::"n"(N) : "memory"); +} + +__device__ __forceinline__ void ldmatrixX4(uint32_t (&r)[4], uint32_t addr) +{ + asm volatile("ldmatrix.sync.aligned.m8n8.x4.shared.b16 {%0, %1, %2, %3}, [%4];" + : "=r"(r[0]), "=r"(r[1]), "=r"(r[2]), "=r"(r[3]) + : "r"(addr)); +} + +__device__ __forceinline__ void mmaBf16(float (&c)[4], uint32_t const (&a)[4], uint32_t b0, uint32_t b1) +{ + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 {%0, %1, %2, %3}, {%4, %5, %6, %7}, {%8, %9}, " + "{%0, %1, %2, %3};" + : "+f"(c[0]), "+f"(c[1]), "+f"(c[2]), "+f"(c[3]) + : "r"(a[0]), "r"(a[1]), "r"(a[2]), "r"(a[3]), "r"(b0), "r"(b1)); +} + +__device__ __forceinline__ void gridDependencyWait() +{ + asm volatile("griddepcontrol.wait;" ::: "memory"); +} + +__device__ __forceinline__ void gridDependencyTrigger() +{ + asm volatile("griddepcontrol.launch_dependents;" ::: "memory"); +} + +//! Byte offset of 16-byte unit ``u`` of a 128-byte row segment for row ``r`` (XOR swizzle, conflict-free ldmatrix). +__device__ __forceinline__ uint32_t swizzled(int r, int u) +{ + return static_cast(((u & ~7) | ((u & 7) ^ (r & 7))) << 4); +} + +//! Copies one 16-row x 64-element weight chunk (2 KB, contiguous in the BlockMajorK layout) into a ring slot. +__device__ __forceinline__ void issueWeightChunk(uint32_t slot, __nv_bfloat16 const* src, int lane) +{ +#pragma unroll + for (int j = 0; j < 4; ++j) + { + int const idx = lane + 32 * j; + int const r = idx >> 3; + int const u = idx & 7; + cpAsync16(slot + r * 128 + swizzled(r, u), src + r * kKChunk + u * 8); + } +} + +//! A fragments of k-step ``q`` (16 elements) of a chunk: rows 0..15, natural K order (m16n8k16 row-major A). +__device__ __forceinline__ void loadA(uint32_t (&a)[4], uint32_t slot, int q, int lane) +{ + int const m = lane >> 3; + int const r = (lane & 7) | ((m & 1) << 3); + int const u = 2 * q + (m >> 1); + ldmatrixX4(a, slot + r * 128 + swizzled(r, u)); +} + +//! B fragments of two consecutive k-steps from a [token][k] BF16 tile (rows ``rowBytes`` apart, units swizzled +//! by token); tokens at or above ``rows`` read token 0 (their columns are discarded). +__device__ __forceinline__ void loadB2(uint32_t (&b)[4], uint32_t base, int rowBytes, int unit, int rows, int lane) +{ + int tok = lane & 7; + tok = tok < rows ? tok : 0; + int const u = unit + (lane >> 3); + ldmatrixX4(b, base + tok * rowBytes + swizzled(tok, u)); +} + +// --------------------------------------------------------------------------------------------------------------- +// Routing (FlashInfer routingCustom: SigmoidBias + ScaledSumNormalize, warp per token). +// --------------------------------------------------------------------------------------------------------------- + +//! Selection key: the score's bits mapped to an unsigned order (a NaN score ranks above every number), then the +//! expert id so that ties go to the lower index. +__device__ __forceinline__ uint64_t packKey(float key, int expert) +{ + uint32_t bits = __float_as_uint(key); + bits = (bits & 0x80000000u) ? ~bits : (bits | 0x80000000u); + bits = key != key ? 0xFFFFFFFFu : bits; + return (static_cast(bits) << 32) | static_cast(0xFFFF & (65535 - expert)); +} + +//! Lexicographic warp maximum of 64-bit keys with two ``redux.sync.max.u32`` (as the native top-k reduction). +__device__ __forceinline__ uint64_t warpMax64(uint64_t v) +{ + uint32_t const hi = static_cast(v >> 32); + uint32_t const lo = static_cast(v); + uint32_t maxHi; + asm volatile("redux.sync.max.u32 %0, %1, 0xffffffff;" : "=r"(maxHi) : "r"(hi)); + uint32_t const loContrib = hi == maxHi ? lo : 0u; + uint32_t maxLo; + asm volatile("redux.sync.max.u32 %0, %1, 0xffffffff;" : "=r"(maxLo) : "r"(loContrib)); + return (static_cast(maxHi) << 32) | maxLo; +} + +struct RouteSmem +{ + int topExpert[kMaxRows][kTopK]; + float topWeight[kMaxRows][kTopK]; //!< Value of the BF16 weight. + int slotDistinct[kMaxRows][kTopK]; + int distinct[kMaxDistinct]; + uint32_t mask[kNumExperts / 32]; //!< Experts selected by any row: bit e % 32 of word e / 32. + int numDistinct; +}; + +//! A lane's routing inputs of one token: logits of experts 8l .. 8l + 7 (two 16-byte loads). +struct RouteLogits +{ + float4 l0; + float4 l1; +}; + +__device__ __forceinline__ RouteLogits loadRouteLogits(DraftRoutedMoeParams const& p, int token, int lane) +{ + float4 const* logits = reinterpret_cast(p.logits + token * kNumExperts + lane * (kNumExperts / 32)); + return RouteLogits{logits[0], logits[1]}; +} + +//! Compare-exchange for a descending order: a[i] >= a[j] afterwards. +__device__ __forceinline__ void keyExchange(uint64_t& hi, uint64_t& lo) +{ + uint64_t const a = hi; + uint64_t const b = lo; + hi = a > b ? a : b; + lo = a > b ? b : a; +} + +//! Sorts 8 keys in descending order (Batcher's odd-even merge sort, 19 compare-exchanges). +__device__ __forceinline__ void sortKeysDescending(uint64_t (&k)[8]) +{ + keyExchange(k[0], k[1]); + keyExchange(k[2], k[3]); + keyExchange(k[4], k[5]); + keyExchange(k[6], k[7]); + keyExchange(k[0], k[2]); + keyExchange(k[1], k[3]); + keyExchange(k[4], k[6]); + keyExchange(k[5], k[7]); + keyExchange(k[1], k[2]); + keyExchange(k[5], k[6]); + keyExchange(k[0], k[4]); + keyExchange(k[1], k[5]); + keyExchange(k[2], k[6]); + keyExchange(k[3], k[7]); + keyExchange(k[2], k[4]); + keyExchange(k[3], k[5]); + keyExchange(k[1], k[2]); + keyExchange(k[3], k[4]); + keyExchange(k[5], k[6]); +} + +//! Top-8 of one token by one warp; lane k < 8 receives slot k (expert, weight). Lane l holds experts 8l .. 8l + 7 +//! (its logits and its 16-byte slice of the BF16 bias, ``braw``); the selection key carries the expert id, so the +//! result does not depend on this placement. +__device__ void routeToken( + DraftRoutedMoeParams const& p, int token, int lane, RouteLogits const& in, uint4 braw, RouteSmem& rs) +{ + constexpr int kPerLane = kNumExperts / 32; + float const logit[kPerLane] = {in.l0.x, in.l0.y, in.l0.z, in.l0.w, in.l1.x, in.l1.y, in.l1.z, in.l1.w}; + uint32_t const biasPairs[kPerLane / 2] = {braw.x, braw.y, braw.z, braw.w}; + float sig[kPerLane]; + uint64_t key[kPerLane]; +#pragma unroll + for (int i = 0; i < kPerLane; ++i) + { + uint32_t const pair = biasPairs[i / 2]; + float const bias = __uint_as_float((i % 2 == 0 ? pair << 16 : pair & 0xFFFF0000u)); + sig[i] = routingSigmoid(logit[i]); + key[i] = packKey(addFtz(sig[i], bias), lane * kPerLane + i); + } + // The lane's keys in descending order; then every round takes the warp maximum of the lanes' leading keys and + // the owning lane drops its leading key (keys are unique: they carry the expert id). Every round thus selects + // the largest key not selected yet, as the native top-k reduction does. + sortKeysDescending(key); + float mySig = 0.f; + int myExpert = 0; +#pragma unroll + for (int k = 0; k < kTopK; ++k) + { + uint64_t const best = warpMax64(key[0]); + bool const pop = key[0] == best; +#pragma unroll + for (int i = 0; i + 1 < kPerLane; ++i) + { + key[i] = pop ? key[i + 1] : key[i]; + } + key[kPerLane - 1] = pop ? 0 : key[kPerLane - 1]; + int const e = 65535 - static_cast(best & 0xFFFF); + int const owner = e / kPerLane; + int const slot = e % kPerLane; + float s = 0.f; +#pragma unroll + for (int i = 0; i < kPerLane; ++i) + { + s = i == slot ? sig[i] : s; + } + s = __shfl_sync(0xffffffffu, s, owner); + if (lane == k) + { + mySig = s; + myExpert = e; + } + } + // cg::reduce(warp, sigmoid, plus): XOR butterfly 16, 8, 4, 2, 1; lanes >= 8 hold 0. + float sum = lane < kTopK ? mySig : 0.f; +#pragma unroll + for (int off = 16; off > 0; off >>= 1) + { + sum = addFtz(sum, __shfl_xor_sync(0xffffffffu, sum, off)); + } + if (lane < kTopK) + { + float const w = divApproxFtz(mulFtz(mySig, p.routedScaling), addFtz(sum, kSumEpsilon)); + rs.topExpert[token][lane] = myExpert; + rs.topWeight[token][lane] = __bfloat162float(__float2bfloat16_rn(w)); + atomicOr(&rs.mask[myExpert >> 5], 1u << (myExpert & 31)); + } +} + +//! Routing of all rows plus the distinct-expert list (ascending ids), by the whole CTA. ``tokensDone`` (optional) +//! receives clock64 when this thread's warp has routed its tokens. +template +__device__ void routeAll( + DraftRoutedMoeParams const& p, RouteSmem& rs, int warp, int lane, int64_t* tokensDone = nullptr) +{ + if (threadIdx.x < kNumExperts / 32) + { + rs.mask[threadIdx.x] = 0; + } + // A warp routes rows warp, warp + kWarps, ...: their logit loads are all issued before the first selection. + constexpr int kPerWarp = (kMaxRows + kWarps - 1) / kWarps; + uint4 const braw = *reinterpret_cast(p.bias + lane * (kNumExperts / 32)); + RouteLogits in[kPerWarp]; +#pragma unroll + for (int i = 0; i < kPerWarp; ++i) + { + if (warp + i * kWarps < p.rows) + { + in[i] = loadRouteLogits(p, warp + i * kWarps, lane); + } + } + __syncthreads(); // the mask is cleared before any row sets its bits +#pragma unroll + for (int i = 0; i < kPerWarp; ++i) + { + if (warp + i * kWarps < p.rows) + { + routeToken(p, warp + i * kWarps, lane, in[i], braw, rs); + } + } + if (tokensDone != nullptr) + { + *tokensDone = clock64(); + } + __syncthreads(); + // Distinct experts in ascending order: the index of expert e is the number of selected experts below it. + if (warp == 0 && lane < kNumExperts / 32) + { + int idx = 0; + for (int w = 0; w < lane; ++w) + { + idx += __popc(rs.mask[w]); + } + uint32_t m = rs.mask[lane]; + if (lane == kNumExperts / 32 - 1) + { + rs.numDistinct = idx + __popc(m); + } + while (m != 0) + { + rs.distinct[idx++] = lane * 32 + __ffs(m) - 1; + m &= m - 1; + } + } + for (int i = threadIdx.x; i < p.rows * kTopK; i += kWarps * 32) + { + int const e = rs.topExpert[i / kTopK][i % kTopK]; + int idx = __popc(rs.mask[e >> 5] & ((1u << (e & 31)) - 1u)); + for (int w = 0; w < (e >> 5); ++w) + { + idx += __popc(rs.mask[w]); + } + rs.slotDistinct[i / kTopK][i % kTopK] = idx; + } + __syncthreads(); +} + +// --------------------------------------------------------------------------------------------------------------- +// FC1: routing + gate/up GEMM + SwiGLU. +// --------------------------------------------------------------------------------------------------------------- + +template +__global__ void __launch_bounds__(kWarps * 32) mtpMoeFc1Kernel(DraftRoutedMoeParams const p) +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + extern __shared__ __align__(128) uint8_t smem[]; + __shared__ RouteSmem rs; + int const warp = threadIdx.x >> 5; + int const lane = threadIdx.x & 31; + int const rows = p.rows; + uint8_t* xs = smem; + uint8_t* ring = smem + rows * kXRowBytes + warp * kStages * kChunkBytes; + uint32_t const xsAddr = smemAddr(xs); + uint32_t const ringAddr = smemAddr(ring); + + // Every input below (logits, activation) is produced by the predecessor grid. + gridDependencyWait(); + + // The activation rows, swizzled per 128-byte segment by token. + for (int i = threadIdx.x; i < rows * (kXRowBytes / 16); i += kWarps * 32) + { + int const t = i / (kXRowBytes / 16); + int const u = i % (kXRowBytes / 16); + cpAsync16(xsAddr + t * kXRowBytes + swizzled(t, u), p.x + t * kHidden + u * 8); + } + cpAsyncCommit(); + + routeAll(p, rs, warp, lane); + int const numDistinct = rs.numDistinct; + if (blockIdx.x == 0) + { + for (int i = threadIdx.x; i < kRouteWords; i += kWarps * 32) + { + int v = 0; + if (i == 0) + { + v = numDistinct; + } + else if (i < kRouteSlotOffset) + { + v = i - kRouteDistinctOffset < numDistinct ? rs.distinct[i - kRouteDistinctOffset] : -1; + } + else if (i < kRouteWeightOffset) + { + int const j = i - kRouteSlotOffset; + v = j / kTopK < rows ? rs.slotDistinct[j / kTopK][j % kTopK] : -1; + } + else + { + int const j = i - kRouteWeightOffset; + v = j / kTopK < rows ? __float_as_int(rs.topWeight[j / kTopK][j % kTopK]) : 0; + } + p.route[i] = v; + } + } + if (p.flags & kFlagEarlyTrigger) + { + gridDependencyTrigger(); + } + cpAsyncWait<0>(); + __syncthreads(); + + int const totalWarps = gridDim.x * kWarps; + int const gw = blockIdx.x * kWarps + warp; + int const items = numDistinct * kFc1Tiles; + int const myItems = items > gw ? (items - gw + totalWarps - 1) / totalWarps : 0; + int const total = myItems * kFc1Chunks; + // With kPairs, the warp's tiles are taken two at a time and their chunks interleave (A0 B0 A1 B1 ...): two + // independent accumulation chains per warp, each still in natural K order. An odd last tile runs alone. + int const pairChunks = kPairs ? (myItems / 2) * 2 * kFc1Chunks : 0; + + // Chunk n of the warp's stream -> (local tile index, K chunk). + auto chunkOf = [&](int n, int& tile, int& kc) + { + if (n < pairChunks) + { + int const r = n % (2 * kFc1Chunks); + tile = 2 * (n / (2 * kFc1Chunks)) + (r & 1); + kc = r >> 1; + } + else + { + tile = (pairChunks / kFc1Chunks) + (n - pairChunks) / kFc1Chunks; + kc = (n - pairChunks) % kFc1Chunks; + } + }; + + auto issue = [&](int n) + { + int tile, kc; + chunkOf(n, tile, kc); + int const item = gw + tile * totalWarps; + int const e = rs.distinct[item / kFc1Tiles]; + int const a = item % kFc1Tiles; + __nv_bfloat16 const* src = p.w13 + ((static_cast(e) * kFc1Chunks + kc) * kFc1Rows + a * 16) * kKChunk; + issueWeightChunk(ringAddr + (n % kStages) * kChunkBytes, src, lane); + }; + + auto epilogue = [&](int tile, float const(&acc)[4]) + { + int const item = gw + tile * totalWarps; + int const d = item / kFc1Tiles; + int const a = item % kFc1Tiles; + int const g = lane >> 2; + int const t = lane & 3; + // Rows 0..7 of the tile are up rows, rows 8..15 the gate rows of the same intermediate columns. + int const j = 16 * (a >> 1) + 2 * g + (a & 1); +#pragma unroll + for (int c = 0; c < 2; ++c) + { + int const tok = 2 * t + c; + if (tok < rows) + { + p.h[(static_cast(d) * kMaxRows + tok) * kInter + j] + = bf16SatFinite(swiGlu(acc[c], acc[2 + c])); + } + } + }; + +#pragma unroll 1 + for (int n = 0; n < kStages - 1; ++n) + { + if (n < total) + { + issue(n); + } + cpAsyncCommit(); + } + + float acc[4] = {-0.f, -0.f, -0.f, -0.f}; + float acc2[4] = {-0.f, -0.f, -0.f, -0.f}; +#pragma unroll 1 + for (int n = 0; n < total; ++n) + { + // Chunk n has landed once at most kStages - 2 newer groups are pending. + cpAsyncWait(); + __syncwarp(); + int tile, kc; + chunkOf(n, tile, kc); + uint32_t const slot = ringAddr + (n % kStages) * kChunkBytes; + if (n >= pairChunks) + { +#pragma unroll + for (int q = 0; q < 4; q += 2) + { + uint32_t b[4]; + loadB2(b, xsAddr, kXRowBytes, kc * 8 + 2 * q, rows, lane); + uint32_t a[4]; + loadA(a, slot, q, lane); + mmaBf16(acc, a, b[0], b[1]); + loadA(a, slot, q + 1, lane); + mmaBf16(acc, a, b[2], b[3]); + } + if (kc == kFc1Chunks - 1) + { + epilogue(tile, acc); + acc[0] = acc[1] = acc[2] = acc[3] = -0.f; + } + } + else if (n & 1) + { + // Both chunks of this K step of the pair are in: chunk n - 1 (first tile) and n (second tile). + uint32_t const slot0 = ringAddr + ((n - 1) % kStages) * kChunkBytes; +#pragma unroll + for (int q = 0; q < 4; q += 2) + { + uint32_t b[4]; + loadB2(b, xsAddr, kXRowBytes, kc * 8 + 2 * q, rows, lane); + uint32_t a0[4]; + uint32_t a1[4]; + loadA(a0, slot0, q, lane); + loadA(a1, slot, q, lane); + mmaBf16(acc, a0, b[0], b[1]); + mmaBf16(acc2, a1, b[0], b[1]); + loadA(a0, slot0, q + 1, lane); + loadA(a1, slot, q + 1, lane); + mmaBf16(acc, a0, b[2], b[3]); + mmaBf16(acc2, a1, b[2], b[3]); + } + if (kc == kFc1Chunks - 1) + { + epilogue(tile - 1, acc); + epilogue(tile, acc2); + acc[0] = acc[1] = acc[2] = acc[3] = -0.f; + acc2[0] = acc2[1] = acc2[2] = acc2[3] = -0.f; + } + } + __syncwarp(); + // The slot refilled now held chunk n - 1, consumed in this or the previous iteration. + if (n + kStages - 1 < total) + { + issue(n + kStages - 1); + } + cpAsyncCommit(); + } + cpAsyncWait<0>(); + + if (p.flags & kFlagPrefetchFc2) + { + int const pieces = numDistinct * kFc2Chunks * kPrefetchPiecesPerBlock; + for (int piece = gw * 32 + lane; piece < pieces; piece += totalWarps * 32) + { + int const d = piece / (kFc2Chunks * kPrefetchPiecesPerBlock); + int const rem = piece % (kFc2Chunks * kPrefetchPiecesPerBlock); + int const kc = rem / kPrefetchPiecesPerBlock; + int const off = rem % kPrefetchPiecesPerBlock; + uint8_t const* addr = reinterpret_cast(p.w2 + + (static_cast(rs.distinct[d]) * kFc2Chunks + kc) * kHidden * kKChunk) + + static_cast(off) * kPrefetchPieceBytes; + asm volatile("cp.async.bulk.prefetch.L2.global [%0], %1;" ::"l"(__cvta_generic_to_global(addr)), + "r"(kPrefetchPieceBytes) + : "memory"); + } + } + if (!(p.flags & kFlagEarlyTrigger)) + { + gridDependencyTrigger(); + } +#endif +} + +// --------------------------------------------------------------------------------------------------------------- +// FC2: down GEMM per distinct expert + native finalize, 16 hidden rows per work item. +// --------------------------------------------------------------------------------------------------------------- + +struct Fc2Smem +{ + float weight[kMaxRows][kTopK]; + int distinct[kMaxDistinct]; + int8_t slotOf[kMaxRows][kMaxDistinct]; + int numDistinct; +}; + +__device__ __forceinline__ int hiddenOfStoredRow(int s) +{ + return (s & ~31) | ((s & 7) << 2) | ((s & 31) >> 3); +} + +template +__global__ void __launch_bounds__(kWarps * 32) mtpMoeFc2Kernel(DraftRoutedMoeParams const p) +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + extern __shared__ __align__(128) uint8_t smem[]; + __shared__ Fc2Smem fs; + int const warp = threadIdx.x >> 5; + int const lane = threadIdx.x & 31; + int const rows = p.rows; + constexpr int kWarpBytes = kStages * kChunkBytes + kFc2HStages * kHEntryBytes + kStageBytes; + uint8_t* mine = smem + warp * kWarpBytes; + uint32_t const ringAddr = smemAddr(mine); + uint32_t const hAddr = ringAddr + kStages * kChunkBytes; + __nv_bfloat16* stage = reinterpret_cast<__nv_bfloat16*>(mine + kStages * kChunkBytes + kFc2HStages * kHEntryBytes); + + // The routing record and the intermediate are written by the FC1 grid. + gridDependencyWait(); + + int const numDistinct = p.route[0]; + for (int i = threadIdx.x; i < kMaxRows * kMaxDistinct; i += kWarps * 32) + { + fs.slotOf[i / kMaxDistinct][i % kMaxDistinct] = -1; + } + for (int i = threadIdx.x; i < kMaxDistinct; i += kWarps * 32) + { + fs.distinct[i] = p.route[kRouteDistinctOffset + i]; + } + __syncthreads(); + for (int i = threadIdx.x; i < rows * kTopK; i += kWarps * 32) + { + int const t = i / kTopK; + int const k = i % kTopK; + fs.slotOf[t][p.route[kRouteSlotOffset + i]] = static_cast(k); + fs.weight[t][k] = __int_as_float(p.route[kRouteWeightOffset + i]); + } + __syncthreads(); + + int const totalWarps = gridDim.x * kWarps; + int const gw = blockIdx.x * kWarps + warp; + int const perItem = numDistinct * kFc2Chunks; + int const myItems = kFc2Tiles > gw ? (kFc2Tiles - gw + totalWarps - 1) / totalWarps : 0; + int const total = myItems * perItem; + // With kPairs, the experts of an item are taken two at a time and their chunks interleave (two independent + // accumulation chains, each in natural K order); an odd last expert runs alone. + int const pairChunks = kPairs ? (numDistinct / 2) * 2 * kFc2Chunks : 0; + + // Chunk m of an item -> (distinct expert index, K chunk). + auto chunkOf = [&](int m, int& d, int& kc) + { + if (m < pairChunks) + { + int const r = m % (2 * kFc2Chunks); + d = 2 * (m / (2 * kFc2Chunks)) + (r & 1); + kc = r >> 1; + } + else + { + d = (pairChunks / kFc2Chunks) + (m - pairChunks) / kFc2Chunks; + kc = (m - pairChunks) % kFc2Chunks; + } + }; + // Intermediate entry of expert d of local item il (entries are consumed in this order). + auto hEntry = [&](int il, int d) { return hAddr + ((il * numDistinct + d) % kFc2HStages) * kHEntryBytes; }; + + auto issue = [&](int n) + { + int const il = n / perItem; + int d, kc; + chunkOf(n % perItem, d, kc); + int const tile = gw + il * totalWarps; + int const e = fs.distinct[d]; + __nv_bfloat16 const* src = p.w2 + ((static_cast(e) * kFc2Chunks + kc) * kHidden + tile * 16) * kKChunk; + issueWeightChunk(ringAddr + (n % kStages) * kChunkBytes, src, lane); + if (kc == 0) + { + uint32_t const entry = hEntry(il, d); + for (int u = lane; u < rows * (kInter / 8); u += 32) + { + int const tok = u / (kInter / 8); + int const unit = u % (kInter / 8); + cpAsync16(entry + tok * (kInter * 2) + swizzled(tok, unit), + p.h + (static_cast(d) * kMaxRows + tok) * kInter + unit * 8); + } + } + }; + + // FC2 rows of expert d (C fragment) -> BF16, into the token's slot of the staging area. + auto scatter = [&](int tile, int d, float const(&acc)[4]) + { + int const g = lane >> 2; + int const t = lane & 3; +#pragma unroll + for (int c = 0; c < 2; ++c) + { + int const tok = 2 * t + c; + int const k = tok < rows ? fs.slotOf[tok][d] : -1; + if (k >= 0) + { + __nv_bfloat16 const lo = bf16SatFinite(acc[c]); + __nv_bfloat16 const hi = bf16SatFinite(acc[2 + c]); + stage[(tok * kTopK + k) * 16 + g] = lo; + stage[(tok * kTopK + k) * 16 + g + 8] = hi; + if (p.debugFc2 != nullptr) + { + int64_t const base = (static_cast(tok) * kTopK + k) * kHidden; + p.debugFc2[base + hiddenOfStoredRow(tile * 16 + g)] = lo; + p.debugFc2[base + hiddenOfStoredRow(tile * 16 + g + 8)] = hi; + } + } + } + }; + + // Native finalize of the item's 16 hidden rows for every token: FP32 fma chain from 0 in slot order. + auto finalize = [&](int tile) + { + __syncwarp(); + for (int pr = lane; pr < rows * 16; pr += 32) + { + int const tok = pr >> 4; + int const row = pr & 15; + float sum = 0.f; +#pragma unroll + for (int k = 0; k < kTopK; ++k) + { + sum = fmaFtz(fs.weight[tok][k], __bfloat162float(stage[(tok * kTopK + k) * 16 + row]), sum); + } + p.out[static_cast(tok) * kHidden + hiddenOfStoredRow(tile * 16 + row)] = __float2bfloat16_rn(sum); + } + }; + +#pragma unroll 1 + for (int n = 0; n < kStages - 1; ++n) + { + if (n < total) + { + issue(n); + } + cpAsyncCommit(); + } + + float acc[4] = {-0.f, -0.f, -0.f, -0.f}; + float acc2[4] = {-0.f, -0.f, -0.f, -0.f}; +#pragma unroll 1 + for (int n = 0; n < total; ++n) + { + cpAsyncWait(); + __syncwarp(); + int const il = n / perItem; + int const m = n % perItem; + int const tile = gw + il * totalWarps; + int d, kc; + chunkOf(m, d, kc); + uint32_t const slot = ringAddr + (n % kStages) * kChunkBytes; + if (m >= pairChunks) + { + uint32_t const entry = hEntry(il, d); +#pragma unroll + for (int q = 0; q < 4; q += 2) + { + uint32_t b[4]; + loadB2(b, entry, kInter * 2, kc * 8 + 2 * q, rows, lane); + uint32_t a[4]; + loadA(a, slot, q, lane); + mmaBf16(acc, a, b[0], b[1]); + loadA(a, slot, q + 1, lane); + mmaBf16(acc, a, b[2], b[3]); + } + if (kc == kFc2Chunks - 1) + { + scatter(tile, d, acc); + acc[0] = acc[1] = acc[2] = acc[3] = -0.f; + } + } + else if (m & 1) + { + uint32_t const slot0 = ringAddr + ((n - 1) % kStages) * kChunkBytes; + uint32_t const entry0 = hEntry(il, d - 1); + uint32_t const entry1 = hEntry(il, d); +#pragma unroll + for (int q = 0; q < 4; q += 2) + { + uint32_t b0[4]; + uint32_t b1[4]; + loadB2(b0, entry0, kInter * 2, kc * 8 + 2 * q, rows, lane); + loadB2(b1, entry1, kInter * 2, kc * 8 + 2 * q, rows, lane); + uint32_t a0[4]; + uint32_t a1[4]; + loadA(a0, slot0, q, lane); + loadA(a1, slot, q, lane); + mmaBf16(acc, a0, b0[0], b0[1]); + mmaBf16(acc2, a1, b1[0], b1[1]); + loadA(a0, slot0, q + 1, lane); + loadA(a1, slot, q + 1, lane); + mmaBf16(acc, a0, b0[2], b0[3]); + mmaBf16(acc2, a1, b1[2], b1[3]); + } + if (kc == kFc2Chunks - 1) + { + scatter(tile, d - 1, acc); + scatter(tile, d, acc2); + acc[0] = acc[1] = acc[2] = acc[3] = -0.f; + acc2[0] = acc2[1] = acc2[2] = acc2[3] = -0.f; + } + } + if (m == perItem - 1) + { + finalize(tile); + } + __syncwarp(); + if (n + kStages - 1 < total) + { + issue(n + kStages - 1); + } + cpAsyncCommit(); + } + cpAsyncWait<0>(); +#endif +} + +// --------------------------------------------------------------------------------------------------------------- +// TMA-fed kernels (kFlagTma). Per CTA one producer thread (last warp, lane 0) streams 3D weight boxes (16 rows per +// chain x 64 elements x several K chunks of the BlockMajorK layout, 128-byte swizzle = the layout loadA reads; 32 KB +// per FC1 box, one expert's K = 256 per FC2 box) into a ring of S stages; W MMA warps each own C 16-row accumulation +// chains per tile and consume every stage. Full barriers complete on the transaction count; each MMA warp releases +// a stage (empty barrier, count W) after the MMAs that read it. The arithmetic of every chain (fragments, k-step +// order, accumulator init, epilogues) is that of the kernels above. +// --------------------------------------------------------------------------------------------------------------- + +#ifndef GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE +#define GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE 0 +#endif + +//! Probe builds (GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE=1) record per-CTA events into DraftRoutedMoeParams::timeline; +//! production builds compile none of it. +constexpr bool kTimeline = GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE != 0; + +//! Columns of a CTA's event record: %globaltimer at entry and when MMA warp 0 is done (ns), the SM id, clock64 +//! cycles after entry, and counts. +enum TimelineSlot : int +{ + kTlEntryNs = 0, + kTlDoneNs = 1, + kTlSmId = 2, + kTlWait = 3, //!< griddepcontrol.wait returned + kTlSetup = 4, //!< routing (FC1) / routing record (FC2) in shared memory, roles split + kTlFirst = 5, //!< MMA warp 0 found its first stage full + kTlLast = 6, //!< MMA warp 0 found its last stage full + kTlDone = 7, //!< MMA warp 0 done (epilogue / finalize stored) + kTlProdFirst = 8, //!< producer issued its first weight copy + kTlProdLast = 9, //!< producer issued its last weight copy + kTlFullBlocked = 10, //!< MMA warp 0: stage waits whose first poll failed + kTlEmptyBlocked = 11, //!< producer: re-arm waits whose first poll failed + kTlChunks = 12, //!< MMA warp 0: K chunks consumed + kTlRouteTokens = 13, //!< FC1 warp 0 has routed its tokens + kTlRouted = 14, //!< FC1 routing and distinct-expert list done (before the activation wait) +}; + +//! K chunks (2 KB per 16-row chain) per stage of the TMA-fed FC1 kernel: one 32 KB 3D box per stage. +static_assert(kTmaStageChunks * kChunkBytes == 32 * 1024, "32 KB FC1 stages"); + +__device__ __forceinline__ int64_t globalTimerNs() +{ + uint64_t t; + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); + return static_cast(t); +} + +__device__ __forceinline__ int64_t smId() +{ + uint32_t id; + asm volatile("mov.u32 %0, %%smid;" : "=r"(id)); + return id; +} + +//! This CTA's event record of kernel ``kernel`` (0: FC1, 1: FC2), or null. +__device__ __forceinline__ int64_t* timelineRow(DraftRoutedMoeParams const& p, int kernel) +{ + if (!kTimeline || p.timeline == nullptr || blockIdx.x >= kTimelineCtas) + { + return nullptr; + } + return p.timeline + (static_cast(kernel) * kTimelineCtas + blockIdx.x) * kTimelineSlots; +} + +__device__ __forceinline__ void mbarInit(uint32_t bar, uint32_t count) +{ + asm volatile("mbarrier.init.shared::cta.b64 [%0], %1;" ::"r"(bar), "r"(count) : "memory"); +} + +__device__ __forceinline__ void mbarFenceInit() +{ + asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory"); +} + +__device__ __forceinline__ void mbarArriveExpectTx(uint32_t bar, uint32_t bytes) +{ + asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;" ::"r"(bar), "r"(bytes) : "memory"); +} + +__device__ __forceinline__ void mbarArrive(uint32_t bar) +{ + asm volatile("mbarrier.arrive.shared::cta.b64 _, [%0];" ::"r"(bar) : "memory"); +} + +__device__ __forceinline__ bool mbarTryWait(uint32_t bar, uint32_t parity) +{ + uint32_t done; + asm volatile( + "{\n\t" + ".reg .pred p;\n\t" + "mbarrier.try_wait.parity.shared::cta.b64 p, [%1], %2;\n\t" + "selp.u32 %0, 1, 0, p;\n\t" + "}" + : "=r"(done) + : "r"(bar), "r"(parity) + : "memory"); + return done != 0; +} + +//! Non-blocking probe of the phase of ``bar`` with parity ``parity`` (acquire when it returns true). +__device__ __forceinline__ bool mbarTestWait(uint32_t bar, uint32_t parity) +{ + uint32_t done; + asm volatile( + "{\n\t" + ".reg .pred p;\n\t" + "mbarrier.test_wait.parity.shared::cta.b64 p, [%1], %2;\n\t" + "selp.u32 %0, 1, 0, p;\n\t" + "}" + : "=r"(done) + : "r"(bar), "r"(parity) + : "memory"); + return done != 0; +} + +//! Blocks until the phase of ``bar`` with parity ``parity`` has completed. Every wait is between threads of one CTA +//! (never on another grid), so one that does not complete within 2^28 polls is a protocol error: trap, do not hang. +__device__ __forceinline__ void mbarWait(uint32_t bar, uint32_t parity) +{ + for (uint32_t i = 0; !mbarTryWait(bar, parity); ++i) + { + if (i == (1u << 28)) + { + __trap(); + } + } +} + +//! mbarWait; returns 1 when the first poll failed (probe builds count these), else 0. +[[maybe_unused]] __device__ __forceinline__ int mbarWaitCounted(uint32_t bar, uint32_t parity) +{ + if (mbarTryWait(bar, parity)) + { + return 0; + } + mbarWait(bar, parity); + return 1; +} + +//! Barrier among the W MMA warps (threads 0 .. 32 W - 1) of a warp-specialized kernel; the producer warp does not +//! take part. Named barrier 1 (``__syncthreads`` uses 0). +template +__device__ __forceinline__ void mmaWarpsSync() +{ + asm volatile("bar.sync 1, %0;" ::"n"(W * 32) : "memory"); +} + +__device__ __forceinline__ void tmaLoad3d(uint32_t dst, CUtensorMap const* map, int c0, int c1, int c2, uint32_t bar) +{ + asm volatile( + "cp.async.bulk.tensor.3d.shared::cluster.global.mbarrier::complete_tx::bytes [%0], [%1, {%2, %3, %4}], " + "[%5];" ::"r"(dst), + "l"(reinterpret_cast(map)), "r"(c0), "r"(c1), "r"(c2), "r"(bar) + : "memory"); +} + +//! tmaLoad3d with an L2 cache policy (createpolicy). +__device__ __forceinline__ void tmaLoad3dHint( + uint32_t dst, CUtensorMap const* map, int c0, int c1, int c2, uint32_t bar, uint64_t policy) +{ + asm volatile( + "cp.async.bulk.tensor.3d.shared::cluster.global.mbarrier::complete_tx::bytes.L2::cache_hint [%0], [%1, {%2, " + "%3, %4}], [%5], %6;" ::"r"(dst), + "l"(reinterpret_cast(map)), "r"(c0), "r"(c1), "r"(c2), "r"(bar), "l"(policy) + : "memory"); +} + +//! L2 policy that evicts the lines it is attached to first. +__device__ __forceinline__ uint64_t l2EvictFirstPolicy() +{ + uint64_t policy; + asm volatile("createpolicy.fractional.L2::evict_first.b64 %0, 1.0;" : "=l"(policy)); + return policy; +} + +__device__ __forceinline__ void tmaPrefetchL2(CUtensorMap const* map, int c0, int c1, int c2) +{ + asm volatile( + "cp.async.bulk.prefetch.tensor.3d.L2.global.tile [%0, {%1, %2, %3}];" ::"l"(reinterpret_cast(map)), + "r"(c0), "r"(c1), "r"(c2) + : "memory"); +} + +__device__ __forceinline__ void bulkLoad(uint32_t dst, void const* src, uint32_t bytes, uint32_t bar) +{ + asm volatile("cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes [%0], [%1], %2, [%3];" ::"r"(dst), + "l"(__cvta_generic_to_global(src)), "r"(bytes), "r"(bar) + : "memory"); +} + +__device__ __forceinline__ void prefetchTensorMap(CUtensorMap const* map) +{ + asm volatile("prefetch.tensormap [%0];" ::"l"(reinterpret_cast(map)) : "memory"); +} + +//! L2 prefetch of this CTA's share of the distinct experts' FC2 weights (16 KB pieces, expert-major, CTA-strided, so +//! the experts the FC2 grid reads first are fetched first). Weights only: legal at any point of the FC1 grid. +__device__ __forceinline__ void prefetchFc2Share(DraftRoutedMoeParams const& p, int const* distinct, int numDistinct) +{ + constexpr int kPiecesPerExpert = kFc2Chunks * kPrefetchPiecesPerBlock; + int const pieces = numDistinct * kPiecesPerExpert; + for (int piece = blockIdx.x; piece < pieces; piece += gridDim.x) + { + uint8_t const* addr + = reinterpret_cast( + p.w2 + static_cast(distinct[piece / kPiecesPerExpert]) * kFc2Chunks * kHidden * kKChunk) + + static_cast(piece % kPiecesPerExpert) * kPrefetchPieceBytes; + asm volatile( + "cp.async.bulk.prefetch.L2.global [%0], %1;" ::"l"(__cvta_generic_to_global(addr)), "r"(kPrefetchPieceBytes) + : "memory"); + } +} + +//! Fragments of one 64-element K chunk for C 16-row chains: A of k-steps 0..3 per chain, B of k-steps (0,1), (2,3). +template +struct ChunkFrags +{ + uint32_t a[C][4][4]; + uint32_t b[2][4]; +}; + +//! The four k-steps of a chunk in natural K order into each chain's own accumulator (the sequence of the kernels +//! above; the chains are interleaved per k-step and never mixed). +template +__device__ __forceinline__ void mmaChunk(float (&acc)[C][4], ChunkFrags const& f) +{ +#pragma unroll + for (int q = 0; q < 4; ++q) + { +#pragma unroll + for (int c = 0; c < C; ++c) + { + mmaBf16(acc[c], f.a[c][q], f.b[q >> 1][2 * (q & 1)], f.b[q >> 1][2 * (q & 1) + 1]); + } + } +} + +//! Per-lane ldmatrix offsets inside one K chunk. ``a[q]``: the lane's row of k-step q in a 2 KB 16-row chunk (as +//! loadA). ``b[h]``: the lane's 16-byte unit of k-steps (2h, 2h + 1) in the first 128-byte K chunk of a [token][k] +//! tile with ``bRowBytes`` per token (as loadB2). The swizzle only permutes the 16-byte units inside a 128-byte K +//! chunk, so K chunk kc of a token row is the same pattern ``kc * 128`` bytes further. +struct LaneOffsets +{ + uint32_t a[4]; + uint32_t b[2]; +}; + +__device__ __forceinline__ LaneOffsets laneOffsets(int rows, int bRowBytes, int lane) +{ + LaneOffsets o; + int const m = lane >> 3; + int const r = (lane & 7) | ((m & 1) << 3); +#pragma unroll + for (int q = 0; q < 4; ++q) + { + o.a[q] = r * 128 + swizzled(r, 2 * q + (m >> 1)); + } + int tok = lane & 7; + tok = tok < rows ? tok : 0; +#pragma unroll + for (int h = 0; h < 2; ++h) + { + o.b[h] = tok * bRowBytes + swizzled(tok, 4 * h + m); + } + return o; +} + +//! Per-lane ldmatrix offsets of the B fragments in a streamed activation slice (kFlagStreamX): one TMA box of +//! ``DraftRoutedMoeTensorMaps::x`` is [8 tokens][kXBoxChunks K chunks][128 bytes] with the 128-byte swizzle, so K chunk +//! p of token t is line 2t + p of the box. ``b[p][h]``: the lane's 16-byte unit of k-steps (2h, 2h + 1) of K chunk p. +struct LaneOffsetsX +{ + uint32_t b[2][2]; +}; + +__device__ __forceinline__ LaneOffsetsX laneOffsetsX(int rows, int lane) +{ + static_assert(kXBoxChunks == 2, "two K chunks per activation box"); + LaneOffsetsX o; + int const m = lane >> 3; + int tok = lane & 7; + tok = tok < rows ? tok : 0; +#pragma unroll + for (int q = 0; q < 2; ++q) + { + int const line = kXBoxChunks * tok + q; +#pragma unroll + for (int h = 0; h < 2; ++h) + { + o.b[q][h] = line * 128 + swizzled(line, 4 * h + m); + } + } + return o; +} + +//! The fragments of one K chunk for C 16-row chains (chain c's 2 KB chunk follows chain 0's at c * kChunkBytes) +//! from per-stage bases and constant chunk offsets, so the loads of a stage need no address arithmetic. +//! ``aBase[q]``: chain 0 of the stage's first chunk + LaneOffsets::a[q]; ``bBase[h]``: the activation K chunk of the +//! stage's first chunk + LaneOffsets::b[h]. +template +__device__ __forceinline__ void loadFragsAt( + ChunkFrags& f, uint32_t const (&aBase)[4], uint32_t aOff, uint32_t const (&bBase)[2], uint32_t bOff) +{ +#pragma unroll + for (int c = 0; c < C; ++c) + { +#pragma unroll + for (int q = 0; q < 4; ++q) + { + ldmatrixX4(f.a[c][q], aBase[q] + aOff + c * kChunkBytes); + } + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + ldmatrixX4(f.b[h], bBase[h] + bOff); + } +} + +//! loadFragsAt for chains ``kChainStride`` bytes apart (the sub-tile boxes of the triples path); chains ``c >= valid`` +//! (absent sub-tiles of the grid's last CTA) are not loaded. +template +__device__ __forceinline__ void loadFragsStrided( + ChunkFrags& f, uint32_t const (&aBase)[4], uint32_t aOff, uint32_t const (&bBase)[2], uint32_t bOff, int valid) +{ +#pragma unroll + for (int c = 0; c < C; ++c) + { + if (c < valid) + { +#pragma unroll + for (int q = 0; q < 4; ++q) + { + ldmatrixX4(f.a[c][q], aBase[q] + aOff + c * kChainStride); + } + } + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + ldmatrixX4(f.b[h], bBase[h] + bOff); + } +} + +//! mmaChunk for the chains ``c < valid`` only (each chain's k-steps in natural order, as mmaChunk). +template +__device__ __forceinline__ void mmaChunkValid(float (&acc)[C][4], ChunkFrags const& f, int valid) +{ +#pragma unroll + for (int q = 0; q < 4; ++q) + { +#pragma unroll + for (int c = 0; c < C; ++c) + { + if (c < valid) + { + mmaBf16(acc[c], f.a[c][q], f.b[q >> 1][2 * (q & 1)], f.b[q >> 1][2 * (q & 1) + 1]); + } + } + } +} + +//! Fragments of one K chunk for E FC2 experts of one 16-row tile: per expert its A (weights) and B (intermediate). +template +struct ExpertFrags +{ + uint32_t a[E][4][4]; + uint32_t b[E][2][4]; +}; + +//! Byte offset of element ``j`` of token ``tok`` in an intermediate entry stored as its shared-memory image (512-byte +//! token rows, 16-byte units swizzled by token as loadB2 reads them). +__device__ __forceinline__ int hImageElem(int tok, int j) +{ + int const u = j >> 3; + return tok * kInter + (((u & ~7) | ((u & 7) ^ (tok & 7))) << 3) + (j & 7); +} + +//! TMA-fed FC1 kernel: W MMA warps with C chains each, a ring of S stages of 32 KB (one box of ``map``). With S3 > 0 +//! (4 MMA warps, kFlagFc1Triples) a call whose tiles exceed the grid but whose 64-row sub-tiles fit three per CTA +//! runs the triples path instead (decided per call from the routing, identically in every CTA): CTA b takes sub-tiles +//! 3b .. 3b + 2 (in distinct-expert order), MMA warp w owns rows 16w .. 16w + 15 of each (three chains), and a stage +//! of its ring of S3 is three boxes of ``map3`` (64 rows x kTripleStageChunks K chunks), one per sub-tile. +//! With X (kFlagStreamX) the activation rows are not kept in shared memory: every stage also carries the activation +//! K chunks it needs (boxes of ``mapX``, kXBoxBytes each, after the weight boxes), so the ring can be deeper. +template +__global__ void __launch_bounds__((W + 1) * 32) + mtpMoeFc1TmaKernel(DraftRoutedMoeParams const p, __grid_constant__ CUtensorMap const map, + __grid_constant__ CUtensorMap const map3, __grid_constant__ CUtensorMap const mapX) +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + constexpr int kChains = W * C; + constexpr int kKs = kTmaStageChunks / kChains; // K chunks per stage (one TMA box) + constexpr int kChunkSet = kChains * kChunkBytes; + constexpr int kStage = kKs * kChunkSet; + constexpr int kTileRows = 16 * kChains; + constexpr int kTilesPerExpert = kFc1Rows / kTileRows; + constexpr int kStagesPerTile = kFc1Chunks / kKs; + constexpr int kL2Ahead = 2 * S; + static_assert(kFc1Chunks % kKs == 0, "a tile must be a whole number of stages"); + static_assert(kKs % 2 == 0, "the fragment double buffer restarts at f[0] on every stage"); + // Triples path (S3 > 0): sub-tiles of 16 W rows, three per CTA; a stage holds one box per sub-tile. + constexpr int kSubRows = 16 * W; + constexpr int kSubPerExpert = kFc1Rows / kSubRows; + constexpr int kChunkStride3 = kSubRows * kKChunk * 2; // next K chunk inside a sub-tile box + constexpr int kBox3 = kTripleStageChunks * kChunkStride3; + constexpr int kStage3 = kTripleSubTiles * kBox3; + constexpr int kStagesPerTile3 = kFc1Chunks / kTripleStageChunks; + static_assert(S3 == 0 || (W == 4 && kSubRows == kTripleRows), "the triples path has 4 MMA warps (64-row boxes)"); + static_assert(kTripleStageChunks == 2, "the triples consumer walks two K chunks per stage"); + // Ring slot strides: the weight boxes, then (X) the activation boxes of the stage's K chunks. + constexpr int kXBoxes = X ? kKs / kXBoxChunks : 0; + constexpr int kSlot = kStage + kXBoxes * kXBoxBytes; + constexpr int kSlot3 = kStage3 + (X ? kXBoxBytes : 0); + static_assert(!X || (kKs % kXBoxChunks == 0 && kTripleStageChunks == kXBoxChunks), "whole activation boxes"); + static_assert(kSlot % 1024 == 0 && kSlot3 % 1024 == 0, "slots stay aligned to the 128-byte swizzle pattern"); + constexpr int kRingBytes = S * kSlot > S3 * kSlot3 ? S * kSlot : S3 * kSlot3; + constexpr int kBars = S > S3 ? S : S3; + extern __shared__ uint8_t smemRaw[]; + __shared__ RouteSmem rs; + __shared__ __align__(8) uint64_t bars[2 * kBars]; + int64_t const t0 = kTimeline ? clock64() : 0; + int64_t const entryNs = kTimeline ? globalTimerNs() : 0; + int const warp = threadIdx.x >> 5; + int const lane = threadIdx.x & 31; + int const rows = p.rows; + uint32_t const ringAddr = (smemAddr(smemRaw) + 1023u) & ~1023u; + uint32_t const xsAddr = ringAddr + kRingBytes; + uint32_t const fullBar = smemAddr(&bars[0]); + uint32_t const emptyBar = smemAddr(&bars[kBars]); + if (threadIdx.x == 0) + { + for (int s = 0; s < kBars; ++s) + { + mbarInit(fullBar + 8 * s, 1); + mbarInit(emptyBar + 8 * s, W * 32); // every MMA thread releases its own reads + } + mbarFenceInit(); + prefetchTensorMap(&map); + if constexpr (S3 > 0) + { + prefetchTensorMap(&map3); + } + if constexpr (X) + { + prefetchTensorMap(&mapX); + } + } + + // Every input below (logits, activation) is produced by the predecessor grid. + gridDependencyWait(); + int64_t const tWait = kTimeline ? clock64() - t0 : 0; + + // The MMA warps stage the activation rows (they alone read them); the producer warp does not wait for them. + for (int i = threadIdx.x; i < (X || warp == W ? 0 : rows * (kXRowBytes / 16)); i += W * 32) + { + int const t = i / (kXRowBytes / 16); + int const u = i % (kXRowBytes / 16); + cpAsync16(xsAddr + t * kXRowBytes + swizzled(t, u), p.x + t * kHidden + u * 8); + } + cpAsyncCommit(); + + int64_t tTokens = 0; + // Ends with __syncthreads (also publishes the barrier initialization). + routeAll(p, rs, warp, lane, kTimeline ? &tTokens : nullptr); + int64_t const tRouted = kTimeline ? clock64() - t0 : 0; + int const numDistinct = rs.numDistinct; + if (blockIdx.x == 0) + { + for (int i = threadIdx.x; i < kRouteWords; i += (W + 1) * 32) + { + int v = 0; + if (i == 0) + { + v = numDistinct; + } + else if (i < kRouteSlotOffset) + { + v = i - kRouteDistinctOffset < numDistinct ? rs.distinct[i - kRouteDistinctOffset] : -1; + } + else if (i < kRouteWeightOffset) + { + int const j = i - kRouteSlotOffset; + v = j / kTopK < rows ? rs.slotDistinct[j / kTopK][j % kTopK] : -1; + } + else + { + int const j = i - kRouteWeightOffset; + v = j / kTopK < rows ? __float_as_int(rs.topWeight[j / kTopK][j % kTopK]) : 0; + } + p.route[i] = v; + } + } + int const tiles = numDistinct * kTilesPerExpert; + bool const noMma = kTimeline && (p.flags & kFlagProbeNoMma) != 0; + bool const resident = kTimeline && (p.flags & kFlagProbeResident) != 0; + // The path depends only on numDistinct and the launch, so every CTA takes the same one. + int const numSub = numDistinct * kSubPerExpert; + int const sub0 = kTripleSubTiles * static_cast(blockIdx.x); + bool const triples = S3 > 0 && (p.flags & kFlagFc1Triples) != 0 && !resident && tiles > static_cast(gridDim.x) + && numSub <= kTripleSubTiles * static_cast(gridDim.x); + int const valid3 = min(kTripleSubTiles, numSub - sub0); // sub-tiles of this CTA (<= 0: none) + // Early trigger (after routing): always with kFlagEarlyTrigger, or for calls on the triples path only. + bool const early = (p.flags & kFlagEarlyTrigger) != 0 || (triples && (p.flags & kFlagEarlyTriggerTriples) != 0); + if (early) + { + gridDependencyTrigger(); + } + bool const evictFirst = (p.flags & kFlagFc1EvictFirst) != 0; + + if (warp == W) + { + if constexpr (S3 > 0) + { + if (lane == 0 && triples) + { + uint64_t const policy = evictFirst ? l2EvictFirstPolicy() : 0; + if (p.flags & kFlagPrefetchFc2Early) + { + prefetchFc2Share(p, rs.distinct, numDistinct); + } + int emptyBlocked = 0; + int64_t tFirst = 0; + for (int u = 0; u < (valid3 > 0 ? kStagesPerTile3 : 0); ++u) + { + int const s = u % S3; + if (u >= S3) + { + if constexpr (kTimeline) + { + emptyBlocked += mbarWaitCounted(emptyBar + 8 * s, ((u / S3) - 1) & 1); + } + else + { + mbarWait(emptyBar + 8 * s, ((u / S3) - 1) & 1); + } + } + mbarArriveExpectTx(fullBar + 8 * s, valid3 * kBox3 + (X ? kXBoxBytes : 0)); + if constexpr (X) + { + tmaLoad3d( + ringAddr + s * kSlot3 + kStage3, &mapX, 0, u * kTripleStageChunks, 0, fullBar + 8 * s); + } + for (int c = 0; c < valid3; ++c) + { + int const sub = sub0 + c; + uint32_t const dst = ringAddr + s * kSlot3 + c * kBox3; + int const row = (sub % kSubPerExpert) * kSubRows; + int const slab = rs.distinct[sub / kSubPerExpert] * kFc1Chunks + u * kTripleStageChunks; + if (evictFirst) + { + tmaLoad3dHint(dst, &map3, 0, row, slab, fullBar + 8 * s, policy); + } + else + { + tmaLoad3d(dst, &map3, 0, row, slab, fullBar + 8 * s); + } + } + if (kTimeline && u == 0) + { + tFirst = clock64() - t0; + } + } + int64_t const tLast = kTimeline ? clock64() - t0 : 0; + if ((p.flags & kFlagPrefetchFc2) && !(p.flags & kFlagPrefetchFc2Early)) + { + prefetchFc2Share(p, rs.distinct, numDistinct); + } + if (int64_t* row = timelineRow(p, 0)) + { + row[kTlProdFirst] = tFirst; + row[kTlProdLast] = tLast; + row[kTlEmptyBlocked] = emptyBlocked; + } + } + } + if (lane == 0 && !triples) + { + int const myTiles + = static_cast(blockIdx.x) < tiles ? (tiles - 1 - static_cast(blockIdx.x)) / gridDim.x + 1 : 0; + int const total = myTiles * kStagesPerTile; + // Box coordinates of this CTA's u-th stage: row of the tile, (expert, first K chunk) slab. + auto rowOf = [&](int u) + { + int const t = blockIdx.x + (u / kStagesPerTile) * gridDim.x; + return (t % kTilesPerExpert) * kTileRows; + }; + auto slabOf = [&](int u) + { + int const t = blockIdx.x + (u / kStagesPerTile) * gridDim.x; + return rs.distinct[t / kTilesPerExpert] * kFc1Chunks + (u % kStagesPerTile) * kKs; + }; + bool const l2 = (p.flags & kFlagL2Prefetch) != 0; + uint64_t const policy = evictFirst ? l2EvictFirstPolicy() : 0; + if (p.flags & kFlagPrefetchFc2Early) + { + prefetchFc2Share(p, rs.distinct, numDistinct); + } + if (l2) + { + for (int u = 0; u < kL2Ahead && u < total; ++u) + { + tmaPrefetchL2(&map, 0, rowOf(u), slabOf(u)); + } + } + int emptyBlocked = 0; + int64_t tFirst = 0; + for (int u = 0; u < total && !(resident && u == S); ++u) + { + if (l2 && u + kL2Ahead < total) + { + tmaPrefetchL2(&map, 0, rowOf(u + kL2Ahead), slabOf(u + kL2Ahead)); + } + int const s = u % S; + if (u >= S) + { + if constexpr (kTimeline) + { + emptyBlocked += mbarWaitCounted(emptyBar + 8 * s, ((u / S) - 1) & 1); + } + else + { + mbarWait(emptyBar + 8 * s, ((u / S) - 1) & 1); + } + } + mbarArriveExpectTx(fullBar + 8 * s, kSlot); + if constexpr (X) + { +#pragma unroll + for (int b = 0; b < kXBoxes; ++b) + { + tmaLoad3d(ringAddr + s * kSlot + kStage + b * kXBoxBytes, &mapX, 0, + (u % kStagesPerTile) * kKs + b * kXBoxChunks, 0, fullBar + 8 * s); + } + } + if (evictFirst) + { + tmaLoad3dHint(ringAddr + s * kSlot, &map, 0, rowOf(u), slabOf(u), fullBar + 8 * s, policy); + } + else + { + tmaLoad3d(ringAddr + s * kSlot, &map, 0, rowOf(u), slabOf(u), fullBar + 8 * s); + } + if (kTimeline && u == 0) + { + tFirst = clock64() - t0; + } + } + int64_t const tLast = kTimeline ? clock64() - t0 : 0; + if ((p.flags & kFlagPrefetchFc2) && !(p.flags & kFlagPrefetchFc2Early)) + { + prefetchFc2Share(p, rs.distinct, numDistinct); + } + if (int64_t* row = timelineRow(p, 0)) + { + row[kTlProdFirst] = tFirst; + row[kTlProdLast] = tLast; + row[kTlEmptyBlocked] = emptyBlocked; + } + } + } + else + { + cpAsyncWait<0>(); + mmaWarpsSync(); // the activation rows of every MMA thread are in shared memory + int64_t const tSetup = kTimeline ? clock64() - t0 : 0; + int n = 0; // K chunks consumed by this warp (all tiles) + int fullBlocked = 0; + int64_t tFirst = 0; + int64_t tLast = 0; + // Stages are used in ring order; a tile starts on a stage boundary (kFc1Chunks % kKs == 0). The fragment + // bases point at the stage of ring slot ``fetchSlot`` (``aBase``) and at the K chunk of the activation rows + // the current stage starts with (``bBase``); both move by constant steps, so no lane offset stays live. + int fetchSlot = 0; + uint32_t fetchPhase = 0; + int releaseSlot = 0; + int fetchUses = 0; + uint32_t aBase[4]; + uint32_t bBase[2]; + uint32_t bX[2][2]; // X: K chunk parity p of the activation boxes of ring slot ``fetchSlot`` + { + LaneOffsets const lo = laneOffsets(rows, kXRowBytes, lane); + LaneOffsetsX const lx = laneOffsetsX(rows, lane); +#pragma unroll + for (int q = 0; q < 4; ++q) + { + aBase[q] = ringAddr + warp * C * kChunkBytes + lo.a[q]; + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + bBase[h] = xsAddr + lo.b[h]; + bX[0][h] = ringAddr + kStage + lx.b[0][h]; + bX[1][h] = ringAddr + kStage + lx.b[1][h]; + } + } + // Waits for the stage of ring slot ``fetchSlot`` unless an earlier probe found it complete (``ready``). + auto waitStage = [&](bool ready) + { + if constexpr (kTimeline) + { + bool const wait = !resident || fetchUses++ < S; + if (wait && !ready) + { + fullBlocked += mbarWaitCounted(fullBar + 8 * fetchSlot, fetchPhase); + } + tLast = clock64() - t0; + tFirst = tFirst == 0 ? tLast : tFirst; + } + else if (!ready) + { + mbarWait(fullBar + 8 * fetchSlot, fetchPhase); + } + }; + // Moves the A bases (X: and the activation bases) to the next ring slot. + auto nextSlot = [&]() + { + int const step = fetchSlot + 1 == S ? -(S - 1) * kSlot : kSlot; +#pragma unroll + for (int q = 0; q < 4; ++q) + { + aBase[q] += step; + } + if constexpr (X) + { +#pragma unroll + for (int h = 0; h < 2; ++h) + { + bX[0][h] += step; + bX[1][h] += step; + } + } + if (++fetchSlot == S) + { + fetchSlot = 0; + fetchPhase ^= 1; + } + }; + if constexpr (S3 > 0) + { + if (triples && valid3 > 0) + { + // Chain c of this warp: rows 16 warp .. 16 warp + 15 of sub-tile sub0 + c, in box c of each stage. + uint32_t a3[4]; + uint32_t b3[2]; + uint32_t b3X[2][2]; // X: K chunk 0 / 1 of the stage's activation box + { + LaneOffsets const lo = laneOffsets(rows, kXRowBytes, lane); + LaneOffsetsX const lx = laneOffsetsX(rows, lane); +#pragma unroll + for (int q = 0; q < 4; ++q) + { + a3[q] = ringAddr + warp * kChunkBytes + lo.a[q]; + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + b3[h] = xsAddr + lo.b[h]; + b3X[0][h] = ringAddr + kStage3 + lx.b[0][h]; + b3X[1][h] = ringAddr + kStage3 + lx.b[1][h]; + } + } + // The fragments of K chunk j (0 / 1) of the current stage. + auto frags3 = [&](ChunkFrags& f, int j) + { + if constexpr (X) + { + loadFragsStrided(f, a3, j * kChunkStride3, b3X[j], 0, valid3); + } + else + { + loadFragsStrided(f, a3, j * kChunkStride3, b3, j * kKChunk * 2, valid3); + } + }; + float acc[kTripleSubTiles][4]; +#pragma unroll + for (int c = 0; c < kTripleSubTiles; ++c) + { + acc[c][0] = acc[c][1] = acc[c][2] = acc[c][3] = -0.f; + } + ChunkFrags f[2]; + waitStage(false); + if (!noMma) + { + frags3(f[0], 0); + } +#pragma unroll 1 + for (int st = 0; st < kStagesPerTile3; ++st) + { + // K chunk 0 of the stage: fetch chunk 1, MMAs of chunk 0. + if (!noMma) + { + frags3(f[1], 1); + mmaChunkValid(acc, f[0], valid3); + } + if (st + 1 < kStagesPerTile3) + { + // K chunk 1: move to the next stage, probe it, MMAs of chunk 1, then wait only if the probe + // failed and fetch the next stage's chunk 0. + int const step = fetchSlot + 1 == S3 ? -(S3 - 1) * kSlot3 : kSlot3; +#pragma unroll + for (int q = 0; q < 4; ++q) + { + a3[q] += step; + } + if (++fetchSlot == S3) + { + fetchSlot = 0; + fetchPhase ^= 1; + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + b3[h] += kTripleStageChunks * kKChunk * 2; + b3X[0][h] += step; + b3X[1][h] += step; + } + bool const ready = mbarTestWait(fullBar + 8 * fetchSlot, fetchPhase); + if (!noMma) + { + mmaChunkValid(acc, f[1], valid3); + } + waitStage(ready); + if (!noMma) + { + frags3(f[0], 0); + } + } + else if (!noMma) + { + mmaChunkValid(acc, f[1], valid3); + } + // Every thread releases its own reads of the stage (their fragments fed the MMAs above). + mbarArrive(emptyBar + 8 * releaseSlot); + releaseSlot = releaseSlot + 1 == S3 ? 0 : releaseSlot + 1; + n += kTripleStageChunks; + } + int const g = lane >> 2; + int const tq = lane & 3; +#pragma unroll + for (int c = 0; c < kTripleSubTiles; ++c) + { + if (c < valid3) + { + int const sub = sub0 + c; + int const d = sub / kSubPerExpert; + int const a = (sub % kSubPerExpert) * W + warp; + int const j = 16 * (a >> 1) + 2 * g + (a & 1); +#pragma unroll + for (int h = 0; h < 2; ++h) + { + int const tok = 2 * tq + h; + if (tok < rows) + { + p.h[static_cast(d) * kMaxRows * kInter + hImageElem(tok, j)] + = bf16SatFinite(swiGlu(acc[c][h], acc[c][2 + h])); + } + } + } + } + } + } + // The regular path (no tile when the triples path ran). + for (int t = triples ? tiles : static_cast(blockIdx.x); t < tiles; t += gridDim.x) + { + float acc[C][4]; +#pragma unroll + for (int c = 0; c < C; ++c) + { + acc[c][0] = acc[c][1] = acc[c][2] = acc[c][3] = -0.f; + } + // The fragments of chunk j + 1 are loaded before the MMAs of chunk j, across stage boundaries too (the + // next stage is waited for at the last chunk of the current one); f[0] holds a stage's first chunk. + ChunkFrags f[2]; + // The fragments of K chunk j of the current stage (X: chunk j is in activation box j / 2, parity j % 2). + auto frags = [&](ChunkFrags& fr, int j) + { + if constexpr (X) + { + loadFragsAt(fr, aBase, j * kChunkSet, bX[j & 1], (j >> 1) * kXBoxBytes); + } + else + { + loadFragsAt(fr, aBase, j * kChunkSet, bBase, j * 128); + } + }; + if (t != static_cast(blockIdx.x)) + { + nextSlot(); +#pragma unroll + for (int h = 0; h < 2; ++h) + { + bBase[h] -= (kStagesPerTile - 1) * kKs * 128; + } + } + waitStage(false); + if (!noMma) + { + frags(f[0], 0); + } +#pragma unroll 1 + for (int st = 0; st < kStagesPerTile; ++st) + { +#pragma unroll + for (int j = 0; j < kKs; ++j) + { + if (j + 1 < kKs) + { + if (!noMma) + { + frags(f[(j + 1) & 1], j + 1); + mmaChunk(acc, f[j & 1]); + } + } + else if (st + 1 < kStagesPerTile) + { + // Last chunk of the stage: probe the next stage, issue this chunk's MMAs, then wait only if + // the probe failed, so the barrier round trip overlaps the MMAs. + nextSlot(); +#pragma unroll + for (int h = 0; h < 2; ++h) + { + bBase[h] += kKs * 128; + } + bool const ready = mbarTestWait(fullBar + 8 * fetchSlot, fetchPhase); + if (!noMma) + { + mmaChunk(acc, f[j & 1]); + } + waitStage(ready); + if (!noMma) + { + frags(f[0], 0); + } + } + else if (!noMma) + { + mmaChunk(acc, f[j & 1]); + } + } + // Every thread releases its own reads of the stage (their fragments fed the MMAs above). + mbarArrive(emptyBar + 8 * releaseSlot); + releaseSlot = releaseSlot + 1 == S ? 0 : releaseSlot + 1; + n += kKs; + } + int const d = t / kTilesPerExpert; + int const g = lane >> 2; + int const tq = lane & 3; +#pragma unroll + for (int c = 0; c < C; ++c) + { + int const a = (t % kTilesPerExpert) * kChains + warp * C + c; + int const j = 16 * (a >> 1) + 2 * g + (a & 1); +#pragma unroll + for (int h = 0; h < 2; ++h) + { + int const tok = 2 * tq + h; + if (tok < rows) + { + p.h[static_cast(d) * kMaxRows * kInter + hImageElem(tok, j)] + = bf16SatFinite(swiGlu(acc[c][h], acc[c][2 + h])); + } + } + } + } + if (warp == 0 && lane == 0) + { + if (int64_t* row = timelineRow(p, 0)) + { + int64_t const cycles = clock64() - t0; + row[kTlEntryNs] = entryNs; + row[kTlDoneNs] = globalTimerNs(); + row[kTlSmId] = smId(); + row[kTlWait] = tWait; + row[kTlSetup] = tSetup; + row[kTlFirst] = tFirst; + row[kTlLast] = tLast; + row[kTlDone] = cycles; + row[kTlFullBlocked] = fullBlocked; + row[kTlChunks] = n; + row[kTlRouteTokens] = tTokens != 0 ? tTokens - t0 : 0; + row[kTlRouted] = tRouted; + } + } + } + if (!early) + { + // A CTA counts as triggered as soon as one of its threads triggers: wait for the MMA warps, or the idle + // producer lanes would release the dependent grid onto free SMs right after routing. + __syncthreads(); + gridDependencyTrigger(); + } +#endif +} + +//! FC2 ring depth for W MMA warps (stages of W * 8 KB weights + one intermediate entry, about 140 KB in all). +template +constexpr int fc2TmaStages() +{ + return W == 4 ? 4 : (W == 3 ? 5 : (W == 2 ? 7 : 12)); +} + +template +__global__ void __launch_bounds__((W + 1) * 32) + mtpMoeFc2TmaKernel(DraftRoutedMoeParams const p, __grid_constant__ CUtensorMap const map) +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + static_assert(E == 1 || E == 2, "one or two experts per step"); + static_assert(S >= 2 * E, "the ring must hold the step being consumed and the next one"); + // One stage per (group, expert): the expert's W * 16 rows x K = 256 weight box (one 3D TMA) and its + // intermediate entry (one bulk copy), both counted on the stage's full barrier. + constexpr int kWBytes = W * kFc2Chunks * kChunkBytes; + constexpr int kStage = kWBytes + kHEntryBytes; + constexpr int kGroups = kFc2Tiles / W; + static_assert(kWBytes % 1024 == 0 && kHEntryBytes % 1024 == 0, "stages must stay 1 KB aligned"); + extern __shared__ uint8_t smemRaw[]; + __shared__ Fc2Smem fs; + __shared__ __align__(8) uint64_t bars[2 * S]; + int64_t const t0 = kTimeline ? clock64() : 0; + int64_t const entryNs = kTimeline ? globalTimerNs() : 0; + int const warp = threadIdx.x >> 5; + int const lane = threadIdx.x & 31; + int const rows = p.rows; + uint32_t const ringAddr = (smemAddr(smemRaw) + 1023u) & ~1023u; + uint8_t* const stageBase = smemRaw + (ringAddr - smemAddr(smemRaw)) + S * kStage; + uint32_t const fullBar = smemAddr(&bars[0]); + uint32_t const emptyBar = smemAddr(&bars[S]); + if (threadIdx.x == 0) + { + for (int s = 0; s < S; ++s) + { + mbarInit(fullBar + 8 * s, 1); + mbarInit(emptyBar + 8 * s, W * 32); // every MMA thread releases its own reads + } + mbarFenceInit(); + prefetchTensorMap(&map); + } + + // The routing record and the intermediate are written by the FC1 grid. + gridDependencyWait(); + int64_t const tWait = kTimeline ? clock64() - t0 : 0; + bool const noMma = kTimeline && (p.flags & kFlagProbeNoMma) != 0; + // Publishes the barrier initialization. Past this point the producer only needs the distinct experts and the MMA + // warps only the slot and weight tables, so each role reads its part of the routing record by itself. + __syncthreads(); + int const numDistinct = p.route[0]; + + if (warp == W) + { + for (int i = lane; i < kMaxDistinct; i += 32) + { + fs.distinct[i] = p.route[kRouteDistinctOffset + i]; + } + __syncwarp(); + if (lane == 0) + { + // The intermediate was written with generic stores by the previous grid; bulk copies read it. + asm volatile("fence.proxy.async.global;" ::: "memory"); + int u = 0; + int emptyBlocked = 0; + int64_t tFirst = 0; + for (int gi = blockIdx.x; gi < kGroups; gi += gridDim.x) + { + for (int d = 0; d < numDistinct; ++d, ++u) + { + int const s = u % S; + if (u >= S) + { + if constexpr (kTimeline) + { + emptyBlocked += mbarWaitCounted(emptyBar + 8 * s, ((u / S) - 1) & 1); + } + else + { + mbarWait(emptyBar + 8 * s, ((u / S) - 1) & 1); + } + } + mbarArriveExpectTx(fullBar + 8 * s, kWBytes + rows * kInter * 2); + tmaLoad3d( + ringAddr + s * kStage, &map, 0, gi * 16 * W, fs.distinct[d] * kFc2Chunks, fullBar + 8 * s); + bulkLoad(ringAddr + s * kStage + kWBytes, p.h + static_cast(d) * kMaxRows * kInter, + rows * kInter * 2, fullBar + 8 * s); + if (kTimeline && u == 0) + { + tFirst = clock64() - t0; + } + } + } + if (int64_t* row = timelineRow(p, 1)) + { + row[kTlProdFirst] = tFirst; + row[kTlProdLast] = clock64() - t0; + row[kTlEmptyBlocked] = emptyBlocked; + } + } + } + else + { + // Slot and weight tables, shared by the W MMA warps only (named barrier 1); first read at the end of a step. + for (int i = threadIdx.x; i < kMaxRows * kMaxDistinct; i += W * 32) + { + fs.slotOf[i / kMaxDistinct][i % kMaxDistinct] = -1; + } + mmaWarpsSync(); + for (int i = threadIdx.x; i < rows * kTopK; i += W * 32) + { + int const t = i / kTopK; + int const k = i % kTopK; + fs.slotOf[t][p.route[kRouteSlotOffset + i]] = static_cast(k); + fs.weight[t][k] = __int_as_float(p.route[kRouteWeightOffset + i]); + } + mmaWarpsSync(); + int64_t const tSetup = kTimeline ? clock64() - t0 : 0; + __nv_bfloat16* stage = reinterpret_cast<__nv_bfloat16*>(stageBase + warp * kStageBytes); + int u0 = 0; // stage uses of the previous groups + int fullBlocked = 0; + int64_t tFirst = 0; + int64_t tLast = 0; + // A group's experts are taken E at a time (a step); step j holds experts E*j .. E*j + E - 1, the last step + // may hold fewer. Each expert has its own stage and accumulator; their MMAs are interleaved per k-step. + int const steps = (numDistinct + E - 1) / E; + int fetchSlot = 0; // stages are used in ring order, one per (group, expert) + uint32_t fetchPhase = 0; + int releaseSlot = 0; + // Fragment bases of the next ring slot to be fetched; they move by constant steps. + uint32_t slotA[4]; + uint32_t slotB[2]; + { + LaneOffsets const lo = laneOffsets(rows, kInter * 2, lane); +#pragma unroll + for (int q = 0; q < 4; ++q) + { + slotA[q] = ringAddr + warp * kChunkBytes + lo.a[q]; + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + slotB[h] = ringAddr + kWBytes + lo.b[h]; + } + } + uint32_t aBase[E][4]; + uint32_t bBase[E][2]; + // Points the fragment bases at the stages of step j (one per expert of the step, in ring order) and probes + // them; finishStep waits for those the probe did not find complete. A step past the last expert reads the + // step's first expert again (results discarded). + uint32_t pendBar[E]; + uint32_t pendParity[E]; + bool pendWait[E]; + auto enterStep = [&](int j) + { +#pragma unroll + for (int e = 0; e < E; ++e) + { + pendWait[e] = false; + if (E * j + e < numDistinct) + { + pendBar[e] = fullBar + 8 * fetchSlot; + pendParity[e] = fetchPhase; + pendWait[e] = !mbarTestWait(pendBar[e], pendParity[e]); +#pragma unroll + for (int q = 0; q < 4; ++q) + { + aBase[e][q] = slotA[q]; + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + bBase[e][h] = slotB[h]; + } + int const step = fetchSlot + 1 == S ? -(S - 1) * kStage : kStage; +#pragma unroll + for (int q = 0; q < 4; ++q) + { + slotA[q] += step; + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + slotB[h] += step; + } + if (++fetchSlot == S) + { + fetchSlot = 0; + fetchPhase ^= 1; + } + } + else + { +#pragma unroll + for (int q = 0; q < 4; ++q) + { + aBase[e][q] = aBase[0][q]; + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + bBase[e][h] = bBase[0][h]; + } + } + } + }; + auto finishStep = [&]() + { +#pragma unroll + for (int e = 0; e < E; ++e) + { + if (pendWait[e]) + { + if constexpr (kTimeline) + { + ++fullBlocked; + } + mbarWait(pendBar[e], pendParity[e]); + } + } + if constexpr (kTimeline) + { + tLast = clock64() - t0; + tFirst = tFirst == 0 ? tLast : tFirst; + } + }; + // K chunk kc of every expert of the current step. + auto load = [&](ExpertFrags& f, int kc) + { +#pragma unroll + for (int e = 0; e < E; ++e) + { +#pragma unroll + for (int q = 0; q < 4; ++q) + { + ldmatrixX4(f.a[e][q], aBase[e][q] + kc * W * kChunkBytes); + } +#pragma unroll + for (int h = 0; h < 2; ++h) + { + ldmatrixX4(f.b[e][h], bBase[e][h] + kc * 128); + } + } + }; + for (int gi = blockIdx.x; gi < kGroups; gi += gridDim.x) + { + int const tile = gi * W + warp; + float acc[E][4]; +#pragma unroll + for (int e = 0; e < E; ++e) + { + acc[e][0] = acc[e][1] = acc[e][2] = acc[e][3] = -0.f; + } + // The fragments of chunk kc + 1 are loaded before the MMAs of chunk kc, across steps too (the next + // step's accumulators are independent); f[0] holds a step's first chunk. + ExpertFrags f[2]; + enterStep(0); + finishStep(); + if (!noMma) + { + load(f[0], 0); + } +#pragma unroll 1 + for (int j = 0; j < steps; ++j) + { +#pragma unroll + for (int kc = 0; kc < kFc2Chunks; ++kc) + { + auto mmas = [&]() + { + if (!noMma) + { +#pragma unroll + for (int q = 0; q < 4; ++q) + { +#pragma unroll + for (int e = 0; e < E; ++e) + { + ExpertFrags const& fr = f[kc & 1]; + mmaBf16(acc[e], fr.a[e][q], fr.b[e][q >> 1][2 * (q & 1)], + fr.b[e][q >> 1][2 * (q & 1) + 1]); + } + } + } + }; + if (kc + 1 < kFc2Chunks) + { + if (!noMma) + { + load(f[(kc + 1) & 1], kc + 1); + } + mmas(); + } + else if (j + 1 < steps) + { + // Last chunk of the step: probe the next step's stages, issue this chunk's MMAs, then wait + // only for stages the probe did not find complete. + enterStep(j + 1); + mmas(); + finishStep(); + if (!noMma) + { + load(f[0], 0); + } + } + else + { + mmas(); + } + } + // Every thread releases its own reads of the step's stages (their fragments fed the MMAs above). +#pragma unroll + for (int e = 0; e < E; ++e) + { + if (E * j + e < numDistinct) + { + mbarArrive(emptyBar + 8 * releaseSlot); + releaseSlot = releaseSlot + 1 == S ? 0 : releaseSlot + 1; + } + } + // FC2 rows of each expert -> BF16, into the token's slot of the warp's staging area. + int const g = lane >> 2; + int const tq = lane & 3; +#pragma unroll + for (int e = 0; e < E; ++e) + { + int const d = E * j + e; +#pragma unroll + for (int c = 0; c < 2; ++c) + { + int const tok = 2 * tq + c; + int const k = (d < numDistinct && tok < rows) ? fs.slotOf[tok][d] : -1; + if (k >= 0) + { + __nv_bfloat16 const lo16 = bf16SatFinite(acc[e][c]); + __nv_bfloat16 const hi16 = bf16SatFinite(acc[e][2 + c]); + stage[(tok * kTopK + k) * 16 + g] = lo16; + stage[(tok * kTopK + k) * 16 + g + 8] = hi16; + if (p.debugFc2 != nullptr) + { + int64_t const base = (static_cast(tok) * kTopK + k) * kHidden; + p.debugFc2[base + hiddenOfStoredRow(tile * 16 + g)] = lo16; + p.debugFc2[base + hiddenOfStoredRow(tile * 16 + g + 8)] = hi16; + } + } + } + acc[e][0] = acc[e][1] = acc[e][2] = acc[e][3] = -0.f; + } + } + u0 += numDistinct; + // Native finalize of the tile's 16 hidden rows for every token: FP32 fma chain from 0 in slot order. + __syncwarp(); + for (int pr = lane; pr < rows * 16; pr += 32) + { + int const tok = pr >> 4; + int const row = pr & 15; + float sum = 0.f; +#pragma unroll + for (int k = 0; k < kTopK; ++k) + { + sum = fmaFtz(fs.weight[tok][k], __bfloat162float(stage[(tok * kTopK + k) * 16 + row]), sum); + } + p.out[static_cast(tok) * kHidden + hiddenOfStoredRow(tile * 16 + row)] + = __float2bfloat16_rn(sum); + } + __syncwarp(); + } + if (warp == 0 && lane == 0) + { + if (int64_t* row = timelineRow(p, 1)) + { + int64_t const cycles = clock64() - t0; + row[kTlEntryNs] = entryNs; + row[kTlDoneNs] = globalTimerNs(); + row[kTlSmId] = smId(); + row[kTlWait] = tWait; + row[kTlSetup] = tSetup; + row[kTlFirst] = tFirst; + row[kTlLast] = tLast; + row[kTlDone] = cycles; + row[kTlFullBlocked] = fullBlocked; + row[kTlChunks] = u0 * kFc2Chunks; + } + } + } +#endif +} + +template +cudaError_t launchOne(Kernel kernel, int ctas, int threads, int smemBytes, bool pdl, DraftRoutedMoeParams const& params, + cudaStream_t stream) +{ + cudaError_t status = cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smemBytes); + if (status != cudaSuccess) + { + return status; + } + cudaLaunchConfig_t config{}; + config.gridDim = dim3(ctas); + config.blockDim = dim3(threads); + config.dynamicSmemBytes = smemBytes; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = pdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 1; + return cudaLaunchKernelEx(&config, kernel, params); +} + +template +cudaError_t launchTma(Kernel kernel, int ctas, int threads, int smemBytes, bool pdl, DraftRoutedMoeParams const& params, + CUtensorMap const& map, cudaStream_t stream) +{ + cudaError_t status = cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smemBytes); + if (status != cudaSuccess) + { + return status; + } + cudaLaunchConfig_t config{}; + config.gridDim = dim3(ctas); + config.blockDim = dim3(threads); + config.dynamicSmemBytes = smemBytes; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = pdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 1; + return cudaLaunchKernelEx(&config, kernel, params, map); +} + +template +cudaError_t launchTma3(Kernel kernel, int ctas, int threads, int smemBytes, bool pdl, + DraftRoutedMoeParams const& params, CUtensorMap const& map, CUtensorMap const& map3, CUtensorMap const& mapX, + cudaStream_t stream) +{ + cudaError_t status = cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, smemBytes); + if (status != cudaSuccess) + { + return status; + } + cudaLaunchConfig_t config{}; + config.gridDim = dim3(ctas); + config.blockDim = dim3(threads); + config.dynamicSmemBytes = smemBytes; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = pdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 1; + return cudaLaunchKernelEx(&config, kernel, params, map, map3, mapX); +} + +template +cudaError_t launchTma2(Kernel kernel, int ctas, int threads, int smemBytes, bool pdl, + DraftRoutedMoeParams const& params, CUtensorMap const& map, CUtensorMap const& map3, cudaStream_t stream) +{ + return launchTma3(kernel, ctas, threads, smemBytes, pdl, params, map, map3, map, stream); +} + +} // namespace + +int fc1SmemBytes(int rows, int warps, bool deep) +{ + return rows * kXRowBytes + warps * (deep ? kFc1StagesDeep : kFc1Stages) * kChunkBytes; +} + +int fc2SmemBytes(int warps, bool deep) +{ + return warps * ((deep ? kFc2StagesDeep : kFc2Stages) * kChunkBytes + kFc2HStages * kHEntryBytes + kStageBytes); +} + +namespace +{ + +template +cudaError_t launchFc1(bool pairs, int smem, bool pdl, DraftRoutedMoeParams const& params, cudaStream_t stream) +{ + return pairs ? launchOne(mtpMoeFc1Kernel, params.fc1Ctas, 32 * W, smem, pdl, params, stream) + : launchOne(mtpMoeFc1Kernel, params.fc1Ctas, 32 * W, smem, pdl, params, stream); +} + +template +cudaError_t launchFc2(bool pairs, int smem, bool pdl, DraftRoutedMoeParams const& params, cudaStream_t stream) +{ + return pairs ? launchOne(mtpMoeFc2Kernel, params.fc2Ctas, 32 * W, smem, pdl, params, stream) + : launchOne(mtpMoeFc2Kernel, params.fc2Ctas, 32 * W, smem, pdl, params, stream); +} + +template +cudaError_t launchFc1Warps(bool pairs, int smem, bool pdl, DraftRoutedMoeParams const& params, cudaStream_t stream) +{ + switch (params.fc1Warps) + { + case 1: return launchFc1<1, S>(pairs, smem, pdl, params, stream); + case 2: return launchFc1<2, S>(pairs, smem, pdl, params, stream); + case 4: return launchFc1<4, S>(pairs, smem, pdl, params, stream); + default: return cudaErrorInvalidValue; + } +} + +template +cudaError_t launchFc2Warps(bool pairs, int smem, bool pdl, DraftRoutedMoeParams const& params, cudaStream_t stream) +{ + switch (params.fc2Warps) + { + case 1: return launchFc2<1, S>(pairs, smem, pdl, params, stream); + case 2: return launchFc2<2, S>(pairs, smem, pdl, params, stream); + case 4: return launchFc2<4, S>(pairs, smem, pdl, params, stream); + default: return cudaErrorInvalidValue; + } +} + +} // namespace + +int fc1TmaSmemBytes(int rows, int stages) +{ + return 1024 + stages * kTmaStageChunks * kChunkBytes + rows * kXRowBytes; +} + +int fc1TmaTriplesSmemBytes(int rows, int stages, int tripleStages) +{ + int const ring = stages * kTmaStageChunks * kChunkBytes; + int const ring3 = tripleStages * kTripleSubTiles * kTripleStageChunks * kTripleRows * kKChunk * 2; + return 1024 + (ring > ring3 ? ring : ring3) + rows * kXRowBytes; +} + +int fc1TmaStreamXSmemBytes() +{ + int const ring = 6 * (kTmaStageChunks * kChunkBytes + kXBoxBytes); + int const ring3 = 4 * (kTripleSubTiles * kTripleRows * kTripleStageChunks * kKChunk * 2 + kXBoxBytes); + return 1024 + (ring > ring3 ? ring : ring3); +} + +int fc2TmaSmemBytes(int warps, int stages) +{ + return 1024 + stages * (warps * kFc2Chunks * kChunkBytes + kHEntryBytes) + warps * kStageBytes; +} + +namespace +{ + +//! Dynamic shared memory budget (the static routing state and barriers take about 3.5 KB of the 227 KB). +constexpr int kMaxDynamicSmem = 220 * 1024; + +//! FC1 kernel with ring depth S (and S3 for the triples path, 4 MMA warps only; 0: no triples path). The instantiated +//! (S, S3) pairs are those launchFc1TmaC chooses: (6, 4), (4, 3), (4, 2), (3, 2). +template +cudaError_t launchFc1TmaCS(int smem, int tripleStages, bool pdl, DraftRoutedMoeParams const& params, + DraftRoutedMoeTensorMaps const& maps, cudaStream_t stream) +{ + int const n = params.fc1Ctas; + CUtensorMap const& w = maps.w13; + switch (params.fc1Warps) + { + case 1: return launchTma2(mtpMoeFc1TmaKernel<1, C, S, 0>, n, 64, smem, pdl, params, w, w, stream); + case 2: return launchTma2(mtpMoeFc1TmaKernel<2, C, S, 0>, n, 96, smem, pdl, params, w, w, stream); + case 4: + { + CUtensorMap const& w3 = maps.w13Triple; + if (tripleStages == 0) + { + return launchTma2(mtpMoeFc1TmaKernel<4, C, S, 0>, n, 160, smem, pdl, params, w, w, stream); + } + if constexpr (S == 6) + { + if (tripleStages == 4) + { + return launchTma2(mtpMoeFc1TmaKernel<4, C, 6, 4>, n, 160, smem, pdl, params, w, w3, stream); + } + } + if constexpr (S == 4) + { + if (tripleStages == 3) + { + return launchTma2(mtpMoeFc1TmaKernel<4, C, 4, 3>, n, 160, smem, pdl, params, w, w3, stream); + } + if (tripleStages == 2) + { + return launchTma2(mtpMoeFc1TmaKernel<4, C, 4, 2>, n, 160, smem, pdl, params, w, w3, stream); + } + } + if constexpr (S == 3) + { + if (tripleStages == 2) + { + return launchTma2(mtpMoeFc1TmaKernel<4, C, 3, 2>, n, 160, smem, pdl, params, w, w3, stream); + } + } + return cudaErrorInvalidValue; + } + default: return cudaErrorInvalidValue; + } +} + +//! FC1 ring depth: the deepest of 6 / 4 / 3 stages of 32 KB that fits next to the activation rows; with +//! kFlagFc1Triples and 4 MMA warps also the deepest triples ring (48 KB stages) of the instantiated pairs that fits. +//! kFlagStreamX (4 MMA warps, tile pairs; ignored otherwise): 6 stages of 32 + 2 KB and 4 triples stages of 48 + 2 KB +//! for any row count. +template +cudaError_t launchFc1TmaC( + bool pdl, DraftRoutedMoeParams const& params, DraftRoutedMoeTensorMaps const& maps, cudaStream_t stream) +{ + if constexpr (C == 2) + { + if ((params.flags & kFlagStreamX) != 0 && params.fc1Warps == 4) + { + int const smem = fc1TmaStreamXSmemBytes(); + return launchTma3(mtpMoeFc1TmaKernel<4, 2, 6, 4, true>, params.fc1Ctas, 160, smem, pdl, params, maps.w13, + maps.w13Triple, maps.x, stream); + } + } + bool const triples = (params.flags & kFlagFc1Triples) != 0 && params.fc1Warps == 4; + for (int stages : {6, 4, 3}) + { + int smem = fc1TmaSmemBytes(params.rows, stages); + if (smem > kMaxDynamicSmem) + { + continue; + } + int tripleStages = 0; + if (triples) + { + for (int t : {4, 3, 2}) + { + bool const instantiated + = (stages == 6 && t == 4) || (stages == 4 && (t == 3 || t == 2)) || (stages == 3 && t == 2); + if (instantiated && fc1TmaTriplesSmemBytes(params.rows, stages, t) <= kMaxDynamicSmem) + { + tripleStages = t; + smem = fc1TmaTriplesSmemBytes(params.rows, stages, t); + break; + } + } + if (tripleStages == 0) + { + return cudaErrorInvalidValue; + } + } + switch (stages) + { + case 6: return launchFc1TmaCS(smem, tripleStages, pdl, params, maps, stream); + case 4: return launchFc1TmaCS(smem, tripleStages, pdl, params, maps, stream); + default: return launchFc1TmaCS(smem, tripleStages, pdl, params, maps, stream); + } + } + return cudaErrorInvalidValue; +} + +template +cudaError_t launchFc2TmaW(bool pdl, DraftRoutedMoeParams const& params, CUtensorMap const& map, cudaStream_t stream) +{ + constexpr int kS = fc2TmaStages(); + int const smem = fc2TmaSmemBytes(W, kS); + return (params.flags & kFlagFc2Pairs) + ? launchTma(mtpMoeFc2TmaKernel, params.fc2Ctas, (W + 1) * 32, smem, pdl, params, map, stream) + : launchTma(mtpMoeFc2TmaKernel, params.fc2Ctas, (W + 1) * 32, smem, pdl, params, map, stream); +} + +cudaError_t launchTmaPair(DraftRoutedMoeParams const& params, DraftRoutedMoeTensorMaps const& maps, cudaStream_t stream) +{ + bool const pdl = (params.flags & kFlagPdl) != 0; + cudaError_t status = (params.flags & kFlagFc1Pairs) ? launchFc1TmaC<2>(pdl, params, maps, stream) + : launchFc1TmaC<1>(pdl, params, maps, stream); + if (status != cudaSuccess) + { + return status; + } + switch (params.fc2Warps) + { + case 1: return launchFc2TmaW<1>(pdl, params, maps.w2, stream); + case 2: return launchFc2TmaW<2>(pdl, params, maps.w2, stream); + case 3: return launchFc2TmaW<3>(pdl, params, maps.w2, stream); + case 4: return launchFc2TmaW<4>(pdl, params, maps.w2, stream); + default: return cudaErrorInvalidValue; + } +} + +} // namespace + +cudaError_t invokeDraftRoutedMoe( + DraftRoutedMoeParams const& params, DraftRoutedMoeTensorMaps const* maps, cudaStream_t stream) +{ + if (params.rows < 1 || params.rows > kMaxRows || params.fc1Ctas < 1 || params.fc2Ctas < 1) + { + return cudaErrorInvalidValue; + } + if (params.flags & kFlagTma) + { + return maps != nullptr ? launchTmaPair(params, *maps, stream) : cudaErrorInvalidValue; + } + bool const pdl = (params.flags & kFlagPdl) != 0; + bool const pairs1 = (params.flags & kFlagFc1Pairs) != 0; + bool const pairs2 = (params.flags & kFlagFc2Pairs) != 0; + bool const deep = (params.flags & kFlagDeepRing) != 0; + int const smem1 = fc1SmemBytes(params.rows, params.fc1Warps, deep); + cudaError_t status = deep ? launchFc1Warps(pairs1, smem1, pdl, params, stream) + : launchFc1Warps(pairs1, smem1, pdl, params, stream); + if (status != cudaSuccess) + { + return status; + } + int const smem2 = fc2SmemBytes(params.fc2Warps, deep); + return deep ? launchFc2Warps(pairs2, smem2, pdl, params, stream) + : launchFc2Warps(pairs2, smem2, pdl, params, stream); +} + +} // namespace kernels::glm_moe_dsa::draft_routed_moe + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.h new file mode 100644 index 000000000000..19d8adada74b --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.h @@ -0,0 +1,205 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" + +#include +#include +#include + +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::draft_routed_moe +{ + +//! The routed experts of the GLM-5.2 MTP draft layer (BF16 weights, TP8/EP1 rank shard) on the +//! FlashInfer TRT-LLM-Gen BF16 layouts, bit-identical to ``trtllm_bf16_moe`` (DeepSeek-V3 routing with +//! ``n_group <= 1``, SwiGLU, ``do_finalize=True``). +//! +//! Two kernels, chained with programmatic dependent launch: +//! +//! * ``fc1``: every CTA computes the native top-8 routing of all rows from the FP32 router logits (sigmoid via +//! ``tanh.approx``, selection key ``sigmoid + bias``, ties to the lower expert index, NaN keys first, weights +//! ``bf16(sigmoid * scale / (sum + 1e-20))`` with the native FP32 sum order and approximate division), then its +//! warps stream 16-row tiles of the distinct experts' gate/up weights (BlockMajorK, rows shuffled for the gated +//! epilogue) and run ``mma.sync.m16n8k16`` over K = 6144 in natural order with one FP32 accumulator per tile, +//! followed by the native SwiGLU epilogue. The BF16 intermediate goes to a scratch buffer. +//! * ``fc2``: after the FC1 grid has completed, each warp computes 16 down-projection rows for every distinct +//! expert (K = 256 in natural order), rounds each row to BF16 like the native FC2 epilogue, and finalizes the +//! rows of every token in top-k slot order (``fma`` from 0, one BF16 rounding), as ``finalizeKernel`` does. +//! +//! The weights are read in place: ``w13`` is ``[256][96][512][64]`` and ``w2`` is ``[256][4][6144][64]`` (BF16). + +constexpr int kHidden = 6144; +constexpr int kInter = 256; //!< Per-rank intermediate size (TP8). +constexpr int kFc1Rows = 512; //!< Gate and up rows per expert per rank. +constexpr int kNumExperts = 256; +constexpr int kTopK = 8; +constexpr int kMaxRows = 8; +constexpr int kMaxDistinct = kMaxRows * kTopK; +constexpr int kKChunk = 64; //!< BlockMajorK block (elements, 128 bytes). +constexpr int kFc1Chunks = kHidden / kKChunk; +constexpr int kFc2Chunks = kInter / kKChunk; +constexpr int kFc1Tiles = kFc1Rows / 16; +constexpr int kFc2Tiles = kHidden / 16; + +//! Routing record written by the FC1 grid and read by the FC2 grid (int32 words). +constexpr int kRouteDistinctOffset = 1; //!< [kMaxDistinct] expert ids, ascending. +constexpr int kRouteSlotOffset = kRouteDistinctOffset + kMaxDistinct; //!< [kMaxRows][kTopK] distinct index. +constexpr int kRouteWeightOffset = kRouteSlotOffset + kMaxRows * kTopK; //!< [kMaxRows][kTopK] float bits of bf16. +constexpr int kRouteWords = kRouteWeightOffset + kMaxRows * kTopK; +//! Intermediate scratch: [kMaxDistinct][kMaxRows][kInter] BF16. +constexpr int64_t kScratchHElems = int64_t{kMaxDistinct} * kMaxRows * kInter; + +//! Optional behaviour (``flags``). +enum DraftRoutedMoeFlags : int +{ + kFlagPdl = 1, //!< Programmatic dependent launch for both kernels (as the native kernels). + kFlagPrefetchFc2 = 2, //!< FC1 warps prefetch the distinct experts' FC2 weights into L2 after their tiles. + kFlagEarlyTrigger = 4, //!< FC1 triggers its dependents after routing instead of after its tiles. + kFlagFc1Pairs = 8, //!< FC1 warps interleave two tiles (two independent accumulation chains). + kFlagFc2Pairs = 16, //!< FC2 warps interleave two experts (two independent accumulation chains). + kFlagDeepRing = 32, //!< Deeper weight rings (FC1 20, FC2 12 chunks of 2 KB per warp instead of 12 and 8). + kFlagTma = 64, //!< TMA-fed kernels: one producer thread streams 16W-row weight boxes into a ring that W + //!< MMA warps consume (same per-chain arithmetic). Needs the tensor maps of invokeDraftRoutedMoe. + //!< With it, kFlagFc1Pairs gives every FC1 MMA warp two 16-row chains (boxes of 32W + //!< rows), kFlagFc2Pairs gives every FC2 MMA warp two experts per step (two stages, two + //!< accumulators), kFlagPrefetchFc2 is issued by the FC1 producer after its last weight + //!< copy, and kFlagDeepRing does not apply (the rings are sized to the shared memory). + kFlagL2Prefetch = 128, //!< TMA FC1: the producer also prefetches its weight boxes into L2, 2 x ring depth ahead. + kFlagPrefetchFc2Early = 256, //!< TMA FC1: the producer prefetches its share of the FC2 weights right after routing. + //! GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE builds only: MMA warps wait for and release stages without reading them + //! (memory schedule alone; the output is meaningless). + kFlagProbeNoMma = 512, + //! GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE builds only: the FC1 producer fills the ring once and the MMA warps cycle + //! over the resident stages without waiting (MMA warps alone; meaningless output). + kFlagProbeResident = 1024, + //! TMA FC1 with 4 MMA warps: when the call's tiles (decided on the device from the number of distinct experts) + //! would need more than one round of the grid, every CTA instead takes three consecutive 64-row sub-tiles (three + //! chains per MMA warp, three TMA boxes per stage), if those fit in one round. Needs + //! DraftRoutedMoeTensorMaps::w13Triple. + kFlagFc1Triples = 4096, + //! TMA FC1: the early trigger of kFlagEarlyTrigger for calls on the kFlagFc1Triples path only (other calls trigger + //! after their tiles). + kFlagEarlyTriggerTriples = 8192, + //! TMA FC1: the weight boxes are loaded with an L2 evict-first policy (each is read once per call), so that they + //! do not displace FC2 weights prefetched into L2. + kFlagFc1EvictFirst = 32768, + //! TMA FC1 with 4 MMA warps and tile pairs (ignored otherwise): the activation rows are not kept in shared + //! memory; every ring stage also carries the activation K chunks it needs (boxes of DraftRoutedMoeTensorMaps::x), + //! so the weight ring is 6 stages deep (4 on the triples path) for every row count. Needs + //! DraftRoutedMoeTensorMaps::x. + kFlagStreamX = 65536 +}; + +//! Event record of the probe builds (compiled with ``GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE=1``): int64 +//! ``[2][kTimelineCtas][kTimelineSlots]`` (FC1, FC2), one row per CTA; see TimelineSlot in draftRoutedMoe.cu. +constexpr int kTimelineCtas = 1024; +constexpr int kTimelineSlots = 16; + +//! K chunks per FC1 stage of the TMA-fed kernel for all its 16-row chains together (32 KB per stage and box). +constexpr int kTmaStageChunks = 16; + +//! Sub-tiles per CTA, rows per sub-tile and K chunks per stage of the kFlagFc1Triples path (4 MMA warps): a stage is +//! three boxes of 64 rows x 2 K chunks (16 KB each). +constexpr int kTripleSubTiles = 3; +constexpr int kTripleRows = 64; +constexpr int kTripleStageChunks = 2; + +//! Activation box of kFlagStreamX: 8 tokens (rows beyond the call's are zero-filled) x kXBoxChunks K chunks. +constexpr int kXBoxChunks = 2; +constexpr int kXBoxBytes = 8 * kXBoxChunks * kKChunk * 2; + +//! Tensor maps of the TMA-fed kernels (kFlagTma): 3D views [slab][row][64] BF16 of ``w13`` (256 * 96 slabs of 512 +//! rows) and ``w2`` (256 * 4 slabs of 6144 rows), 128-byte swizzle; boxes of 64 x fc1TmaBoxRows x fc1TmaBoxSlabs +//! (FC1) and 64 x 16 * fc2Warps x 4 (FC2). +struct DraftRoutedMoeTensorMaps +{ + CUtensorMap w13; + CUtensorMap w2; + CUtensorMap w13Triple; //!< kFlagFc1Triples: view of ``w13`` with boxes of 64 x kTripleRows x kTripleStageChunks. + //! kFlagStreamX: 3D view [token][K chunk][64] of the call's ``x`` (dims 64, 96, rows), boxes of 64 x kXBoxChunks x + //! 8, 128-byte swizzle. + CUtensorMap x; +}; + +struct DraftRoutedMoeParams +{ + float const* logits; //!< [rows, 256] FP32. + __nv_bfloat16 const* bias; //!< [256] router bias. + __nv_bfloat16 const* x; //!< [rows, 6144] activation (16-byte aligned). + __nv_bfloat16 const* w13; //!< [256][96][512][64]. + __nv_bfloat16 const* w2; //!< [256][4][6144][64]. + __nv_bfloat16* out; //!< [rows, 6144] finalized routed output. + __nv_bfloat16* h; //!< Scratch, kScratchHElems. + int32_t* route; //!< Scratch, kRouteWords. + __nv_bfloat16* debugFc2; //!< Optional [rows, 8, 6144]: FC2 rows per (token, slot) in hidden order. + int64_t* timeline; //!< Optional event record (GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE builds; ignored otherwise). + int rows; + float routedScaling; + int fc1Ctas; + int fc1Warps; //!< 1, 2 or 4 warps per FC1 CTA. + int fc2Ctas; + int fc2Warps; //!< 1, 2 or 4 warps per FC2 CTA (also 3 with kFlagTma: 128 groups of 48 rows). + int flags; +}; + +//! Dynamic shared memory of the FC1 kernel for ``rows`` and ``warps`` (bytes); ``deep``: kFlagDeepRing. +int fc1SmemBytes(int rows, int warps, bool deep); + +//! Dynamic shared memory of the FC2 kernel for ``warps`` (bytes); ``deep``: kFlagDeepRing. +int fc2SmemBytes(int warps, bool deep); + +//! Dynamic shared memory of the TMA-fed FC1 kernel (bytes, incl. 1 KB alignment slack); stages: ring depth. +int fc1TmaSmemBytes(int rows, int stages); + +//! Dynamic shared memory of the TMA-fed FC1 kernel with the kFlagFc1Triples path (bytes): the larger of the two +//! rings (``stages`` of 32 KB, ``tripleStages`` of three sub-tile boxes) next to the activation rows. +int fc1TmaTriplesSmemBytes(int rows, int stages, int tripleStages); + +//! Dynamic shared memory of the TMA-fed FC1 kernel with kFlagStreamX (bytes): the larger of the 6-stage regular and +//! the 4-stage triples rings, activation boxes included. +int fc1TmaStreamXSmemBytes(); + +//! Dynamic shared memory of the TMA-fed FC2 kernel (bytes, incl. 1 KB alignment slack); stages: ring depth. +int fc2TmaSmemBytes(int warps, int stages); + +//! Launches the FC1 and FC2 kernels on ``stream``; ``fc1Warps``/``fc2Warps`` must be 1, 2 or 4 (``fc2Warps`` also 3 +//! with kFlagTma). ``maps`` is required +//! with kFlagTma (see DraftRoutedMoeTensorMaps) and ignored otherwise. +cudaError_t invokeDraftRoutedMoe( + DraftRoutedMoeParams const& params, DraftRoutedMoeTensorMaps const* maps, cudaStream_t stream); + +//! Rows of the FC1 weight box of the TMA-fed kernel (16 per chain). +inline int fc1TmaBoxRows(int flags, int fc1Warps) +{ + return 16 * fc1Warps * ((flags & kFlagFc1Pairs) ? 2 : 1); +} + +//! K chunks (slabs) of the FC1 weight box of the TMA-fed kernel. +inline int fc1TmaBoxSlabs(int flags, int fc1Warps) +{ + return kTmaStageChunks * 16 / fc1TmaBoxRows(flags, fc1Warps); +} + +} // namespace kernels::glm_moe_dsa::draft_routed_moe + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/draftTail.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftTail.cu new file mode 100644 index 000000000000..a22ae0d29395 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftTail.cu @@ -0,0 +1,1568 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/assert.h" +#include "tensorrt_llm/common/cudaUtils.h" +#include "tensorrt_llm/kernels/glmMoeDsa/draftTail.h" + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::draft_tail +{ + +namespace +{ + +// --------------------------------------------------------------------------- +// Candidate keys +// --------------------------------------------------------------------------- + +// Monotone map of BF16 bits to 16 bits: -0 and +0 compare equal, NaN of either +// sign ranks above +inf (as the native torch.max reduction does). +__device__ __forceinline__ uint32_t orderedBf16(uint32_t bits) +{ + uint32_t const mag = bits & 0x7FFFu; + if (mag > 0x7F80u) + { + return 0xFFFFu; + } + if (mag == 0u) + { + bits = 0u; + } + return (bits & 0x8000u) ? (~bits & 0xFFFFu) : (bits | 0x8000u); +} + +// Larger key == larger value, then smaller global id. Never zero. +__device__ __forceinline__ uint64_t makeKey(uint32_t bf16Bits, int64_t globalId) +{ + return (static_cast(orderedBf16(bf16Bits)) << 32) + | static_cast(0xFFFFFFFFu - static_cast(globalId)); +} + +__device__ __forceinline__ int32_t keyToToken(uint64_t key) +{ + return static_cast(0xFFFFFFFFu - static_cast(key & 0xFFFFFFFFull)); +} + +__device__ __forceinline__ uint64_t umax64(uint64_t a, uint64_t b) +{ + return a > b ? a : b; +} + +__device__ __forceinline__ uint64_t warpMax64(uint64_t v) +{ +#pragma unroll + for (int off = 16; off > 0; off >>= 1) + { + v = umax64(v, __shfl_xor_sync(0xFFFFFFFFu, v, off)); + } + return v; +} + +// --------------------------------------------------------------------------- +// Memory helpers +// --------------------------------------------------------------------------- + +__device__ __forceinline__ void griddepWait() +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + asm volatile("griddepcontrol.wait;" ::: "memory"); +#endif +} + +__device__ __forceinline__ uint64_t ldRelaxedSys(uint64_t const* addr) +{ + uint64_t v; + asm volatile("ld.relaxed.sys.global.u64 %0, [%1];" : "=l"(v) : "l"(addr) : "memory"); + return v; +} + +__device__ __forceinline__ void stRelaxedSys(uint64_t* addr, uint64_t v) +{ + asm volatile("st.relaxed.sys.global.u64 [%0], %1;" ::"l"(addr), "l"(v) : "memory"); +} + +__device__ __forceinline__ void mcStoreRelaxedSys(uint64_t* mcAddr, uint64_t v) +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + asm volatile("multimem.st.relaxed.sys.global.b64 [%0], %1;" ::"l"(mcAddr), "l"(v) : "memory"); +#endif +} + +__device__ __forceinline__ uint64_t evictFirstPolicy() +{ + uint64_t policy; + asm volatile("createpolicy.fractional.L2::evict_first.b64 %0, 1.0;" : "=l"(policy)); + return policy; +} + +__device__ __forceinline__ uint64_t evictLastPolicy() +{ + uint64_t policy; + asm volatile("createpolicy.fractional.L2::evict_last.b64 %0, 1.0;" : "=l"(policy)); + return policy; +} + +// One 32-byte chunk = 16 BF16 values. +struct alignas(16) Chunk +{ + uint64_t v[4]; +}; + +enum LoadKind : int +{ + kLoad32Hint = 0, // 32-byte load, L2 evict_first policy + kLoad32 = 1, // 32-byte load + kLoad16x2Pf = 2, // two 16-byte loads with 256-byte L2 prefetch +}; + +template +__device__ __forceinline__ void loadChunk(Chunk& c, void const* ptr, uint64_t policy) +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 1000) + // 32-byte global loads need SM100+. + __trap(); +#else + if constexpr (kKind == kLoad32Hint) + { + asm volatile("ld.global.nc.L1::no_allocate.L2::cache_hint.v4.b64 {%0, %1, %2, %3}, [%4], %5;" + : "=l"(c.v[0]), "=l"(c.v[1]), "=l"(c.v[2]), "=l"(c.v[3]) + : "l"(ptr), "l"(policy)); + } + else if constexpr (kKind == kLoad32) + { + asm volatile("ld.global.nc.L1::no_allocate.v4.b64 {%0, %1, %2, %3}, [%4];" + : "=l"(c.v[0]), "=l"(c.v[1]), "=l"(c.v[2]), "=l"(c.v[3]) + : "l"(ptr)); + } + else + { + int4 a; + int4 b; + asm volatile("ld.global.nc.L1::no_allocate.L2::256B.v4.s32 {%0, %1, %2, %3}, [%4];" + : "=r"(a.x), "=r"(a.y), "=r"(a.z), "=r"(a.w) + : "l"(ptr)); + asm volatile("ld.global.nc.L1::no_allocate.L2::256B.v4.s32 {%0, %1, %2, %3}, [%4];" + : "=r"(b.x), "=r"(b.y), "=r"(b.z), "=r"(b.w) + : "l"(static_cast(ptr) + 16)); + c.v[0] = (static_cast(static_cast(a.y)) << 32) | static_cast(a.x); + c.v[1] = (static_cast(static_cast(a.w)) << 32) | static_cast(a.z); + c.v[2] = (static_cast(static_cast(b.y)) << 32) | static_cast(b.x); + c.v[3] = (static_cast(static_cast(b.w)) << 32) | static_cast(b.z); + } +#endif +} + +// acc += dot(w, h) over 16 BF16 pairs, in a fixed order. +__device__ __forceinline__ float dotChunk(Chunk const& w, Chunk const& h, float acc) +{ +#pragma unroll + for (int i = 0; i < 4; ++i) + { + uint32_t const wl = static_cast(w.v[i]); + uint32_t const wh = static_cast(w.v[i] >> 32); + uint32_t const hl = static_cast(h.v[i]); + uint32_t const hh = static_cast(h.v[i] >> 32); + acc = fmaf(__uint_as_float(wl << 16), __uint_as_float(hl << 16), acc); + acc = fmaf(__uint_as_float(wl & 0xFFFF0000u), __uint_as_float(hl & 0xFFFF0000u), acc); + acc = fmaf(__uint_as_float(wh << 16), __uint_as_float(hh << 16), acc); + acc = fmaf(__uint_as_float(wh & 0xFFFF0000u), __uint_as_float(hh & 0xFFFF0000u), acc); + } + return acc; +} + +constexpr int kHidden6144ChunksDev = 6144 / 512; // 32-byte chunks per lane per row + +__device__ __forceinline__ uint32_t* arrivalCounter(uint64_t* scratch) +{ + return reinterpret_cast(scratch + kMaxRows); +} + +__device__ __forceinline__ uint32_t* epochCounter(uint64_t* scratch) +{ + return reinterpret_cast(scratch + kMaxRows) + 1; +} + +// --------------------------------------------------------------------------- +// Cross-rank exchange (one warp) +// +// Slot s of the exchange buffer holds kMaxRanks x kMaxRows keys; rank q writes +// only its own row block, every rank polls its local copy until all tpSize +// entries of each row are non-zero. Epoch e uses slot e % 3. Before publishing +// epoch e a rank clears slot (e + 1) % 3, whose last use was epoch e - 2: every +// key of that epoch was already observed here, and no peer can publish epoch +// e + 1 before it has observed this rank's epoch-e key, which the release fence +// orders after the clear (the peer's own fence before its next publish is the +// matching acquire). All ranks run the same sequence of epochs. +// --------------------------------------------------------------------------- +__device__ void exchangeAndWrite( + DraftTailParams const& p, uint64_t const* localKeys /* smem [rows] */, uint64_t (*gathered)[kMaxRows] /* smem */) +{ + int const lane = threadIdx.x & 31; + int const rows = p.rows; + bool const exchange = p.mode != ExchangeMode::kNone && p.tpSize > 1; + uint64_t result = 0; + if (!exchange) + { + if (lane < rows) + { + result = localKeys[lane]; + } + } + else + { + uint32_t* epochPtr = epochCounter(p.scratch); + uint32_t const epoch = *reinterpret_cast(epochPtr); + int const cur = static_cast(epoch % kNumSlots); + int const nxt = static_cast((epoch + 1) % kNumSlots); + for (int i = lane; i < kSlotWords; i += 32) + { + p.ucSlotsLocal[nxt * kSlotWords + i] = 0ull; + } + // Release: the clear above is ordered before this rank's publish below; + // a peer's next write to the cleared slot is causally after that publish. + asm volatile("fence.acq_rel.sys;" ::: "memory"); + __syncwarp(); + int const mine = cur * kSlotWords + p.tpRank * kMaxRows; + if (p.mode == ExchangeMode::kMulticast) + { + if (lane < rows) + { + mcStoreRelaxedSys(p.mcSlots + mine + lane, localKeys[lane]); + } + } + else + { + for (int i = lane; i < p.tpSize * rows; i += 32) + { + int const q = i / rows; + int const r = i - q * rows; + stRelaxedSys(p.ucSlotsPeers[q] + mine + r, localKeys[r]); + } + } + long long const start = clock64(); + for (int i = lane; i < p.tpSize * rows; i += 32) + { + int const q = i / rows; + int const r = i - q * rows; + uint64_t const* addr = p.ucSlotsLocal + cur * kSlotWords + q * kMaxRows + r; + uint64_t v = ldRelaxedSys(addr); + while (v == 0ull) + { + if (p.timeoutCycles > 0 && clock64() - start > p.timeoutCycles) + { + // A peer never published: the ranks disagree on the + // collective sequence. Record it and abort loudly. + atomicExch(reinterpret_cast(p.scratch + kMaxRows + 1), + 0x5733000000000000ull | (static_cast(epoch) << 8) + | static_cast(q)); + __threadfence_system(); + __trap(); + } + v = ldRelaxedSys(addr); + } + gathered[q][r] = v; + } + __syncwarp(); + if (lane < rows) + { + for (int q = 0; q < p.tpSize; ++q) + { + result = umax64(result, gathered[q][lane]); + } + } + if (lane == 0) + { + *epochPtr = epoch + 1; + } + } + if (lane < rows) + { + p.tokens[lane] = keyToToken(result); + if (p.keysOut != nullptr) + { + p.keysOut[lane] = result; + } + } +} + +// --------------------------------------------------------------------------- +// Fused kernel: GEMV + rank-local max + exchange +// +// CTA c owns a contiguous block of vocab rows; warp w of it takes rows +// begin + w, begin + w + kWarps, ... Each row is split into kSplit units of +// kUnit 32-byte chunks per lane, double-buffered in registers so a warp always +// has the next unit in flight while it computes the current one. The first +// unit is requested before griddepcontrol.wait, i.e. while the producer of +// the hidden rows may still be running under programmatic dependent launch. +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// Direct exchange: every CTA publishes its partial key for each row into +// entry [slot][row][rank][cta] of every rank's buffer; CTA 0 of each rank polls +// all tpSize x numCtas entries of each row, writes the tokens, then clears +// exactly the entries it polled and advances the epoch. A slot's entries are +// written again only three epochs later, i.e. after this rank's next two +// publishes, which follow this launch; so no peer can write a cleared entry +// before the clear has completed. CTAs without vocab rows publish key 1, below +// every real key. +// --------------------------------------------------------------------------- +__device__ __forceinline__ bool isDirectMode(ExchangeMode m) +{ + return m == ExchangeMode::kMulticastDirect || m == ExchangeMode::kUnicastDirect; +} + +__device__ __forceinline__ size_t directEntry(int slot, int row, int rank, int cta) +{ + return kDirectOffsetWords + static_cast(slot) * kDirectSlotWords + + (static_cast(row) * kMaxRanks + rank) * kMaxDirectCtas + cta; +} + +// All threads of the CTA call this once ctaKeys[0..rows) (shared) is final. +__device__ void directExchange(DraftTailParams const& p, uint64_t const* ctaKeys, int ctaIdx, int numCtas) +{ + __shared__ uint32_t sEpoch; + __shared__ uint64_t red[32][kMaxRows]; + int const tid = threadIdx.x; + int const lane = tid & 31; + int const warp = tid >> 5; + int const rows = p.rows; + if (tid == 0) + { + sEpoch = *reinterpret_cast(epochCounter(p.scratch)); + } + __syncthreads(); + uint32_t const epoch = sEpoch; + int const cur = static_cast(epoch % kNumSlots); + if (p.mode == ExchangeMode::kMulticastDirect) + { + if (tid < rows) + { + uint64_t const key = ctaKeys[tid] != 0ull ? ctaKeys[tid] : 1ull; + mcStoreRelaxedSys(p.mcSlots + directEntry(cur, tid, p.tpRank, ctaIdx), key); + } + } + else + { + for (int i = tid; i < p.tpSize * rows; i += blockDim.x) + { + int const q = i / rows; + int const r = i - q * rows; + uint64_t const key = ctaKeys[r] != 0ull ? ctaKeys[r] : 1ull; + stRelaxedSys(p.ucSlotsPeers[q] + directEntry(cur, r, p.tpRank, ctaIdx), key); + } + } + if (ctaIdx != 0) + { + return; + } + long long const start = clock64(); + int const perRow = p.tpSize * numCtas; + // Each thread issues up to kPoll independent loads per round and re-polls + // only the entries still empty, so a row costs about one memory round + // trip after the last entry lands, however many CTAs published. + constexpr int kPoll = 32; + for (int r = 0; r < rows; ++r) + { + uint64_t m = 0ull; + for (int base = tid; base < perRow; base += kPoll * blockDim.x) + { + uint64_t v[kPoll]; +#pragma unroll + for (int b = 0; b < kPoll; ++b) + { + int const i = base + b * blockDim.x; + v[b] = i < perRow ? ldRelaxedSys(p.ucSlotsLocal + directEntry(cur, r, i / numCtas, i % numCtas)) : 1ull; + } + bool done = false; + while (!done) + { + done = true; +#pragma unroll + for (int b = 0; b < kPoll; ++b) + { + if (v[b] == 0ull) + { + int const i = base + b * blockDim.x; + done = false; + v[b] = ldRelaxedSys(p.ucSlotsLocal + directEntry(cur, r, i / numCtas, i % numCtas)); + } + } + if (!done && p.timeoutCycles > 0 && clock64() - start > p.timeoutCycles) + { + atomicExch(reinterpret_cast(p.scratch + kMaxRows + 1), + 0x5734000000000000ull | (static_cast(epoch) << 16) + | static_cast(base)); + __threadfence_system(); + __trap(); + } + } +#pragma unroll + for (int b = 0; b < kPoll; ++b) + { + m = umax64(m, v[b]); + } + } + m = warpMax64(m); + if (lane == 0) + { + red[warp][r] = m; + } + } + __syncthreads(); + if (tid < rows) + { + uint64_t result = 0ull; + int const numWarps = (blockDim.x + 31) >> 5; + for (int w = 0; w < numWarps; ++w) + { + result = umax64(result, red[w][tid]); + } + p.tokens[tid] = keyToToken(result); + if (p.keysOut != nullptr) + { + p.keysOut[tid] = result; + } + } + for (int i = tid; i < rows * perRow; i += blockDim.x) + { + int const r = i / perRow; + int const j = i - r * perRow; + int const q = j / numCtas; + int const c = j - q * numCtas; + p.ucSlotsLocal[directEntry(cur, r, q, c)] = 0ull; + } + if (tid == 0) + { + *epochCounter(p.scratch) = epoch + 1; + } +} + +// Per-CTA epilogue of the fused kernels: CTA max of the warps' keys, one +// atomicMax per row into rank-local scratch, then the last CTA to arrive takes +// (and resets) the rank-local keys and runs the exchange. +template +__device__ __forceinline__ void ctaFinish(DraftTailParams const& p, uint64_t const (&best)[R]) +{ + __shared__ uint64_t warpBest[kWarps][R]; + __shared__ uint64_t ctaKeys[kMaxRows]; + __shared__ uint64_t gathered[kMaxRanks][kMaxRows]; + __shared__ int isLast; + int const lane = threadIdx.x & 31; + int const warp = threadIdx.x >> 5; + if (lane == 0) + { +#pragma unroll + for (int r = 0; r < R; ++r) + { + warpBest[warp][r] = best[r]; + } + } + __syncthreads(); + if (isDirectMode(p.mode)) + { + if (threadIdx.x < R) + { + uint64_t m = 0ull; +#pragma unroll + for (int w = 0; w < kWarps; ++w) + { + m = umax64(m, warpBest[w][threadIdx.x]); + } + ctaKeys[threadIdx.x] = m; + } + __syncthreads(); + directExchange(p, ctaKeys, blockIdx.x, gridDim.x); + return; + } + if (threadIdx.x < R) + { + uint64_t m = 0ull; +#pragma unroll + for (int w = 0; w < kWarps; ++w) + { + m = umax64(m, warpBest[w][threadIdx.x]); + } + if (m != 0ull) + { + atomicMax(reinterpret_cast(p.scratch + threadIdx.x), m); + } + } + __syncthreads(); + if (threadIdx.x == 0) + { + __threadfence(); + uint32_t const prev = atomicAdd(arrivalCounter(p.scratch), 1u); + isLast = prev == gridDim.x - 1; + } + __syncthreads(); + if (!isLast || warp != 0) + { + return; + } + __threadfence(); + if (lane < R) + { + ctaKeys[lane] = atomicExch(reinterpret_cast(p.scratch + lane), 0ull); + } + if (lane == 0) + { + atomicExch(arrivalCounter(p.scratch), 0u); + } + __syncwarp(); + exchangeAndWrite(p, ctaKeys, gathered); +} + +template +__device__ __forceinline__ void loadUnit(Chunk (&buf)[kUnit], char const* base, uint64_t policy) +{ +#pragma unroll + for (int j = 0; j < kUnit; ++j) + { + loadChunk(buf[j], base + j * 32 * 32, policy); + } +} + +// Byte address of unit u of this lane: row firstRow + (u / kSplit) * kWarps, +// part u % kSplit; chunk c of a row lives at bytes [32 c, 32 c + 32) and lane l +// owns chunks part * kUnit * 32 + j * 32 + l. +template +__device__ __forceinline__ char const* unitAddress(__nv_bfloat16 const* weight, int firstRow, int u, int lane) +{ + constexpr int kUnit = kChunks / kSplit; + int const rowIdx = u / kSplit; + int const part = u - rowIdx * kSplit; + int64_t const row = firstRow + static_cast(rowIdx) * kWarps; + return reinterpret_cast(weight + row * (kChunks * 32 * 16)) + (part * kUnit * 32 + lane) * 32; +} + +template +__device__ __forceinline__ void computeUnit(Chunk const (&buf)[kChunks / kSplit], Chunk const* smemH, int u, + int firstRow, int lane, int64_t vocabOffset, float (&acc)[R], uint64_t (&best)[R]) +{ + constexpr int kUnit = kChunks / kSplit; + int const rowIdx = u / kSplit; + int const part = u - rowIdx * kSplit; +#pragma unroll + for (int j = 0; j < kUnit; ++j) + { + int const c = (part * kUnit + j) * 32 + lane; +#pragma unroll + for (int r = 0; r < R; ++r) + { + acc[r] = dotChunk(buf[j], smemH[r * kChunks * 32 + c], acc[r]); + } + } + if (part == kSplit - 1) + { + int64_t const row = firstRow + static_cast(rowIdx) * kWarps; +#pragma unroll + for (int r = 0; r < R; ++r) + { + float s = acc[r]; +#pragma unroll + for (int off = 16; off > 0; off >>= 1) + { + s += __shfl_xor_sync(0xFFFFFFFFu, s, off); + } + uint32_t const bits = static_cast(__bfloat16_as_ushort(__float2bfloat16_rn(s))); + best[r] = umax64(best[r], makeKey(bits, vocabOffset + row)); + acc[r] = 0.f; + } + } +} + +template +__global__ void __launch_bounds__(kWarps * 32, kMinBlocks) fusedDraftTailKernel(DraftTailParams p) +{ + constexpr int kUnit = kChunks / kSplit; + static_assert(kChunks % kSplit == 0, "row must split evenly"); + + extern __shared__ uint4 smemDyn[]; + Chunk* smemH = reinterpret_cast(smemDyn); // [R][kChunks * 32] + + int const lane = threadIdx.x & 31; + int const warp = threadIdx.x >> 5; + int const numRows = p.vocabLocal; + int const rowsPerCta = (numRows + gridDim.x - 1) / gridDim.x; + int const rowBegin = blockIdx.x * rowsPerCta; + int const rowEnd = min(numRows, rowBegin + rowsPerCta); + int const firstRow = rowBegin + warp; + int const myRows = firstRow < rowEnd ? (rowEnd - firstRow + kWarps - 1) / kWarps : 0; + int const numUnits = myRows * kSplit; + + uint64_t const policyFirst = kLoad == kLoad32Hint ? evictFirstPolicy() : 0ull; + uint64_t const policyLast = kLoad == kLoad32Hint ? evictLastPolicy() : 0ull; + int const keepRows = static_cast((static_cast(myRows) * p.keepPermille) / 1000); + __nv_bfloat16 const* weight = p.weight; + + Chunk bufA[kUnit]; + Chunk bufB[kUnit]; + if (numUnits > 0) + { + loadUnit(bufA, unitAddress(weight, firstRow, 0, lane), + keepRows > 0 ? policyLast : policyFirst); + } + + griddepWait(); + + { + int const total = R * kChunks * 32; + for (int i = threadIdx.x; i < total; i += blockDim.x) + { + int const r = i / (kChunks * 32); + int const c = i - r * (kChunks * 32); + uint4 const* src = reinterpret_cast(p.hidden + r * p.hiddenStride) + 2 * c; + uint4 const lo = src[0]; + uint4 const hi = src[1]; + Chunk ch; + ch.v[0] = (static_cast(lo.y) << 32) | lo.x; + ch.v[1] = (static_cast(lo.w) << 32) | lo.z; + ch.v[2] = (static_cast(hi.y) << 32) | hi.x; + ch.v[3] = (static_cast(hi.w) << 32) | hi.z; + smemH[i] = ch; + } + } + __syncthreads(); + + float acc[R]; + uint64_t best[R]; +#pragma unroll + for (int r = 0; r < R; ++r) + { + acc[r] = 0.f; + best[r] = 0ull; + } + + for (int u = 0; u < numUnits; u += 2) + { + if (u + 1 < numUnits) + { + loadUnit(bufB, unitAddress(weight, firstRow, u + 1, lane), + (u + 1) / kSplit < keepRows ? policyLast : policyFirst); + } + computeUnit(bufA, smemH, u, firstRow, lane, p.vocabOffset, acc, best); + if (u + 1 < numUnits) + { + if (u + 2 < numUnits) + { + loadUnit(bufA, unitAddress(weight, firstRow, u + 2, lane), + (u + 2) / kSplit < keepRows ? policyLast : policyFirst); + } + computeUnit(bufB, smemH, u + 1, firstRow, lane, p.vocabOffset, acc, best); + } + } + + ctaFinish(p, best); +} + +// --------------------------------------------------------------------------- +// Fused kernel, bulk-copy flavour: each warp streams its rows through its own +// ring of kSlots shared-memory row buffers filled by cp.async.bulk (one 12 KB +// copy per row, completion on a per-slot mbarrier), so the bytes in flight do +// not depend on registers. Lane l reads chunks l + 32 j of a row in the same +// order as the register kernel, so both produce bitwise-identical keys. +// --------------------------------------------------------------------------- +__device__ __forceinline__ uint32_t smemAddr(void const* ptr) +{ + return static_cast(__cvta_generic_to_shared(ptr)); +} + +__device__ __forceinline__ void mbarInit(uint64_t* bar, uint32_t count) +{ + asm volatile("mbarrier.init.shared::cta.b64 [%0], %1;" ::"r"(smemAddr(bar)), "r"(count) : "memory"); +} + +__device__ __forceinline__ void mbarExpectTx(uint64_t* bar, uint32_t bytes) +{ + asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;" ::"r"(smemAddr(bar)), "r"(bytes) : "memory"); +} + +__device__ __forceinline__ void mbarWait(uint64_t* bar, uint32_t phase) +{ + uint32_t const addr = smemAddr(bar); + asm volatile( + "{\n" + ".reg .pred done;\n" + "DRAFT_TAIL_WAIT_%=:\n" + "mbarrier.try_wait.parity.shared::cta.b64 done, [%0], %1;\n" + "@!done bra DRAFT_TAIL_WAIT_%=;\n" + "}\n" ::"r"(addr), + "r"(phase) + : "memory"); +} + +__device__ __forceinline__ void bulkLoad(void* dst, void const* src, uint32_t bytes, uint64_t* bar, uint64_t policy) +{ + asm volatile( + "cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes.L2::cache_hint [%0], [%1], %2, [%3], %4;" :: + "r"(smemAddr(dst)), + "l"(src), "r"(bytes), "r"(smemAddr(bar)), "l"(policy) + : "memory"); +} + +template +__global__ void __launch_bounds__(kWarps * 32, 1) fusedBulkDraftTailKernel(DraftTailParams p) +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ < 900) + // Bulk copies and transaction mbarriers need SM90+. + __trap(); +#else + constexpr int kChunks = kHidden6144ChunksDev; + constexpr int kRowBytes = kChunks * 32 * 32; + extern __shared__ uint4 smemDyn[]; + Chunk* smemH = reinterpret_cast(smemDyn); // [R][kChunks * 32] + Chunk* ring = smemH + R * kChunks * 32; // [kWarps][kSlots][kChunks * 32] + __shared__ __align__(8) uint64_t bars[kWarps][kSlots]; + + int const lane = threadIdx.x & 31; + int const warp = threadIdx.x >> 5; + int const numRows = p.vocabLocal; + int const rowsPerCta = (numRows + gridDim.x - 1) / gridDim.x; + int const rowBegin = blockIdx.x * rowsPerCta; + int const rowEnd = min(numRows, rowBegin + rowsPerCta); + int const firstRow = rowBegin + warp; + int const myRows = firstRow < rowEnd ? (rowEnd - firstRow + kWarps - 1) / kWarps : 0; + int const keepRows = static_cast((static_cast(myRows) * p.keepPermille) / 1000); + Chunk* myRing = ring + warp * kSlots * kChunks * 32; + + uint64_t const policyFirst = evictFirstPolicy(); + uint64_t const policyLast = evictLastPolicy(); + auto rowPtr = [&](int i) -> void const* + { return p.weight + (static_cast(firstRow) + static_cast(i) * kWarps) * (kChunks * 32 * 16); }; + + if (lane == 0) + { +#pragma unroll + for (int s = 0; s < kSlots; ++s) + { + mbarInit(&bars[warp][s], 1); + } + asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory"); +#pragma unroll + for (int s = 0; s < kSlots; ++s) + { + if (s < myRows) + { + mbarExpectTx(&bars[warp][s], kRowBytes); + bulkLoad(myRing + s * kChunks * 32, rowPtr(s), kRowBytes, &bars[warp][s], + s < keepRows ? policyLast : policyFirst); + } + } + } + + griddepWait(); + { + int const total = R * kChunks * 32; + for (int i = threadIdx.x; i < total; i += blockDim.x) + { + int const r = i / (kChunks * 32); + int const c = i - r * (kChunks * 32); + uint4 const* src = reinterpret_cast(p.hidden + r * p.hiddenStride) + 2 * c; + uint4 const lo = src[0]; + uint4 const hi = src[1]; + Chunk ch; + ch.v[0] = (static_cast(lo.y) << 32) | lo.x; + ch.v[1] = (static_cast(lo.w) << 32) | lo.z; + ch.v[2] = (static_cast(hi.y) << 32) | hi.x; + ch.v[3] = (static_cast(hi.w) << 32) | hi.z; + smemH[i] = ch; + } + } + __syncthreads(); + + uint64_t best[R]; +#pragma unroll + for (int r = 0; r < R; ++r) + { + best[r] = 0ull; + } + for (int i = 0; i < myRows; ++i) + { + int const slot = i % kSlots; + mbarWait(&bars[warp][slot], static_cast((i / kSlots) & 1)); + Chunk const* row = myRing + slot * kChunks * 32; + float acc[R]; +#pragma unroll + for (int r = 0; r < R; ++r) + { + acc[r] = 0.f; + } +#pragma unroll + for (int j = 0; j < kChunks; ++j) + { + int const c = j * 32 + lane; + Chunk const w = row[c]; +#pragma unroll + for (int r = 0; r < R; ++r) + { + acc[r] = dotChunk(w, smemH[r * kChunks * 32 + c], acc[r]); + } + } + __syncwarp(); + if (lane == 0 && i + kSlots < myRows) + { + mbarExpectTx(&bars[warp][slot], kRowBytes); + bulkLoad(myRing + slot * kChunks * 32, rowPtr(i + kSlots), kRowBytes, &bars[warp][slot], + i + kSlots < keepRows ? policyLast : policyFirst); + } + int64_t const rowId = firstRow + static_cast(i) * kWarps; +#pragma unroll + for (int r = 0; r < R; ++r) + { + float sum = acc[r]; +#pragma unroll + for (int off = 16; off > 0; off >>= 1) + { + sum += __shfl_xor_sync(0xFFFFFFFFu, sum, off); + } + uint32_t const bits = static_cast(__bfloat16_as_ushort(__float2bfloat16_rn(sum))); + best[r] = umax64(best[r], makeKey(bits, p.vocabOffset + rowId)); + } + } + ctaFinish(p, best); +#endif +} + +// --------------------------------------------------------------------------- +// Fused kernel, tensor-core flavour: each warp owns 16-row tiles of the vocab +// shard and computes them with mma.sync m16n8k16 (BF16 in, FP32 accumulate), +// the R <= 8 hidden rows being the n columns. Lane (g = lane / 4, t = lane % 4) +// loads 32 contiguous bytes (16 values) of rows g and g + 8 per 64-value K +// block; inside a block the K order is permuted consistently for A and B +// (values 16t + 4s .. 16t + 4s + 3 feed k-step s), so a lane's two 16-byte +// halves map directly onto the A/B fragments without shuffles. This replaces +// the CUDA-core unpack + FMA loop (instruction bound) with 4 HMMA per 2 KB +// of weight per warp. +// +// Native-order flavours (MmaOrder below): k-step q of a 64-value block takes +// the 16 consecutive values 16q .. 16q + 15, so every MMA sums the same K +// values, in the same k-step order, as a GEMM that walks K in 16-value steps. +// They differ only in how the values reach the lanes. +// --------------------------------------------------------------------------- +__device__ __forceinline__ void mmaBf16( + float (&d)[4], uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t b0, uint32_t b1) +{ + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 {%0, %1, %2, %3}, {%4, %5, %6, %7}, {%8, %9}, " + "{%0, %1, %2, %3};\n" + : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b0), "r"(b1)); +} + +constexpr int kMmaBlockElems = 64; // K values per lane-group block (4 lanes x 16) +constexpr int kMmaBlocks = 6144 / kMmaBlockElems; // 96 +constexpr int kMmaHStride = 6144 + 16; // padded smem row (elements) against bank conflicts + +// Which K values lane t (= lane % 4) of a quad feeds into the 4 mma k-steps +// q = 0..3 of each 64-value block, and how they are loaded. Every lane feeds +// 4 values (one 8-byte piece) per k-step and row. +enum MmaOrder : int +{ + // Lane t: values 16 t .. 16 t + 15 (one 32-byte load), piece q into k-step + // q; each k-step sums a K subset spread over the block (not native order). + kOrderPermuted = 0, + // Native orders: k-step q sums exactly values 16 q .. 16 q + 15. + // Lane t loads piece t of every k-step: four 8-byte loads. + kOrderNat8 = 1, + // Lane t loads k-step t whole (one 32-byte load); three in-quad shuffles + // hand piece (t - q) mod 4 of k-step q to lane t. + kOrderRot32 = 2, + // Lane t loads half (t >> 1) of k-steps 2 (t & 1) and 2 (t & 1) + 1 (two + // 16-byte loads), keeps the first piece of each half and swaps the second + // with lane t ^ 1. + kOrderPair16 = 3, +}; + +// Byte offset of lane t's first load inside a 128-byte block row. +template +__device__ __forceinline__ int mmaLaneOffset(int t) +{ + if constexpr (kOrder == kOrderNat8) + { + return 8 * t; + } + else if constexpr (kOrder == kOrderPair16) + { + return 64 * (t & 1) + 16 * (t >> 1); + } + else + { + return 32 * t; + } +} + +// Rot32 / Pair16: which 8-byte piece (0..3) of k-step q lane t feeds. +template +__device__ __forceinline__ int mmaNativePiece(int t, int q) +{ + if constexpr (kOrder == kOrderRot32) + { + return (t - q) & 3; + } + else + { + return q < 2 ? t : (t ^ 1); + } +} + +template +__device__ __forceinline__ void load16(uint64_t& a, uint64_t& b, void const* ptr, uint64_t policy) +{ + if constexpr (kLoad == kLoad32Hint) + { + asm volatile("ld.global.nc.L1::no_allocate.L2::cache_hint.v2.b64 {%0, %1}, [%2], %3;" + : "=l"(a), "=l"(b) + : "l"(ptr), "l"(policy)); + } + else + { + asm volatile("ld.global.nc.L1::no_allocate.v2.b64 {%0, %1}, [%2];" : "=l"(a), "=l"(b) : "l"(ptr)); + } +} + +template +__device__ __forceinline__ uint64_t load8(void const* ptr, uint64_t policy) +{ + uint64_t v; + if constexpr (kLoad == kLoad32Hint) + { + asm volatile("ld.global.nc.L1::no_allocate.L2::cache_hint.b64 %0, [%1], %2;" : "=l"(v) : "l"(ptr), "l"(policy)); + } + else + { + asm volatile("ld.global.nc.L1::no_allocate.b64 %0, [%1];" : "=l"(v) : "l"(ptr)); + } + return v; +} + +// row0 / row1 already include the lane offset (mmaLaneOffset). +template +__device__ __forceinline__ void mmaLoadStage( + Chunk (&x)[kStage], Chunk (&y)[kStage], char const* row0, char const* row1, int block, uint64_t policy) +{ +#pragma unroll + for (int j = 0; j < kStage; ++j) + { + int const off = (block + j) * kMmaBlockElems * 2; + if constexpr (kOrder == kOrderNat8) + { +#pragma unroll + for (int q = 0; q < 4; ++q) + { + x[j].v[q] = load8(row0 + off + 32 * q, policy); + y[j].v[q] = load8(row1 + off + 32 * q, policy); + } + } + else if constexpr (kOrder == kOrderPair16) + { + // v[0], v[1]: k-step 2 (t & 1); v[2], v[3]: k-step 2 (t & 1) + 1. + load16(x[j].v[0], x[j].v[1], row0 + off, policy); + load16(x[j].v[2], x[j].v[3], row0 + off + 32, policy); + load16(y[j].v[0], y[j].v[1], row1 + off, policy); + load16(y[j].v[2], y[j].v[3], row1 + off + 32, policy); + } + else + { + loadChunk(x[j], row0 + off, policy); + loadChunk(y[j], row1 + off, policy); + } + } +} + +// a[q]: the weight piece lane t feeds into k-step q, from what it loaded. +// Every lane of the warp must call this (in-quad shuffles). +template +__device__ __forceinline__ void mmaWeightPieces(uint64_t (&a)[4], Chunk const& c, int t) +{ + if constexpr (kOrder == kOrderRot32) + { + // c = k-step t. Round r takes piece r of k-step (t - r) mod 4 from the + // lane that loaded it, so k-step q arrives in round (t - q) mod 4. + uint64_t s[4]; + s[0] = c.v[0]; + int const quadBase = (threadIdx.x & 31) & ~3; +#pragma unroll + for (int r = 1; r < 4; ++r) + { + s[r] = __shfl_sync(0xFFFFFFFFu, c.v[r], quadBase | ((t - r) & 3)); + } +#pragma unroll + for (int q = 0; q < 4; ++q) + { + int const idx = (t - q) & 3; + uint64_t const lo = (idx & 1) ? s[1] : s[0]; + uint64_t const hi = (idx & 1) ? s[3] : s[2]; + a[q] = (idx & 2) ? hi : lo; + } + } + else if constexpr (kOrder == kOrderPair16) + { + // Even lanes hold k-steps 0, 1 and odd lanes k-steps 2, 3 (pieces + // 2 (t >> 1) and 2 (t >> 1) + 1 of each); swap the second pieces. + bool const odd = (t & 1) != 0; + uint64_t const r0 = __shfl_xor_sync(0xFFFFFFFFu, c.v[1], 1); + uint64_t const r1 = __shfl_xor_sync(0xFFFFFFFFu, c.v[3], 1); + a[0] = odd ? r0 : c.v[0]; + a[1] = odd ? r1 : c.v[2]; + a[2] = odd ? c.v[0] : r0; + a[3] = odd ? c.v[2] : r1; + } + else + { +#pragma unroll + for (int q = 0; q < 4; ++q) + { + a[q] = c.v[q]; + } + } +} + +template +__device__ __forceinline__ void mmaComputeStage(float (&d)[4], Chunk const (&x)[kStage], Chunk const (&y)[kStage], + __nv_bfloat16 const* hRow, bool hLive, int block, int t) +{ +#pragma unroll + for (int j = 0; j < kStage; ++j) + { + uint4 h0 = make_uint4(0, 0, 0, 0); + uint4 h1 = make_uint4(0, 0, 0, 0); + if (hLive) + { + if constexpr (kOrder == kOrderNat8) + { + // Values 16 q + 4 t .. + 3 for q = 0..3: 8 bytes every 32. + uint2 const* src = reinterpret_cast(hRow + (block + j) * kMmaBlockElems + 4 * t); + uint2 const q0 = src[0]; + uint2 const q1 = src[4]; + uint2 const q2 = src[8]; + uint2 const q3 = src[12]; + h0 = make_uint4(q0.x, q0.y, q1.x, q1.y); + h1 = make_uint4(q2.x, q2.y, q3.x, q3.y); + } + else + { + // Permuted: values 16 t .. 16 t + 15 of the block. Rot32 / + // Pair16: the row is staged in lane order, so the same 32 + // bytes are lane t's pieces for k-steps 0..3. + uint4 const* src = reinterpret_cast(hRow + (block + j) * kMmaBlockElems + 16 * t); + h0 = src[0]; + h1 = src[1]; + } + } + uint32_t const hb[8] = {h0.x, h0.y, h0.z, h0.w, h1.x, h1.y, h1.z, h1.w}; + uint64_t xp[4]; + uint64_t yp[4]; + mmaWeightPieces(xp, x[j], t); + mmaWeightPieces(yp, y[j], t); +#pragma unroll + for (int sIdx = 0; sIdx < 4; ++sIdx) + { + // Piece s holds the pairs mapped to k positions (2t, 2t + 1) and + // (2t + 8, 2t + 9) of k-step s, for A and B alike. + uint64_t const xw = xp[sIdx]; + uint64_t const yw = yp[sIdx]; + mmaBf16(d, static_cast(xw), static_cast(yw), static_cast(xw >> 32), + static_cast(yw >> 32), hb[2 * sIdx], hb[2 * sIdx + 1]); + } + } +} + +template +__global__ void __launch_bounds__(kWarps * 32, kMinBlocks) fusedMmaDraftTailKernel(DraftTailParams p) +{ + static_assert(R >= 1 && R <= 8, "n columns of m16n8k16"); + static_assert(kMmaBlocks % (2 * kStage) == 0, "double-buffered stages must tile K"); + extern __shared__ uint4 smemDyn[]; + __nv_bfloat16* smemH = reinterpret_cast<__nv_bfloat16*>(smemDyn); // [R][kMmaHStride] + + int const lane = threadIdx.x & 31; + int const warp = threadIdx.x >> 5; + int const g = lane >> 2; + int const t = lane & 3; + int const numRows = p.vocabLocal; + int const numTiles = (numRows + 15) / 16; + int const warpStride = gridDim.x * kWarps; + int tile = blockIdx.x * kWarps + warp; + + uint64_t const policy = kLoad == kLoad32Hint ? evictFirstPolicy() : 0ull; + char const* weight = reinterpret_cast(p.weight); + // Rows beyond the shard read row 0 (valid memory); their results are dropped. + auto rowPtr = [&](int row) -> char const* + { return weight + static_cast(row < numRows ? row : 0) * (6144 * 2) + mmaLaneOffset(t); }; + + Chunk xa[kStage], ya[kStage], xb[kStage], yb[kStage]; + if (tile < numTiles) + { + mmaLoadStage(xa, ya, rowPtr(tile * 16 + g), rowPtr(tile * 16 + g + 8), 0, policy); + } + + griddepWait(); + if constexpr (kOrder == kOrderRot32 || kOrder == kOrderPair16) + { + // Lane order: in each 64-value block, lane t's pieces for k-steps + // 0..3 are contiguous at values 16 t .. 16 t + 15. + for (int i = threadIdx.x; i < R * kMmaBlocks * 16; i += blockDim.x) + { + int const r = i / (kMmaBlocks * 16); + int const rem = i - r * (kMmaBlocks * 16); + int const block = rem >> 4; + int const lt = (rem >> 2) & 3; + int const q = rem & 3; + reinterpret_cast(smemH + r * kMmaHStride)[rem] = reinterpret_cast( + p.hidden + r * p.hiddenStride)[block * 16 + q * 4 + mmaNativePiece(lt, q)]; + } + } + else + { + for (int i = threadIdx.x; i < R * 6144 / 8; i += blockDim.x) + { + int const r = i / (6144 / 8); + int const c = i - r * (6144 / 8); + reinterpret_cast(smemH + r * kMmaHStride)[c] + = reinterpret_cast(p.hidden + r * p.hiddenStride)[c]; + } + } + __syncthreads(); + + bool const hLive = g < R; + __nv_bfloat16 const* hRow = smemH + (hLive ? g : 0) * kMmaHStride; + uint64_t bestJ[2] = {0ull, 0ull}; + + for (; tile < numTiles; tile += warpStride) + { + int const row0 = tile * 16 + g; + int const row1 = row0 + 8; + char const* p0 = rowPtr(row0); + char const* p1 = rowPtr(row1); + float d[4] = {0.f, 0.f, 0.f, 0.f}; + for (int b = 0; b < kMmaBlocks; b += 2 * kStage) + { + mmaLoadStage(xb, yb, p0, p1, b + kStage, policy); + mmaComputeStage(d, xa, ya, hRow, hLive, b, t); + if (b + 2 * kStage < kMmaBlocks) + { + mmaLoadStage(xa, ya, p0, p1, b + 2 * kStage, policy); + } + else if (tile + warpStride < numTiles) + { + int const next = tile + warpStride; + mmaLoadStage( + xa, ya, rowPtr(next * 16 + g), rowPtr(next * 16 + g + 8), 0, policy); + } + mmaComputeStage(d, xb, yb, hRow, hLive, b + kStage, t); + } + // d[j] = (row0, column 2t + j), d[2 + j] = (row1, column 2t + j). +#pragma unroll + for (int j = 0; j < 2; ++j) + { + int const n = 2 * t + j; + if (n < R) + { + if (row0 < numRows) + { + __nv_bfloat16 const v = __float2bfloat16_rn(d[j]); + bestJ[j] = umax64(bestJ[j], makeKey(__bfloat16_as_ushort(v), p.vocabOffset + row0)); + if (p.logitsOut != nullptr) + { + p.logitsOut[n * p.logitsOutStride + row0] = v; + } + } + if (row1 < numRows) + { + __nv_bfloat16 const v = __float2bfloat16_rn(d[2 + j]); + bestJ[j] = umax64(bestJ[j], makeKey(__bfloat16_as_ushort(v), p.vocabOffset + row1)); + if (p.logitsOut != nullptr) + { + p.logitsOut[n * p.logitsOutStride + row1] = v; + } + } + } + } + } + // Reduce over g (lane bits 2..4), then lane 0 collects column n from lane n / 2. +#pragma unroll + for (int j = 0; j < 2; ++j) + { +#pragma unroll + for (int off = 4; off < 32; off <<= 1) + { + bestJ[j] = umax64(bestJ[j], __shfl_xor_sync(0xFFFFFFFFu, bestJ[j], off)); + } + } + uint64_t best[R]; +#pragma unroll + for (int n = 0; n < R; ++n) + { + best[n] = __shfl_sync(0xFFFFFFFFu, bestJ[n & 1], n >> 1); + } + ctaFinish(p, best); +} + +// --------------------------------------------------------------------------- +// Logits kernel: rank-local max over precomputed BF16 logits + exchange. +// --------------------------------------------------------------------------- +template +__global__ void __launch_bounds__(512, 1) fromLogitsDraftTailKernel(DraftTailParams p) +{ + __shared__ uint64_t warpBest[32][R]; + __shared__ uint64_t ctaKeys[kMaxRows]; + __shared__ uint64_t gathered[kMaxRanks][kMaxRows]; + + griddepWait(); + int const lane = threadIdx.x & 31; + int const warp = threadIdx.x >> 5; + int const numWarps = blockDim.x >> 5; + uint64_t best[R]; +#pragma unroll + for (int r = 0; r < R; ++r) + { + best[r] = 0ull; + } + // Flat (row, 8-element vector) index space; each thread issues kBatch + // independent 16-byte loads before reducing them, so a row costs about one + // memory round trip instead of one per loop iteration. + constexpr int kBatch = 8; + bool vec = (p.logitsStride % 8) == 0 && (reinterpret_cast(p.logits) & 15) == 0; + int const nVec = vec ? p.vocabLocal / 8 : 0; + int const total = R * nVec; + for (int base = threadIdx.x; base < total; base += kBatch * blockDim.x) + { + uint4 x[kBatch]; +#pragma unroll + for (int b = 0; b < kBatch; ++b) + { + int const i = base + b * blockDim.x; + if (i < total) + { + int const r = i / nVec; + int const v = i - r * nVec; + x[b] = __ldg(reinterpret_cast(p.logits + r * p.logitsStride) + v); + } + } +#pragma unroll + for (int b = 0; b < kBatch; ++b) + { + int const i = base + b * blockDim.x; + if (i < total) + { + int const r = i / nVec; + int const v = i - r * nVec; + uint32_t const w[4] = {x[b].x, x[b].y, x[b].z, x[b].w}; + uint64_t m = 0ull; +#pragma unroll + for (int k = 0; k < 4; ++k) + { + int64_t const id = p.vocabOffset + 8 * v + 2 * k; + m = umax64(m, makeKey(w[k] & 0xFFFFu, id)); + m = umax64(m, makeKey(w[k] >> 16, id + 1)); + } +#pragma unroll + for (int rr = 0; rr < R; ++rr) + { + if (rr == r) + { + best[rr] = umax64(best[rr], m); + } + } + } + } + } +#pragma unroll + for (int r = 0; r < R; ++r) + { + __nv_bfloat16 const* row = p.logits + r * p.logitsStride; + for (int i = nVec * 8 + threadIdx.x; i < p.vocabLocal; i += blockDim.x) + { + uint32_t const bits = __bfloat16_as_ushort(row[i]); + best[r] = umax64(best[r], makeKey(bits, p.vocabOffset + i)); + } + best[r] = warpMax64(best[r]); + } + if (lane == 0) + { +#pragma unroll + for (int r = 0; r < R; ++r) + { + warpBest[warp][r] = best[r]; + } + } + __syncthreads(); + if (warp == 0) + { +#pragma unroll + for (int r = 0; r < R; ++r) + { + uint64_t m = lane < numWarps ? warpBest[lane][r] : 0ull; + m = warpMax64(m); + if (lane == 0) + { + ctaKeys[r] = m; + } + } + } + __syncthreads(); + if (isDirectMode(p.mode)) + { + directExchange(p, ctaKeys, 0, 1); + return; + } + if (warp != 0) + { + return; + } + exchangeAndWrite(p, ctaKeys, gathered); +} + +// --------------------------------------------------------------------------- +// Launch +// --------------------------------------------------------------------------- + +constexpr int kHidden6144Chunks = 6144 / 512; // 32-byte chunks per lane per row + +struct VariantDesc +{ + int warps; + int split; + int load; + int minBlocks; +}; + +// Kernel shape and load flavour, selectable at run time for tuning. +constexpr VariantDesc kVariants[] = { + {8, 2, kLoad32Hint, 1}, // 0: default + {8, 2, kLoad32, 1}, // 1: no L2 eviction hint + {8, 2, kLoad16x2Pf, 1}, // 2: 16-byte loads with L2 prefetch + {16, 3, kLoad32Hint, 1}, // 3: more warps, shorter units + {4, 2, kLoad32Hint, 2}, // 4: two CTAs per SM + {8, 1, kLoad32Hint, 1}, // 5: whole-row units + {8, 0, kLoad32Hint, 1}, // 6: per-warp cp.async.bulk row ring (R <= 2; else variant 0) + {8, 0, kLoad32Hint, 1}, // 7: tensor-core tiles, 8 warps, 2-block stages + {4, 0, kLoad32Hint, 2}, // 8: tensor-core tiles, 4 warps, 2 CTAs/SM + {8, 0, kLoad32, 1}, // 9: tensor-core tiles, no L2 hint + {8, 0, kLoad32Hint, 1}, // 10: tensor-core tiles, 8 warps, 4-block stages + {8, 0, kLoad32Hint, 1}, // 11: as 10, native K order (kOrderNat8: 8-byte lane loads) + {4, 0, kLoad32Hint, 2}, // 12: native K order (kOrderNat8), 4 warps, 2 CTAs/SM + {8, 0, kLoad32Hint, 1}, // 13: as 10, native K order (kOrderRot32: 32-byte loads, quad shuffles) + {8, 0, kLoad32Hint, 1}, // 14: as 10, native K order (kOrderPair16: 16-byte loads, pair shuffles) +}; +constexpr int kNumVariants = sizeof(kVariants) / sizeof(kVariants[0]); +// Variants from here on are tensor-core kernels; their default grid covers +// one 16-row tile per warp. +constexpr int kFirstMmaVariant = 7; + +template +void launchFusedShape(DraftTailParams const& p, int numCtas, bool pdl, cudaStream_t stream) +{ + auto kernel = fusedDraftTailKernel; + size_t const smem = static_cast(R) * 6144 * sizeof(__nv_bfloat16); + static bool const attrSet = [&]() + { + TLLM_CUDA_CHECK( + cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(smem))); + return true; + }(); + (void) attrSet; + cudaLaunchConfig_t config{}; + config.gridDim = dim3(numCtas); + config.blockDim = dim3(kWarps * 32); + config.dynamicSmemBytes = smem; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = pdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 1; + TLLM_CUDA_CHECK(cudaLaunchKernelEx(&config, kernel, p)); +} + +template +void launchBulkShape(DraftTailParams const& p, int numCtas, bool pdl, cudaStream_t stream) +{ + auto kernel = fusedBulkDraftTailKernel; + size_t const smem = static_cast(R + kWarps * kSlots) * 6144 * sizeof(__nv_bfloat16); + static bool const attrSet = [&]() + { + TLLM_CUDA_CHECK( + cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(smem))); + return true; + }(); + (void) attrSet; + cudaLaunchConfig_t config{}; + config.gridDim = dim3(numCtas); + config.blockDim = dim3(kWarps * 32); + config.dynamicSmemBytes = smem; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = pdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 1; + TLLM_CUDA_CHECK(cudaLaunchKernelEx(&config, kernel, p)); +} + +template +void launchMmaShape(DraftTailParams const& p, int numCtas, bool pdl, cudaStream_t stream) +{ + auto kernel = fusedMmaDraftTailKernel; + size_t const smem = static_cast(R) * kMmaHStride * sizeof(__nv_bfloat16); + static bool const attrSet = [&]() + { + TLLM_CUDA_CHECK( + cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(smem))); + return true; + }(); + (void) attrSet; + if (numCtas <= 0) + { + int const numTiles = (p.vocabLocal + 15) / 16; + numCtas = (numTiles + kWarps - 1) / kWarps; + } + TLLM_CHECK_WITH_INFO((p.mode != ExchangeMode::kMulticastDirect && p.mode != ExchangeMode::kUnicastDirect) + || numCtas <= kMaxDirectCtas, + "glm_moe_dsa_draft_tail: direct exchange supports at most %d CTAs", kMaxDirectCtas); + cudaLaunchConfig_t config{}; + config.gridDim = dim3(numCtas); + config.blockDim = dim3(kWarps * 32); + config.dynamicSmemBytes = smem; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = pdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 1; + TLLM_CUDA_CHECK(cudaLaunchKernelEx(&config, kernel, p)); +} + +template +void launchFusedRows(DraftTailParams const& p, int variant, int numCtas, bool pdl, cudaStream_t stream) +{ + switch (variant) + { + case 0: launchFusedShape(p, numCtas, pdl, stream); break; + case 1: launchFusedShape(p, numCtas, pdl, stream); break; + case 2: launchFusedShape(p, numCtas, pdl, stream); break; + case 3: launchFusedShape(p, numCtas, pdl, stream); break; + case 4: launchFusedShape(p, numCtas, pdl, stream); break; + case 5: launchFusedShape(p, numCtas, pdl, stream); break; + case 6: + if constexpr (R <= 2) + { + launchBulkShape(p, numCtas, pdl, stream); + } + else + { + launchFusedShape(p, numCtas, pdl, stream); + } + break; + case 7: launchMmaShape(p, numCtas, pdl, stream); break; + case 8: launchMmaShape(p, numCtas, pdl, stream); break; + case 9: launchMmaShape(p, numCtas, pdl, stream); break; + case 10: launchMmaShape(p, numCtas, pdl, stream); break; + case 11: launchMmaShape(p, numCtas, pdl, stream); break; + case 12: launchMmaShape(p, numCtas, pdl, stream); break; + case 13: launchMmaShape(p, numCtas, pdl, stream); break; + case 14: launchMmaShape(p, numCtas, pdl, stream); break; + default: TLLM_THROW("glm_moe_dsa_draft_tail: unknown fused variant %d", variant); + } +} + +int smCount() +{ + static int const count = []() + { + int dev = 0; + TLLM_CUDA_CHECK(cudaGetDevice(&dev)); + int n = 0; + TLLM_CUDA_CHECK(cudaDeviceGetAttribute(&n, cudaDevAttrMultiProcessorCount, dev)); + return n; + }(); + return count; +} + +} // namespace + +int numFusedVariants() +{ + return kNumVariants; +} + +void invokeDraftTailFused(DraftTailParams const& p, FusedLaunchConfig const& config, cudaStream_t stream) +{ + TLLM_CHECK_WITH_INFO(p.hiddenDim == 6144, "glm_moe_dsa_draft_tail: fused kernel supports hidden size 6144 only"); + TLLM_CHECK_WITH_INFO(p.rows >= 1 && p.rows <= kMaxRows, "glm_moe_dsa_draft_tail: rows out of range"); + TLLM_CHECK_WITH_INFO(config.variant >= 0 && config.variant < kNumVariants, "glm_moe_dsa_draft_tail: bad variant"); + TLLM_CHECK_WITH_INFO(p.logitsOut == nullptr || config.variant >= kFirstMmaVariant, + "glm_moe_dsa_draft_tail: logits output needs a tensor-core variant"); + int numCtas = config.numCtas; + if (numCtas <= 0 && config.variant < kFirstMmaVariant) + { + numCtas = smCount() * kVariants[config.variant].minBlocks; + } + TLLM_CHECK_WITH_INFO((p.mode != ExchangeMode::kMulticastDirect && p.mode != ExchangeMode::kUnicastDirect) + || numCtas <= kMaxDirectCtas, + "glm_moe_dsa_draft_tail: direct exchange supports at most %d CTAs", kMaxDirectCtas); + switch (p.rows) + { + case 1: launchFusedRows<1>(p, config.variant, numCtas, config.pdl, stream); break; + case 2: launchFusedRows<2>(p, config.variant, numCtas, config.pdl, stream); break; + case 3: launchFusedRows<3>(p, config.variant, numCtas, config.pdl, stream); break; + case 4: launchFusedRows<4>(p, config.variant, numCtas, config.pdl, stream); break; + case 5: launchFusedRows<5>(p, config.variant, numCtas, config.pdl, stream); break; + case 6: launchFusedRows<6>(p, config.variant, numCtas, config.pdl, stream); break; + case 7: launchFusedRows<7>(p, config.variant, numCtas, config.pdl, stream); break; + case 8: launchFusedRows<8>(p, config.variant, numCtas, config.pdl, stream); break; + default: break; + } +} + +template +void launchFromLogitsRows(DraftTailParams const& p, bool pdl, cudaStream_t stream) +{ + cudaLaunchConfig_t config{}; + config.gridDim = dim3(1); + config.blockDim = dim3(512); + config.dynamicSmemBytes = 0; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = pdl ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 1; + TLLM_CUDA_CHECK(cudaLaunchKernelEx(&config, fromLogitsDraftTailKernel, p)); +} + +void invokeDraftTailFromLogits(DraftTailParams const& p, bool pdl, cudaStream_t stream) +{ + TLLM_CHECK_WITH_INFO(p.rows >= 1 && p.rows <= kMaxRows, "glm_moe_dsa_draft_tail: rows out of range"); + switch (p.rows) + { + case 1: launchFromLogitsRows<1>(p, pdl, stream); break; + case 2: launchFromLogitsRows<2>(p, pdl, stream); break; + case 3: launchFromLogitsRows<3>(p, pdl, stream); break; + case 4: launchFromLogitsRows<4>(p, pdl, stream); break; + case 5: launchFromLogitsRows<5>(p, pdl, stream); break; + case 6: launchFromLogitsRows<6>(p, pdl, stream); break; + case 7: launchFromLogitsRows<7>(p, pdl, stream); break; + case 8: launchFromLogitsRows<8>(p, pdl, stream); break; + default: break; + } +} + +} // namespace kernels::glm_moe_dsa::draft_tail + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/draftTail.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftTail.h new file mode 100644 index 000000000000..82e13a7f084a --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/draftTail.h @@ -0,0 +1,144 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Greedy draft-token selection over a vocab-sharded LM head. +// +// For R hidden rows and this rank's vocab shard W [V_local, H] the kernels +// produce, on every rank, the token id of the global maximum of the BF16 +// logits h @ W^T over all tensor-parallel shards; ties go to the lowest global +// token id and NaN ranks above every number. That is the selection rule of +// SpecWorkerBase.greedy_sample_draft_with_tp_gather. +// +// The logits kernel selects from BF16 logits computed by the caller (the +// native LM-head GEMM), so its tokens are the native ones. The fused kernels +// compute the logits themselves (FP32 accumulation, then BF16 rounding); +// where the largest logits are within one BF16 rounding step of each other, +// the selected token depends on the exact FP32 sums, i.e. on the accumulation +// order. Fused variants 11 to 14 accumulate like the native cuBLAS LM-head +// GEMM selected for this shape on SM103 (K in 16-value tensor-core steps from +// 0 to H into one FP32 accumulator), so their BF16 logits and tokens equal the +// native ones bit for bit (checked by the draft_tail unit tests against F.linear; +// re-check whenever cuBLAS or the GPU changes). The other fused variants +// accumulate in other orders and are decision-level variants at BF16 +// near-ties. +// +// Each candidate is packed into one 64-bit key, (ordered BF16 bits << 32) | +// (0xFFFFFFFF - global id), so the selection is a plain unsigned max that is +// associative, commutative and independent of reduction order. The keys of +// all ranks are exchanged through a multicast (or unicast peer) buffer with a +// 0-sentinel protocol over three rotating slots; see draftTail.cu. + +#pragma once + +#include "tensorrt_llm/common/config.h" + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::draft_tail +{ + +constexpr int kMaxRows = 8; +constexpr int kMaxRanks = 8; +constexpr int kNumSlots = 3; +// Exchange buffer: kNumSlots x kMaxRanks x kMaxRows 64-bit keys. +constexpr int kSlotWords = kMaxRanks * kMaxRows; +constexpr int kExchangeWords = kNumSlots * kSlotWords; +// Rank-local scratch, in 64-bit words: kMaxRows partial keys, then the CTA +// arrival counter and the exchange epoch (32 bits each in one word), then an +// error word. +constexpr int kScratchWords = kMaxRows + 2; + +enum class ExchangeMode : int +{ + kNone = 0, // tpSize == 1, or debug: tokens from the rank-local maximum + kMulticast = 1, // one multimem.st per row into the multicast slot + kUnicast = 2, // one store per (peer, row) into each peer's slot + // Direct variants: every CTA publishes its own partial key (no rank-local + // atomics or last-CTA election); CTA 0 of each rank polls all + // tpSize x numCtas entries, writes the tokens and recycles the slot. + kMulticastDirect = 3, + kUnicastDirect = 4, +}; + +constexpr int kMaxDirectCtas = 512; +constexpr int kDirectSlotWords = kMaxRows * kMaxRanks * kMaxDirectCtas; +// The direct region follows the last-CTA region in the same buffer. +constexpr int kDirectOffsetWords = kExchangeWords; +constexpr int kExchangeBufferWords = kExchangeWords + kNumSlots * kDirectSlotWords; + +struct DraftTailParams +{ + // Fused mode: hidden [rows, hiddenDim] (row stride hiddenStride elements) + // and the vocab shard weight [vocabLocal, hiddenDim], row-major. + __nv_bfloat16 const* hidden = nullptr; + int64_t hiddenStride = 0; + __nv_bfloat16 const* weight = nullptr; + // Logits mode: BF16 logits [rows, vocabLocal] (row stride logitsStride). + __nv_bfloat16 const* logits = nullptr; + int64_t logitsStride = 0; + + int rows = 0; + int vocabLocal = 0; + int hiddenDim = 0; + int64_t vocabOffset = 0; // global id of local column 0 + + int32_t* tokens = nullptr; // [rows] output + uint64_t* keysOut = nullptr; // optional [rows] output: the winning key + + uint64_t* scratch = nullptr; // [kScratchWords], zero-initialized once + + int tpRank = 0; + int tpSize = 1; + ExchangeMode mode = ExchangeMode::kNone; + uint64_t* mcSlots = nullptr; // multicast address of the exchange buffer + uint64_t* ucSlotsLocal = nullptr; // this rank's unicast address of the buffer + uint64_t* const* ucSlotsPeers = nullptr; // device array [tpSize] of unicast addresses + long long timeoutCycles = 0; // 0: wait forever + // Optional BF16 logits [rows, vocabLocal] (row stride logitsOutStride), + // written by the tensor-core fused kernels only (tests compare them with + // the native GEMM bit for bit); nullptr in production. + __nv_bfloat16* logitsOut = nullptr; + int64_t logitsOutStride = 0; + // Fused kernel, L2-hinted variants: each warp loads the first + // keepPermille / 1000 of its rows with an L2 evict_last policy (the rest + // evict_first), so that part of the shard tends to stay in L2 across the + // draft steps that re-read the same weight. + int keepPermille = 0; +}; + +struct FusedLaunchConfig +{ + int numCtas = 0; // 0: one per SM + int variant = 0; // see draftTail.cu: kernel shape and load flavour + bool pdl = true; +}; + +int numFusedVariants(); + +// GEMV + rank-local max + exchange in one launch. +void invokeDraftTailFused(DraftTailParams const& params, FusedLaunchConfig const& config, cudaStream_t stream); + +// Rank-local max over precomputed BF16 logits + exchange in one launch. +void invokeDraftTailFromLogits(DraftTailParams const& params, bool pdl, cudaStream_t stream); + +} // namespace kernels::glm_moe_dsa::draft_tail + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.cu new file mode 100644 index 000000000000..8d21b5092336 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.cu @@ -0,0 +1,439 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.h" + +#include "tensorrt_llm/common/assert.h" +#include "tensorrt_llm/common/cudaUtils.h" +#include "tensorrt_llm/kernels/decoderMaskedMultiheadAttentionUtils.h" + +#include + +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::indexer_layer_front +{ + +using glm_moe_dsa::rope_kv_append::kKvLoraRank; +using glm_moe_dsa::rope_kv_append::kLatentDim; +using glm_moe_dsa::rope_kv_append::kRopeDim; +using glm_moe_dsa::rope_kv_append::RopeAppendParams; + +namespace +{ + +constexpr int32_t kRopeThreads = 256; +constexpr int32_t kEltsPerVec = 8; // bf16 elements per 16-byte load +constexpr int32_t kNopeVecs = kKvLoraRank / kEltsPerVec; // 64 +constexpr int32_t kRopeVecs = kRopeDim / kEltsPerVec; // 8 +constexpr int32_t kTotalVecsPerHead = kNopeVecs + kRopeVecs; // 72 +constexpr int32_t kIdxBytesPerItem = 4; + +__device__ __forceinline__ void pdlWait() +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + asm volatile("griddepcontrol.wait;" ::: "memory"); +#endif +} + +__device__ __forceinline__ void pdlLaunchDependents() +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + asm volatile("griddepcontrol.launch_dependents;" ::: "memory"); +#endif +} + +//! Same arithmetic as quantCopy<__nv_bfloat16, 8> in mlaKernels.cu: bf16 -> fp32, scale, saturating E4M3. +__device__ __forceinline__ uint2 quantizeBf16x8(uint4 const& src, float scale) +{ + __nv_bfloat162 const* in = reinterpret_cast<__nv_bfloat162 const*>(&src); + uint2 out; + __nv_fp8x2_e4m3* dst = reinterpret_cast<__nv_fp8x2_e4m3*>(&out); +#pragma unroll + for (int32_t j = 0; j < 4; ++j) + { + float2 v = __bfloat1622float2(in[j]); + v.x *= scale; + v.y *= scale; + dst[j] = __nv_fp8x2_e4m3(v); + } + return out; +} + +//! GPT-J RoPE on 8 bf16 values (4 adjacent pairs), identical to the per-pair transform of the native kernel. +__device__ __forceinline__ void rotateBf16x8(uint4& data, float2 const (&coef)[4]) +{ + __nv_bfloat162* pairs = reinterpret_cast<__nv_bfloat162*>(&data); +#pragma unroll + for (int32_t j = 0; j < 4; ++j) + { + pairs[j] = mmha::rotary_embedding_transform(pairs[j], coef[j]); + } +} + +//! Native unravel of a flat byte index over the strided 4-D indexer cache view (indexerKCacheScatter.cu); +//! 32-bit arithmetic when the index fits, same result. +__device__ __forceinline__ int64_t idxFlatToOffset(int64_t flat, int32_t const (&dims)[4], int64_t const (&strides)[4]) +{ + int32_t i0, i1, i2, i3; + if (flat >= 0 && flat <= 0x7fffffff) + { + uint32_t f = static_cast(flat); + i3 = static_cast(f % static_cast(dims[3])); + f /= static_cast(dims[3]); + i2 = static_cast(f % static_cast(dims[2])); + f /= static_cast(dims[2]); + i1 = static_cast(f % static_cast(dims[1])); + f /= static_cast(dims[1]); + i0 = static_cast(f); + } + else + { + i3 = flat % dims[3]; + flat /= dims[3]; + i2 = flat % dims[2]; + flat /= dims[2]; + i1 = flat % dims[1]; + flat /= dims[1]; + i0 = flat; + } + return i0 * strides[0] + i1 * strides[1] + i2 * strides[2] + i3 * strides[3]; +} + +enum class RopeItem : int32_t +{ + kNone = 0, + kQNope, + kQRope, + kKRope, + kKNope, + kIdxData, + kIdxScale, +}; + +//! One thread per 16-byte work item of one token. Per token the item space is +//! [0, H*64) q_nope FP8 quantization (head = i / 64, vec = i % 64) +//! [H*64, H*72) q_pe RoPE + FP8 (head, vec) +//! [H*72, H*72+8) k_pe RoPE + FP8 cache write (vec) +//! [H*72+8, H*72+72) compressed-KV FP8 cache write (vec) +//! then idxHeadBytes/4 indexer K data items (4 bytes each) and one indexer scale item. +//! Per-step metadata (cache lengths, KV block offsets, indexer slot mappings) and the item's source data are read only +//! after griddepcontrol.wait (see the header). Identical to ropeAppendScatterKernel except for the trigger position. +template +__global__ void __launch_bounds__(kRopeThreads) ropeAppendScatterZ14Kernel(RopeAppendParams const p) +{ + int32_t const token = blockIdx.x; + int32_t const item = blockIdx.y * kRopeThreads + threadIdx.x; + int32_t const qNopeEnd = p.numHeads * kNopeVecs; + int32_t const qRopeEnd = qNopeEnd + p.numHeads * kRopeVecs; + int32_t const kRopeEnd = qRopeEnd + kRopeVecs; + int32_t const kNopeEnd = kRopeEnd + kNopeVecs; + int32_t const idxDataEnd = kNopeEnd + p.idxHeadBytes / kIdxBytesPerItem; + + bool const ropeToken = token < p.numTokens; + bool const idxToken = token < p.idxNumTokens; + + RopeItem kind = RopeItem::kNone; + int32_t head = 0; + int32_t vec = 0; + if (item < qNopeEnd) + { + kind = ropeToken ? RopeItem::kQNope : RopeItem::kNone; + head = item / kNopeVecs; + vec = item % kNopeVecs; + } + else if (item < qRopeEnd) + { + kind = ropeToken ? RopeItem::kQRope : RopeItem::kNone; + head = (item - qNopeEnd) / kRopeVecs; + vec = (item - qNopeEnd) % kRopeVecs; + } + else if (item < kRopeEnd) + { + kind = ropeToken ? RopeItem::kKRope : RopeItem::kNone; + vec = item - qRopeEnd; + } + else if (item < kNopeEnd) + { + kind = ropeToken ? RopeItem::kKNope : RopeItem::kNone; + vec = item - kRopeEnd; + } + else if (item < idxDataEnd) + { + kind = idxToken ? RopeItem::kIdxData : RopeItem::kNone; + vec = item - kNopeEnd; + } + else if (item == idxDataEnd) + { + kind = idxToken ? RopeItem::kIdxScale : RopeItem::kNone; + } + + int32_t const batchIdx = token / p.seqLen; + int32_t const localIdx = token % p.seqLen; + bool const needPos = kind == RopeItem::kQRope || kind == RopeItem::kKRope || kind == RopeItem::kKNope; + bool const needCoef = kind == RopeItem::kQRope || kind == RopeItem::kKRope; + bool const needBlock = kind == RopeItem::kKRope || kind == RopeItem::kKNope; + bool const isIdx = kind == RopeItem::kIdxData || kind == RopeItem::kIdxScale; + + // Values that never change while kernels of the step run (the quantization scales) may be read before the wait; + // per-step metadata and producer outputs may not. + float scale = 1.F; + if (needBlock) + { + scale = p.quantScaleKv ? p.quantScaleKv[0] : 1.F; + } + else if (kind == RopeItem::kQNope || kind == RopeItem::kQRope) + { + scale = p.quantScaleQ ? p.quantScaleQ[0] : 1.F; + } + auto loadCoef = [&](int32_t pos, float2(&c)[4]) + { + float2 const* src = p.cosSin + static_cast(kRopeDim) * pos + (vec * kEltsPerVec) / 2; +#pragma unroll + for (int32_t j = 0; j < 4; ++j) + { + c[j] = src[j]; + } + }; + auto idxOffset = [&](int64_t slotData, int64_t slotScale) -> int64_t + { + if (slotData < 0 || slotScale < 0) + { + return -1; + } + return kind == RopeItem::kIdxData ? idxFlatToOffset(slotData + vec * kIdxBytesPerItem, p.idxDims, p.idxStrides) + : idxFlatToOffset(slotScale, p.idxDims, p.idxStrides); + }; + // Source data of this item; written by producers, so it is read after the wait. + uint4 vdata = make_uint4(0, 0, 0, 0); + uint32_t wdata = 0; + auto loadSource = [&]() + { + switch (kind) + { + case RopeItem::kQNope: + vdata = *reinterpret_cast( + p.fusedQ + (static_cast(token) * p.numHeads + head) * kLatentDim + vec * kEltsPerVec); + break; + case RopeItem::kQRope: + vdata = *reinterpret_cast(p.qPe + static_cast(token) * p.qPeStride + + static_cast(p.qPeLd) * head + vec * kEltsPerVec); + break; + case RopeItem::kKRope: + vdata = *reinterpret_cast( + p.latent + static_cast(token) * kLatentDim + kKvLoraRank + vec * kEltsPerVec); + break; + case RopeItem::kKNope: + vdata = *reinterpret_cast( + p.latent + static_cast(token) * kLatentDim + vec * kEltsPerVec); + break; + case RopeItem::kIdxData: + wdata = *reinterpret_cast( + p.idxK + static_cast(token) * p.idxHeadBytes + vec * kIdxBytesPerItem); + break; + case RopeItem::kIdxScale: + wdata = *reinterpret_cast(p.idxScale + static_cast(token) * kIdxBytesPerItem); + break; + default: break; + } + }; + + int32_t kvPos = 0; + float2 coef[4]; + __nv_fp8_e4m3* kBlock = nullptr; + int64_t idxDst = -1; + pdlWait(); + // Issue the source-data load first so it overlaps the metadata loads and the address math below. + loadSource(); + if (needPos) + { + kvPos = p.kvLens[batchIdx] - p.seqLen + localIdx; + } + if (needCoef) + { + loadCoef(kvPos, coef); + } + if (needBlock) + { + kBlock = reinterpret_cast<__nv_fp8_e4m3*>(p.kvCache.getKBlockPtr(batchIdx, kvPos)); + } + if (isIdx) + { + idxDst = idxOffset(p.idxSlotData[token], p.idxSlotScale[token]); + } + if constexpr (kEarlyTrigger) + { + // Every global read of this thread's item has been issued. The block counts as triggered at the first of + // its threads to get here (later invocations in the block have no further effect), so a dependent may be + // resident before this block's stores below and before block (0, 0)'s scheduler-buffer stores and their + // cache-length reads. Dependents read this kernel's outputs only after their own griddepcontrol.wait, and + // write no global memory before it, so neither the stores nor the reads below can race with them. + pdlLaunchDependents(); + } + + switch (kind) + { + case RopeItem::kQNope: + { + size_t const off = (static_cast(token) * p.numHeads + head) * kLatentDim + vec * kEltsPerVec; + *reinterpret_cast(p.quantQ + off) = quantizeBf16x8(vdata, scale); + break; + } + case RopeItem::kQRope: + { + uint4 data = vdata; + rotateBf16x8(data, coef); + size_t const dst + = (static_cast(token) * p.numHeads + head) * kLatentDim + kKvLoraRank + vec * kEltsPerVec; + *reinterpret_cast(p.quantQ + dst) = quantizeBf16x8(data, scale); + break; + } + case RopeItem::kKRope: + { + uint4 data = vdata; + rotateBf16x8(data, coef); + int32_t const inBlockIdx = p.kvCache.getKVLocalIdx(kvPos, 0, kTotalVecsPerHead, kNopeVecs + vec); + *reinterpret_cast(kBlock + inBlockIdx * kEltsPerVec) = quantizeBf16x8(data, scale); + break; + } + case RopeItem::kKNope: + { + int32_t const inBlockIdx = p.kvCache.getKVLocalIdx(kvPos, 0, kTotalVecsPerHead, vec); + *reinterpret_cast(kBlock + inBlockIdx * kEltsPerVec) = quantizeBf16x8(vdata, scale); + if (vec == 0 && localIdx == 0) + { + p.cuQSeqlens[batchIdx + 1] = p.numHeads * p.seqLen * (batchIdx + 1); + } + break; + } + case RopeItem::kIdxData: + case RopeItem::kIdxScale: + { + if (idxDst >= 0) + { + *reinterpret_cast(p.idxCache + idxDst) = wdata; + } + break; + } + default: break; + } + + if (blockIdx.x == 0 && blockIdx.y == 0) + { + if (threadIdx.x == 0) + { + p.fmhaTileCounter[0] = 0; + p.cuQSeqlens[0] = 0; + float const dequantQ = p.dequantScaleQ ? p.dequantScaleQ[0] : 1.F; + float const dequantKv = p.dequantScaleKv ? p.dequantScaleKv[0] : 1.F; + float const quantO = p.quantScaleO ? p.quantScaleO[0] : 1.F; + if (p.bmm1Scale) + { + constexpr float kLog2e = 1.4426950408889634074F; + float const bmm1 = dequantQ * dequantKv * p.hostBmm1Scale; + p.bmm1Scale[0] = bmm1; + p.bmm1Scale[1] = bmm1 * kLog2e; + } + if (p.bmm2Scale) + { + p.bmm2Scale[0] = quantO * dequantKv; + } + } + if (threadIdx.x < 32) + { + // Exclusive prefix sum of the cache lengths -> cuKvSeqlens[0..batch]. + int32_t const lane = threadIdx.x; + int32_t const batch = p.numTokens / p.seqLen; + int32_t carry = 0; + for (int32_t base = 0; base <= batch; base += 32) + { + int32_t const idx = base + lane; + int32_t const len = idx < batch ? p.kvLens[idx] : 0; + int32_t incl = len; +#pragma unroll + for (int32_t off = 1; off < 32; off <<= 1) + { + int32_t const n = __shfl_up_sync(0xffffffffU, incl, off); + if (lane >= off) + { + incl += n; + } + } + if (idx <= batch) + { + p.cuKvSeqlens[idx] = incl - len + carry; + } + carry += __shfl_sync(0xffffffffU, incl, 31); + } + } + } + + if constexpr (!kEarlyTrigger) + { + // ropeAppendScatterKernel's position. + pdlLaunchDependents(); + } +} + +template +void launchWithOptionalPdl(Kernel kernel, dim3 grid, dim3 block, bool enablePdl, cudaStream_t stream, Params const& p) +{ + cudaLaunchConfig_t config{}; + config.gridDim = grid; + config.blockDim = block; + config.dynamicSmemBytes = 0; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = enablePdl ? 1 : 0; + config.numAttrs = 1; + config.attrs = attrs; + TLLM_CUDA_CHECK(cudaLaunchKernelEx(&config, kernel, p)); +} + +} // namespace + +void invokeIndexerLayerRopeKvAppend( + RopeAppendParams const& params, bool enablePdl, bool earlyTrigger, cudaStream_t stream) +{ + int32_t const gridTokens = std::max(params.numTokens, params.idxNumTokens); + if (gridTokens == 0) + { + return; + } + TLLM_CHECK_WITH_INFO(params.seqLen > 0, "seqLen must be positive"); + TLLM_CHECK_WITH_INFO(params.cosSinRows > 0, "the RoPE table must have at least one position"); + TLLM_CHECK_WITH_INFO(params.idxHeadBytes % kIdxBytesPerItem == 0, "indexer head bytes must be a multiple of 4"); + int32_t const itemsPerToken + = params.numHeads * kTotalVecsPerHead + kTotalVecsPerHead + params.idxHeadBytes / kIdxBytesPerItem + 1; + dim3 const grid(gridTokens, (itemsPerToken + kRopeThreads - 1) / kRopeThreads); + dim3 const block(kRopeThreads); + if (earlyTrigger) + { + launchWithOptionalPdl(ropeAppendScatterZ14Kernel, grid, block, enablePdl, stream, params); + } + else + { + launchWithOptionalPdl(ropeAppendScatterZ14Kernel, grid, block, enablePdl, stream, params); + } +} + +} // namespace kernels::glm_moe_dsa::indexer_layer_front + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.h new file mode 100644 index 000000000000..20a8e8b530de --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.h @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.h" + +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::indexer_layer_front +{ + +//! Fork of the generation rope of ropeKvAppend.h (ropeAppendScatterKernel): the same bytes for the same parameters +//! (glm_moe_dsa::rope_kv_append::RopeAppendParams), with the programmatic-launch trigger moved. +//! +//! Memory order: like ropeAppendScatterKernel, only the quantization scales are read before griddepcontrol.wait; every +//! producer output and every per-step metadata word is read after it. With earlyTrigger every thread executes +//! griddepcontrol.launch_dependents right after the global reads of its work item (source item, cache length, rotary +//! coefficients, KV block offset and indexer slot mapping), before its stores. A block counts as triggered at its first +//! such invocation, so dependents may become resident before any store of the kernel, including block (0, 0)'s +//! scheduler buffers (cuQSeqlens, cuKvSeqlens, the tile counter, the bmm scales) and the cache-length reads of its +//! prefix sum. Without earlyTrigger the trigger is at the end of the kernel, as there. A dependent must not read +//! anything this kernel writes, nor write anything it reads, before its own griddepcontrol.wait; the rope's dependents +//! already require this, since a trigger at the end of the code does not make the stores visible either. +void invokeIndexerLayerRopeKvAppend(glm_moe_dsa::rope_kv_append::RopeAppendParams const& params, bool enablePdl, + bool earlyTrigger, cudaStream_t stream); + +} // namespace kernels::glm_moe_dsa::indexer_layer_front + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.cu new file mode 100644 index 000000000000..5f85a965e1d7 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.cu @@ -0,0 +1,680 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.h" + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::post_attention_hop +{ +namespace +{ +namespace cg = cooperative_groups; + +// Work decomposition. +// +// Every cluster has kSlices CTAs; CTA c owns slice [c * kSlice, (c + 1) * kSlice), the slice a CTA of the native +// one-shot kernel owns for numRows <= 8 (96 threads x 8 elements, 8 CTAs per row), so the RMSNorm reduction below +// reproduces the native tree exactly. +// +// Without router logits (dense layers) the layout is the native one: cluster y handles row y, and every CTA collects +// one row slice (8 ranks x 768 elements). +// +// With router logits there are kG x kRowSplit clusters (kG = 4, kRowSplit = min(R, 3): at most 12 clusters, which stay +// co-resident). Cluster (g, h) collects and normalizes the rows r with r % kRowSplit == h, so a CTA reads at most +// ceil(R / kRowSplit) row slices, and computes the logits of those rows for experts [64 g, 64 g + 64). Its router +// weights live in registers (loaded before the PDL wait). Each CTA multiplies its rows of slice c by the experts' +// weights of slice c with BF16 MMAs (experts on m16, rows on n8) and pushes the partial logits to the CTA that owns the +// expert; owners add the 8 slices in a fixed order, so the logits are identical on every rank. Clusters never +// synchronize with each other; inside a cluster, slice sums and partial logits travel as st.async stores that complete +// transactions on the consumer's mbarrier. +constexpr int kSlices = 8; +constexpr int kSlice = kHiddenDim / kSlices; +constexpr int kEltsPerVec = 8; +constexpr int kVecPerSlice = kSlice / kEltsPerVec; +constexpr int kNativeWarpsPerSlice = kVecPerSlice / 32; +// Row stride of the normalized-slice tile in 16-byte vectors; 100 = 96 + 4 keeps the 16-byte B-fragment loads of two +// rows in one quarter-warp phase on disjoint banks. +constexpr int kNormedStride = kVecPerSlice + 4; +constexpr int kMmaChains = 4; +constexpr uint32_t kLamportSentinel = 0x80000000U; + +static_assert(kHiddenDim % (kSlices * kEltsPerVec * 32) == 0); +static_assert(kVecPerSlice == kNativeWarpsPerSlice * 32); +static_assert(kMaxRows == 8, "the router MMA maps rows onto the n8 dimension"); + +// kG = number of expert groups (0: hop without router logits); kRowSplit = number of row groups. +template +struct HopConfig +{ + static constexpr int kThreads = 384; + static constexpr int kWarps = kThreads / 32; + static constexpr int kExpertsPerCluster = kNumExperts / kG; + static constexpr int kExpertTiles = kExpertsPerCluster / 16; + static constexpr int kKGroups = kWarps / kExpertTiles; + static constexpr int kKPerGroup = kSlice / kKGroups; + static constexpr int kPairsPerGroup = kKPerGroup / 32; + static constexpr int kExpertsPerOwner = kExpertsPerCluster / kSlices; + static_assert(kExpertsPerCluster % 16 == 0 && kWarps % kExpertTiles == 0 && kSlice % (kKGroups * 32) == 0); + static_assert(kPairsPerGroup % (kMmaChains / 2) == 0); +}; + +template +struct HopConfig<0, kRowSplit> +{ + static constexpr int kThreads = 128; + static constexpr int kWarps = kThreads / 32; + static constexpr int kMaxRowsPerCta = 1; + static_assert(kMaxRowsPerCta * kVecPerSlice <= kThreads && kMaxRowsPerCta * kNativeWarpsPerSlice <= kWarps); +}; + +constexpr int kMaxRowsPerCtaAll = kMaxRows / 2; + +struct alignas(16) HopStorage +{ + uint4 gamma[kVecPerSlice]; + float vecSq[kMaxRowsPerCtaAll][kVecPerSlice]; + float warpSq[kMaxRowsPerCtaAll][kNativeWarpsPerSlice]; + float blockSq[kMaxRowsPerCtaAll][kSlices]; + // Completes when all slice sums of this CTA's rows have arrived (st.async complete_tx). + alignas(8) uint64_t normBar; + // Completes when all partial logits of the experts this CTA owns have arrived. + alignas(8) uint64_t routerBar; +}; + +template +struct alignas(16) RouterStorage +{ + using Cfg = HopConfig; + uint4 normed[kMaxRows][kNormedStride]; // local row on the MMA n dimension; unused rows stay zero + float partial[kSlices][Cfg::kKGroups][Cfg::kExpertsPerOwner][kMaxRows]; +}; + +struct EmptyStorage +{ +}; + +#ifdef TLLM_GLM_LL_W2_TIMING +// Development-only phase timestamps (globaltimer, ns) of thread 0 of every CTA for the most recent launch. +__device__ unsigned long long gW2Timing[kTimingMaxCtas][kTimingStamps]; +#define W2_STAMP(i) \ + do \ + { \ + if (threadIdx.x == 0) \ + { \ + unsigned long long t; \ + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); \ + gW2Timing[blockIdx.y * gridDim.x + blockIdx.x][i] = t; \ + } \ + } while (0) +#else +#define W2_STAMP(i) \ + do \ + { \ + } while (0) +#endif + +union BfVec +{ + uint4 packed; + __nv_bfloat16 elts[kEltsPerVec]; +}; + +__device__ __forceinline__ uint4 loadVolatile(void const* ptr) +{ + uint4 v; + asm volatile("ld.volatile.global.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) + : "l"(ptr) + : "memory"); + return v; +} + +__device__ __forceinline__ uint4 loadConstant(void const* ptr) +{ + uint4 v; + asm volatile("ld.global.nc.v4.u32 {%0, %1, %2, %3}, [%4];" : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) : "l"(ptr)); + return v; +} + +__device__ __forceinline__ bool isDirty(uint4 const& v) +{ + return v.x == kLamportSentinel || v.y == kLamportSentinel || v.z == kLamportSentinel || v.w == kLamportSentinel; +} + +// Lamport payloads must never contain the sentinel word: map bf16 -0.0 to +0.0 (the reduction starts from +0.0, so +// the sum is unchanged). +__device__ __forceinline__ uint4 sanitize(uint4 v) +{ + BfVec b; + b.packed = v; +#pragma unroll + for (int i = 0; i < kEltsPerVec; ++i) + { + if (__bfloat16_as_ushort(b.elts[i]) == 0x8000U) + { + b.elts[i] = __ushort_as_bfloat16(0); + } + } + return b.packed; +} + +__device__ __forceinline__ uint32_t sharedAddr(void const* ptr) +{ + return static_cast(__cvta_generic_to_shared(ptr)); +} + +// Address of the same shared-memory object in CTA `rank` of the cluster. +__device__ __forceinline__ uint32_t clusterAddr(uint32_t addr, uint32_t rank) +{ + uint32_t out; + asm volatile("mapa.shared::cluster.u32 %0, %1, %2;" : "=r"(out) : "r"(addr), "r"(rank)); + return out; +} + +__device__ __forceinline__ void mbarrierInitExpectTx(uint64_t* bar, uint32_t bytes) +{ + asm volatile("mbarrier.init.shared::cta.b64 [%0], 1;" ::"r"(sharedAddr(bar)) : "memory"); + asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;" ::"r"(sharedAddr(bar)), "r"(bytes) + : "memory"); +} + +__device__ __forceinline__ void mbarrierWaitPhase0(uint64_t* bar) +{ + asm volatile( + "{\n" + ".reg .pred done;\n" + "HOP_MBAR_WAIT_%=:\n" + "mbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 done, [%0], 0;\n" + "@!done bra HOP_MBAR_WAIT_%=;\n" + "}\n" ::"r"(sharedAddr(bar)) + : "memory"); +} + +// Remote shared-memory stores that complete transactions on the destination CTA's mbarrier. +__device__ __forceinline__ void storeAsync(uint32_t remoteAddr, float v, uint32_t remoteBar) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.f32 [%0], %1, [%2];" ::"r"(remoteAddr), "f"(v), + "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void storeAsync(uint32_t remoteAddr, float a, float b, uint32_t remoteBar) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.v2.f32 [%0], {%1, %2}, [%3];" ::"r"(remoteAddr), + "f"(a), "f"(b), "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void mmaBf16( + float (&d)[4], uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t b0, uint32_t b1) +{ + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 {%0, %1, %2, %3}, {%4, %5, %6, %7}, {%8, %9}, " + "{%0, %1, %2, %3};" + : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b0), "r"(b1)); +} + +template +__global__ void __launch_bounds__(HopConfig::kThreads, 1) + postAttnHopKernel(PostAttnHopParams const params) +{ + static_assert(R >= 1 && R <= kMaxRows); +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + constexpr bool kRouter = kG > 0; + using Cfg = HopConfig; + constexpr int kClusters = kRouter ? kG * kRowSplit : R; + constexpr int kGridCtas = kClusters * kSlices; + // Rows one CTA collects, normalizes and multiplies. + constexpr int kRowsPerCta = kRouter ? (R + kRowSplit - 1) / kRowSplit : 1; + static_assert(kRowsPerCta <= kMaxRowsPerCtaAll); + static_assert(kRowsPerCta * kVecPerSlice <= Cfg::kThreads && kRowsPerCta * kNativeWarpsPerSlice <= Cfg::kWarps); + if constexpr (kRouter) + { + static_assert( + HopConfig<(kG > 0 ? kG : 4), kRowSplit>::kExpertsPerOwner * kRowsPerCta * kSlices <= Cfg::kThreads); + } + + __shared__ HopStorage hop; + __shared__ std::conditional_t 0 ? kG : 4), (kG > 0 ? kRowSplit : 2)>, EmptyStorage> rs; + + cg::cluster_group cluster = cg::this_cluster(); + int const slice = static_cast(cluster.block_rank()); + int const clusterId = blockIdx.y; + int const expertGroup = kRouter ? clusterId % (kG > 0 ? kG : 1) : 0; + int const rowGroup = kRouter ? clusterId / (kG > 0 ? kG : 1) : clusterId; + int const tid = threadIdx.x; + int const lane = tid & 31; + int const warp = tid >> 5; + int const sliceBase = slice * kSlice; + + // Rows this CTA collects and normalizes: local row j is global row rowGroup + j * rowStride. + constexpr int kRowStride = kRouter ? kRowSplit : 1; + int const numRowsCta = kRouter ? (R - rowGroup + kRowSplit - 1) / kRowSplit : 1; + // This thread's item: one 16-byte vector of one row slice. + int const itemRow = tid / kVecPerSlice; + int const itemVec = tid % kVecPerSlice; + bool const hasItem = itemRow < numRowsCta; + int const row = rowGroup + itemRow * kRowStride; + // The rank's own partial of a row slice is published by exactly one CTA of the grid. + bool const sends = hasItem && (!kRouter || itemRow % (kG > 0 ? kG : 1) == expertGroup); + size_t const rowOff = static_cast(row) * kHiddenDim + sliceBase + itemVec * kEltsPerVec; + // The cluster that writes the normalized rows and the residual. + bool const writesOutputs = !kRouter || expertGroup == 0; + + // ---- Prologue: constant model weights only; may overlap the producer of `input` under PDL. ---- + if (tid < kVecPerSlice) + { + hop.gamma[tid] = loadConstant(reinterpret_cast(params.gamma) + slice * kVecPerSlice + tid); + } + [[maybe_unused]] int const gid = lane >> 2; + [[maybe_unused]] int const tig = lane & 3; + [[maybe_unused]] uint4 wLo[kRouter ? HopConfig<(kG > 0 ? kG : 4), (kG > 0 ? kRowSplit : 2)>::kPairsPerGroup : 1]; + [[maybe_unused]] uint4 wHi[kRouter ? HopConfig<(kG > 0 ? kG : 4), (kG > 0 ? kRowSplit : 2)>::kPairsPerGroup : 1]; + if constexpr (kRouter) + { + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + auto const* w = reinterpret_cast<__nv_bfloat16 const*>(params.routerWeight); + int const expert = expertGroup * Cfg::kExpertsPerCluster + tile * 16 + gid; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + int const k = sliceBase + kGroup * Cfg::kKPerGroup + p * 32 + tig * kEltsPerVec; + wLo[p] = loadConstant(w + static_cast(expert) * kHiddenDim + k); + wHi[p] = loadConstant(w + static_cast(expert + 8) * kHiddenDim + k); + } + // MMA columns of rows this CTA does not own stay zero. + for (int i = tid; i < kMaxRows * kNormedStride; i += Cfg::kThreads) + { + rs.normed[i / kNormedStride][i % kNormedStride] = make_uint4(0, 0, 0, 0); + } + } + + // Cluster-local completion barriers: every CTA of a cluster has the same rows, so each expects the 8 slice sums + // of its rows and, with the router, the 8 x kKGroups partial tiles of the experts it owns. A split cluster + // barrier publishes the initialization; its wait precedes the first remote store, long after all CTAs arrived. + if (tid == 0) + { + mbarrierInitExpectTx(&hop.normBar, static_cast(numRowsCta * kSlices * sizeof(float))); + if constexpr (kRouter) + { + mbarrierInitExpectTx(&hop.routerBar, numRowsCta > 0 ? static_cast(sizeof(rs.partial)) : 0U); + } + asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory"); + } + asm volatile("barrier.cluster.arrive.release.aligned;" ::: "memory"); + + // ---- Wait for the producer of `input` (and transitively of `residualIn` and the protocol state). ---- + W2_STAMP(0); + asm volatile("griddepcontrol.wait;" ::: "memory"); + W2_STAMP(1); + // Dependents only start their own prologue; they still wait for this grid to complete. + asm volatile("griddepcontrol.launch_dependents;"); + + uint4 const flags = loadVolatile(params.flags); + auto const* input = reinterpret_cast<__nv_bfloat16 const*>(params.input); + auto const* residualIn = reinterpret_cast<__nv_bfloat16 const*>(params.residualIn); + uint4 res = make_uint4(0, 0, 0, 0); + // This rank's own partial, as published (sanitized); it is used from registers, not from the multicast copy. + uint4 own = make_uint4(0, 0, 0, 0); + if (hasItem) + { + res = *reinterpret_cast(residualIn + rowOff); + own = sanitize(*reinterpret_cast(input + rowOff)); + } + uint32_t const cur = flags.x; + uint32_t const clearBytes = flags.y; + char* ucBase = reinterpret_cast(params.ucBuffer); + auto* curUc = reinterpret_cast<__nv_bfloat16*>(ucBase + cur * kLamportBufferBytes); + auto* curMc + = reinterpret_cast<__nv_bfloat16*>(reinterpret_cast(params.mcBuffer) + cur * kLamportBufferBytes); + // Lamport layout of one buffer: [row][source rank][kHiddenDim]. + size_t const slotBase = static_cast(row) * kNumRanks * kHiddenDim + sliceBase + itemVec * kEltsPerVec; + + // ---- Publish this rank's partial to every rank (one multicast store per vector). ---- + if (sends) + { + *reinterpret_cast(curMc + slotBase + static_cast(params.rank) * kHiddenDim) = own; + } + + W2_STAMP(2); + // ---- Collect the other ranks' partials. ---- + uint4 vals[kNumRanks]; + if (hasItem) + { +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + vals[s] = s == params.rank ? own : loadVolatile(curUc + slotBase + static_cast(s) * kHiddenDim); + } + } + + // ---- Reset the previous call's buffer to the sentinel (reused two calls from now), off the poll's path. ---- + { + uint32_t const dirty = (cur + kNumLamportBuffers - 1) % kNumLamportBuffers; + auto* dirtyBuf = reinterpret_cast(ucBase + dirty * kLamportBufferBytes); + uint32_t const numVec = clearBytes / sizeof(uint4); + uint4 const sentinel = make_uint4(kLamportSentinel, kLamportSentinel, kLamportSentinel, kLamportSentinel); + uint32_t const ctaLinear = clusterId * kSlices + slice; + for (uint32_t j = ctaLinear * Cfg::kThreads + tid; j < numVec; j += kGridCtas * Cfg::kThreads) + { + dirtyBuf[j] = sentinel; + } + } + + if (hasItem) + { + bool pending = true; + while (pending) + { + pending = false; +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + if (isDirty(vals[s])) + { + pending = true; + vals[s] = loadVolatile(curUc + slotBase + static_cast(s) * kHiddenDim); + } + } + } + } + + W2_STAMP(3); + // ---- Reduce in rank order, add the residual, and form per-vector sums of squares (native arithmetic). ---- + uint4 resOut = make_uint4(0, 0, 0, 0); + if (hasItem) + { + float acc[kEltsPerVec]; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + acc[e] = 0.F; + } +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + BfVec x; + x.packed = vals[s]; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + acc[e] += __bfloat162float(x.elts[e]); + } + } + BfVec rv; + rv.packed = res; + BfVec y; + float sq = 0.F; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + y.elts[e] = __hadd(__float2bfloat16(acc[e]), rv.elts[e]); + sq += __bfloat162float(__hmul(y.elts[e], y.elts[e])); + } + resOut = y.packed; + hop.vecSq[itemRow][itemVec] = sq; + } + __syncthreads(); + if (tid == 0) + { + // Every thread of this CTA has read the protocol state; CTA 0 advances it only after all CTAs report. + asm volatile("red.release.gpu.global.add.u32 [%0], %1;" ::"l"(params.flags + 2), "r"(1U) : "memory"); + } + + // ---- RMS: warp butterfly over 32 vectors, then (w0 + w2) + w1 per slice, then the 8 slices in order. ---- + if (warp < numRowsCta * kNativeWarpsPerSlice) + { + int const j = warp / kNativeWarpsPerSlice; + int const q = warp % kNativeWarpsPerSlice; + float val = hop.vecSq[j][q * 32 + lane]; +#pragma unroll + for (int mask = 16; mask > 0; mask >>= 1) + { + val = val + __shfl_xor_sync(0xffffffffU, val, mask, 32); + } + if (lane == 0) + { + hop.warpSq[j][q] = val; + } + } + __syncthreads(); + asm volatile("barrier.cluster.wait.acquire.aligned;" ::: "memory"); + if (tid < numRowsCta * kSlices) + { + int const j = tid / kSlices; + int const dst = tid % kSlices; + float const blockSum = (hop.warpSq[j][0] + hop.warpSq[j][2]) + hop.warpSq[j][1]; + storeAsync( + clusterAddr(sharedAddr(&hop.blockSq[j][slice]), dst), blockSum, clusterAddr(sharedAddr(&hop.normBar), dst)); + } + W2_STAMP(4); + mbarrierWaitPhase0(&hop.normBar); + W2_STAMP(5); + + if (hasItem) + { + float fullSum = 0.F; +#pragma unroll + for (int c = 0; c < kSlices; ++c) + { + fullSum += hop.blockSq[itemRow][c]; + } + float const rcpRms = rsqrtf(fullSum / static_cast(kHiddenDim) + params.eps); + BfVec y; + y.packed = resOut; + BfVec g; + g.packed = hop.gamma[itemVec]; + BfVec n; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + n.elts[e] = __float2bfloat16(__bfloat162float(y.elts[e]) * rcpRms * __bfloat162float(g.elts[e])); + } + if (writesOutputs) + { + *reinterpret_cast(reinterpret_cast<__nv_bfloat16*>(params.normOut) + rowOff) = n.packed; + *reinterpret_cast(reinterpret_cast<__nv_bfloat16*>(params.residualOut) + rowOff) = y.packed; + } + if constexpr (kRouter) + { + rs.normed[itemRow][itemVec] = n.packed; + } + } + + if constexpr (kRouter) + { + if (numRowsCta > 0) + { + __syncthreads(); + W2_STAMP(6); + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + // Independent accumulator chains keep the MMA latency off a single dependency chain. + float acc[kMmaChains][4] = {}; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + // One 16-byte load covers two k16 steps; the k-permutation inside a step is the same for both operands. + uint4 const b = rs.normed[gid][kGroup * (Cfg::kKPerGroup / kEltsPerVec) + p * 4 + tig]; + int const chain = (2 * p) % kMmaChains; + mmaBf16(acc[chain], wLo[p].x, wHi[p].x, wLo[p].y, wHi[p].y, b.x, b.y); + mmaBf16(acc[chain + 1], wLo[p].z, wHi[p].z, wLo[p].w, wHi[p].w, b.z, b.w); + } + float sum4[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) + { + sum4[i] = (acc[0][i] + acc[1][i]) + (acc[2][i] + acc[3][i]); + } + // Expert e (cluster-local) is owned by CTA e / kExpertsPerOwner. + int const e0 = tile * 16 + gid; + int const e1 = e0 + 8; + uint32_t const owner0 = e0 / Cfg::kExpertsPerOwner; + uint32_t const owner1 = e1 / Cfg::kExpertsPerOwner; + storeAsync(clusterAddr(sharedAddr(&rs.partial[slice][kGroup][e0 % Cfg::kExpertsPerOwner][tig * 2]), owner0), + sum4[0], sum4[1], clusterAddr(sharedAddr(&hop.routerBar), owner0)); + storeAsync(clusterAddr(sharedAddr(&rs.partial[slice][kGroup][e1 % Cfg::kExpertsPerOwner][tig * 2]), owner1), + sum4[2], sum4[3], clusterAddr(sharedAddr(&hop.routerBar), owner1)); + W2_STAMP(7); + // Also keeps this CTA resident until every partial addressed to it has landed. + mbarrierWaitPhase0(&hop.routerBar); + W2_STAMP(8); + + // This CTA owns experts [slice * kExpertsPerOwner, (slice + 1) * kExpertsPerOwner) of the cluster. Lane + // groups of 8 sum over the 8 slices. + if (tid < Cfg::kExpertsPerOwner * numRowsCta * kSlices) + { + int const c = tid % kSlices; + int const ij = tid / kSlices; + int const ei = ij / numRowsCta; + int const j = ij % numRowsCta; + float sum = 0.F; +#pragma unroll + for (int kg = 0; kg < Cfg::kKGroups; ++kg) + { + sum += rs.partial[c][kg][ei][j]; + } +#pragma unroll + for (int mask = kSlices / 2; mask > 0; mask >>= 1) + { + sum = sum + __shfl_xor_sync(0xffffffffU, sum, mask, kSlices); + } + if (c == 0) + { + int const r = rowGroup + j * kRowStride; + params.routerLogits[r * kNumExperts + expertGroup * Cfg::kExpertsPerCluster + + slice * Cfg::kExpertsPerOwner + ei] + = sum; + } + } + } + } + + W2_STAMP(9); + // ---- CTA 0 advances the protocol state once every CTA has read it. ---- + if (tid == 0 && slice == 0 && clusterId == 0) + { + uint32_t arrived = 0; + do + { + asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(arrived) : "l"(params.flags + 2) : "memory"); + } while (arrived < static_cast(kGridCtas)); + uint4 const next = make_uint4((cur + 1) % kNumLamportBuffers, + static_cast(R * kNumRanks * kHiddenDim * sizeof(__nv_bfloat16)), 0U, 0U); + asm volatile("st.relaxed.gpu.global.v4.u32 [%0], {%1, %2, %3, %4};" ::"l"(params.flags), "r"(next.x), + "r"(next.y), "r"(next.z), "r"(next.w) + : "memory"); + } +#else + // Clusters, distributed shared memory and PDL need SM90+; the launcher is only reached on supported devices. + __trap(); +#endif +} + +template +cudaError_t launchTyped(PostAttnHopParams const& params) +{ + using Cfg = HopConfig; + cudaLaunchAttribute attrs[2]; + attrs[0].id = cudaLaunchAttributeClusterDimension; + attrs[0].val.clusterDim.x = kSlices; + attrs[0].val.clusterDim.y = 1; + attrs[0].val.clusterDim.z = 1; + attrs[1].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[1].val.programmaticStreamSerializationAllowed = params.enablePdl ? 1 : 0; + + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kSlices, kG > 0 ? kG * kRowSplit : R, 1); + config.blockDim = dim3(Cfg::kThreads, 1, 1); + config.dynamicSmemBytes = 0; + config.stream = params.stream; + config.attrs = attrs; + config.numAttrs = 2; + return cudaLaunchKernelEx(&config, postAttnHopKernel, params); +} + +template +cudaError_t launchRows(PostAttnHopParams const& params) +{ + switch (params.numRows) + { + case 1: return launchTyped<1, kG, kRowSplit>(params); + case 2: return launchTyped<2, kG, kRowSplit>(params); + case 3: return launchTyped<3, kG, kRowSplit>(params); + case 4: return launchTyped<4, kG, kRowSplit>(params); + case 5: return launchTyped<5, kG, kRowSplit>(params); + case 6: return launchTyped<6, kG, kRowSplit>(params); + case 7: return launchTyped<7, kG, kRowSplit>(params); + case 8: return launchTyped<8, kG, kRowSplit>(params); + default: return cudaErrorInvalidValue; + } +} + +// Router hop: 4 expert groups x min(R, 3) row groups (at most 12 clusters of 8 CTAs, each CTA <= 3 row slices). +cudaError_t launchRouter(PostAttnHopParams const& params) +{ + switch (params.numRows) + { + case 1: return launchTyped<1, 4, 1>(params); + case 2: return launchTyped<2, 4, 2>(params); + case 3: return launchTyped<3, 4, 3>(params); + case 4: return launchTyped<4, 4, 3>(params); + case 5: return launchTyped<5, 4, 3>(params); + case 6: return launchTyped<6, 4, 3>(params); + case 7: return launchTyped<7, 4, 3>(params); + case 8: return launchTyped<8, 4, 3>(params); + default: return cudaErrorInvalidValue; + } +} + +} // namespace + +#ifdef TLLM_GLM_LL_W2_TIMING +cudaError_t copyPostAttnHopTiming(unsigned long long* dst, cudaStream_t stream) +{ + return cudaMemcpyFromSymbolAsync(dst, gW2Timing, sizeof(gW2Timing), 0, cudaMemcpyDeviceToHost, stream); +} +#endif + +cudaError_t invokePostAttentionHop(PostAttnHopParams const& params) +{ + if (params.rank < 0 || params.rank >= kNumRanks || params.flags == nullptr || params.ucBuffer == nullptr + || params.mcBuffer == nullptr) + { + return cudaErrorInvalidValue; + } + if (params.routerWeight != nullptr) + { + if (params.routerLogits == nullptr) + { + return cudaErrorInvalidValue; + } + return launchRouter(params); + } + return launchRows<0, 1>(params); +} + +} // namespace kernels::glm_moe_dsa::post_attention_hop + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.h new file mode 100644 index 000000000000..ee7c62202f5b --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.h @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::post_attention_hop +{ + +//! Fused post-attention hop. +//! +//! For ``numRows`` rows of hidden size ``kHiddenDim`` on a TP group of ``kNumRanks`` ranks it computes, on every +//! rank and with identical bits on every rank: +//! sum = bf16(sum over ranks 0..7 of float(partial_rank)) (one-shot Lamport exchange, fixed order) +//! residual = sum + residualIn (bf16 add) +//! normed = bf16(float(residual) * rsqrt(mean(bf16(residual^2)) + eps) * float(gamma)) +//! logits = normed @ routerWeight^T in FP32 (optional, [numRows, kNumExperts]) +//! ``sum``, ``residual`` and ``normed`` follow the arithmetic and reduction tree of the native MNNVL one-shot +//! AR + residual + RMSNorm kernel, so they are bit-identical to it. +//! +//! The exchange uses a private multicast workspace of ``kNumLamportBuffers`` rotating buffers. ``flags`` holds the +//! device-side protocol state ``{current buffer, bytes to clear in the previous buffer, arrival counter, unused}`` +//! and must be zero-initialized together with the buffers filled with the Lamport sentinel (all 32-bit words equal +//! to 0x80000000) on every rank before the first call. +struct PostAttnHopParams +{ + void const* input = nullptr; //!< [numRows, kHiddenDim] bf16, rank-local partial (o_proj output) + void const* residualIn = nullptr; //!< [numRows, kHiddenDim] bf16 + void const* gamma = nullptr; //!< [kHiddenDim] bf16 RMSNorm weight + void const* routerWeight = nullptr; //!< [kNumExperts, kHiddenDim] bf16 or nullptr (no router logits) + void* normOut = nullptr; //!< [numRows, kHiddenDim] bf16 + void* residualOut = nullptr; //!< [numRows, kHiddenDim] bf16 + float* routerLogits = nullptr; //!< [numRows, kNumExperts] fp32, required iff routerWeight != nullptr + void* ucBuffer = nullptr; //!< local unicast view of the multicast workspace + void* mcBuffer = nullptr; //!< multicast address of the same workspace + uint32_t* flags = nullptr; //!< [4] device-side protocol state of this workspace + int numRows = 0; + int rank = 0; + float eps = 1e-5F; + bool enablePdl = true; + cudaStream_t stream = nullptr; +}; + +constexpr int kHiddenDim = 6144; +constexpr int kNumRanks = 8; +constexpr int kNumExperts = 256; +constexpr int kMaxRows = 8; +constexpr int kNumLamportBuffers = 3; + +//! Bytes of one Lamport buffer: [kMaxRows][kNumRanks][kHiddenDim] bf16. +constexpr size_t kLamportBufferBytes = size_t(kMaxRows) * kNumRanks * kHiddenDim * 2; +//! Bytes of the whole multicast workspace. +constexpr size_t kWorkspaceBytes = kLamportBufferBytes * kNumLamportBuffers; + +//! Launch the fused hop on ``params.stream``. Returns the launch status. +cudaError_t invokePostAttentionHop(PostAttnHopParams const& params); + +#ifdef TLLM_GLM_LL_W2_TIMING +//! Development builds only: phase timestamps of the most recent launch, [kTimingMaxCtas][kTimingStamps] ns. +constexpr int kTimingMaxCtas = 128; +constexpr int kTimingStamps = 10; +cudaError_t copyPostAttnHopTiming(unsigned long long* dst, cudaStream_t stream); +#endif + +} // namespace kernels::glm_moe_dsa::post_attention_hop + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.cu new file mode 100644 index 000000000000..17c81e24877a --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.cu @@ -0,0 +1,707 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.h" + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::attention_output_into_hop +{ +namespace +{ +namespace cg = cooperative_groups; + +// The post-attention hop (glmMoeDsa/postAttentionHop.cu) with its input taken from the transport: the only differences +// are marked "Transport" below; the work decomposition, the arithmetic and the protocol are that hop's. +// +// Work decomposition. +// +// Every cluster has kSlices CTAs; CTA c owns slice [c * kSlice, (c + 1) * kSlice), the slice a CTA of the native +// one-shot kernel owns for numRows <= 8 (96 threads x 8 elements, 8 CTAs per row), so the RMSNorm reduction below +// reproduces the native tree exactly. +// +// Without router logits (dense layers) the layout is the native one: cluster y handles row y, and every CTA collects +// one row slice (8 ranks x 768 elements). +// +// With router logits there are kG x kRowSplit clusters (kG = 4, kRowSplit = min(R, 3): at most 12 clusters, which stay +// co-resident). Cluster (g, h) collects and normalizes the rows r with r % kRowSplit == h, so a CTA reads at most +// ceil(R / kRowSplit) row slices, and computes the logits of those rows for experts [64 g, 64 g + 64). Its router +// weights live in registers (loaded before the PDL wait). Each CTA multiplies its rows of slice c by the experts' +// weights of slice c with BF16 MMAs (experts on m16, rows on n8) and pushes the partial logits to the CTA that owns the +// expert; owners add the 8 slices in a fixed order, so the logits are identical on every rank. Clusters never +// synchronize with each other; inside a cluster, slice sums and partial logits travel as st.async stores that complete +// transactions on the consumer's mbarrier. +constexpr int kSlices = 8; +constexpr int kSlice = kHiddenDim / kSlices; +constexpr int kEltsPerVec = 8; +constexpr int kVecPerSlice = kSlice / kEltsPerVec; +constexpr int kNativeWarpsPerSlice = kVecPerSlice / 32; +// Row stride of the normalized-slice tile in 16-byte vectors; 100 = 96 + 4 keeps the 16-byte B-fragment loads of two +// rows in one quarter-warp phase on disjoint banks. +constexpr int kNormedStride = kVecPerSlice + 4; +constexpr int kMmaChains = 4; +constexpr uint32_t kLamportSentinel = 0x80000000U; + +static_assert(kHiddenDim % (kSlices * kEltsPerVec * 32) == 0); +static_assert(kVecPerSlice == kNativeWarpsPerSlice * 32); +static_assert(kMaxRows == 8, "the router MMA maps rows onto the n8 dimension"); + +// kG = number of expert groups (0: hop without router logits); kRowSplit = number of row groups. +template +struct HopConfig +{ + static constexpr int kThreads = 384; + static constexpr int kWarps = kThreads / 32; + static constexpr int kExpertsPerCluster = kNumExperts / kG; + static constexpr int kExpertTiles = kExpertsPerCluster / 16; + static constexpr int kKGroups = kWarps / kExpertTiles; + static constexpr int kKPerGroup = kSlice / kKGroups; + static constexpr int kPairsPerGroup = kKPerGroup / 32; + static constexpr int kExpertsPerOwner = kExpertsPerCluster / kSlices; + static_assert(kExpertsPerCluster % 16 == 0 && kWarps % kExpertTiles == 0 && kSlice % (kKGroups * 32) == 0); + static_assert(kPairsPerGroup % (kMmaChains / 2) == 0); +}; + +template +struct HopConfig<0, kRowSplit> +{ + static constexpr int kThreads = 128; + static constexpr int kWarps = kThreads / 32; + static constexpr int kMaxRowsPerCta = 1; + static_assert(kMaxRowsPerCta * kVecPerSlice <= kThreads && kMaxRowsPerCta * kNativeWarpsPerSlice <= kWarps); +}; + +constexpr int kMaxRowsPerCtaAll = kMaxRows / 2; + +struct alignas(16) HopStorage +{ + uint4 gamma[kVecPerSlice]; + float vecSq[kMaxRowsPerCtaAll][kVecPerSlice]; + float warpSq[kMaxRowsPerCtaAll][kNativeWarpsPerSlice]; + float blockSq[kMaxRowsPerCtaAll][kSlices]; + // Completes when all slice sums of this CTA's rows have arrived (st.async complete_tx). + alignas(8) uint64_t normBar; + // Completes when all partial logits of the experts this CTA owns have arrived. + alignas(8) uint64_t routerBar; +}; + +template +struct alignas(16) RouterStorage +{ + using Cfg = HopConfig; + uint4 normed[kMaxRows][kNormedStride]; // local row on the MMA n dimension; unused rows stay zero + float partial[kSlices][Cfg::kKGroups][Cfg::kExpertsPerOwner][kMaxRows]; +}; + +struct EmptyStorage +{ +}; + +union BfVec +{ + uint4 packed; + __nv_bfloat16 elts[kEltsPerVec]; +}; + +__device__ __forceinline__ uint4 loadVolatile(void const* ptr) +{ + uint4 v; + asm volatile("ld.volatile.global.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) + : "l"(ptr) + : "memory"); + return v; +} + +__device__ __forceinline__ uint4 loadConstant(void const* ptr) +{ + uint4 v; + asm volatile("ld.global.nc.v4.u32 {%0, %1, %2, %3}, [%4];" : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) : "l"(ptr)); + return v; +} + +__device__ __forceinline__ void prefetchL2(void const* ptr) +{ + asm volatile("prefetch.global.L2 [%0];" ::"l"(ptr)); +} + +__device__ __forceinline__ bool isDirty(uint4 const& v) +{ + return v.x == kLamportSentinel || v.y == kLamportSentinel || v.z == kLamportSentinel || v.w == kLamportSentinel; +} + +// Transport: the producer writes the Lamport payloads already sanitized (bf16 -0.0 mapped to +0.0), so they never +// contain the sentinel word. + +__device__ __forceinline__ uint32_t sharedAddr(void const* ptr) +{ + return static_cast(__cvta_generic_to_shared(ptr)); +} + +// Address of the same shared-memory object in CTA `rank` of the cluster. +__device__ __forceinline__ uint32_t clusterAddr(uint32_t addr, uint32_t rank) +{ + uint32_t out; + asm volatile("mapa.shared::cluster.u32 %0, %1, %2;" : "=r"(out) : "r"(addr), "r"(rank)); + return out; +} + +__device__ __forceinline__ void mbarrierInitExpectTx(uint64_t* bar, uint32_t bytes) +{ + asm volatile("mbarrier.init.shared::cta.b64 [%0], 1;" ::"r"(sharedAddr(bar)) : "memory"); + asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;" ::"r"(sharedAddr(bar)), "r"(bytes) + : "memory"); +} + +__device__ __forceinline__ void mbarrierWaitPhase0(uint64_t* bar) +{ + asm volatile( + "{\n" + ".reg .pred done;\n" + "HOP_MBAR_WAIT_%=:\n" + "mbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 done, [%0], 0;\n" + "@!done bra HOP_MBAR_WAIT_%=;\n" + "}\n" ::"r"(sharedAddr(bar)) + : "memory"); +} + +// Remote shared-memory stores that complete transactions on the destination CTA's mbarrier. +__device__ __forceinline__ void storeAsync(uint32_t remoteAddr, float v, uint32_t remoteBar) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.f32 [%0], %1, [%2];" ::"r"(remoteAddr), "f"(v), + "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void storeAsync(uint32_t remoteAddr, float a, float b, uint32_t remoteBar) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.v2.f32 [%0], {%1, %2}, [%3];" ::"r"(remoteAddr), + "f"(a), "f"(b), "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void mmaBf16( + float (&d)[4], uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t b0, uint32_t b1) +{ + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 {%0, %1, %2, %3}, {%4, %5, %6, %7}, {%8, %9}, " + "{%0, %1, %2, %3};" + : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b0), "r"(b1)); +} + +// Transport: one warp's router-weight MMA fragments (the post-attention hop's register layout): experts +// tile * 16 + gid (+ 8) of this cluster's expert group, k pairs of this warp's k group within the CTA's slice. +template +__device__ __forceinline__ void loadRouterFragments(uint4 (&wLo)[Cfg::kPairsPerGroup], + uint4 (&wHi)[Cfg::kPairsPerGroup], void const* routerWeight, int expertGroup, int warp, int gid, int tig, + int sliceBase) +{ + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + auto const* w = reinterpret_cast<__nv_bfloat16 const*>(routerWeight); + int const expert = expertGroup * Cfg::kExpertsPerCluster + tile * 16 + gid; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + int const k = sliceBase + kGroup * Cfg::kKPerGroup + p * 32 + tig * kEltsPerVec; + wLo[p] = loadConstant(w + static_cast(expert) * kHiddenDim + k); + wHi[p] = loadConstant(w + static_cast(expert + 8) * kHiddenDim + k); + } +} + +// Transport: where the router-weight fragments are loaded (all orders give the same values and arithmetic): +// kRouterAfterBarrier: after the barrier that ends the rank sum, overlapping the RMS tree and cluster exchange; +// kRouterAfterSum: right after the rank sum, before that barrier (the default; the loads go through +// loadRouterFragments so the rank sum is not sunk below them and the kernel stays at +// few enough registers to share an SM with a producer CTA); +// kRouterBeforeMma: right before the MMAs (fewest registers, the load latency follows the RMSNorm). +constexpr int kRouterAfterBarrier = 0; +constexpr int kRouterAfterSum = 1; +constexpr int kRouterBeforeMma = 2; + +template +__global__ void __launch_bounds__(HopConfig::kThreads, 1) + postAttnHopX1Kernel(PostAttentionHopFromPartialsParams const params) +{ + static_assert(R >= 1 && R <= kMaxRows); +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + constexpr bool kRouter = kG > 0; + using Cfg = HopConfig; + constexpr int kClusters = kRouter ? kG * kRowSplit : R; + constexpr int kGridCtas = kClusters * kSlices; + // Rows one CTA collects, normalizes and multiplies. + constexpr int kRowsPerCta = kRouter ? (R + kRowSplit - 1) / kRowSplit : 1; + static_assert(kRowsPerCta <= kMaxRowsPerCtaAll); + static_assert(kRowsPerCta * kVecPerSlice <= Cfg::kThreads && kRowsPerCta * kNativeWarpsPerSlice <= Cfg::kWarps); + if constexpr (kRouter) + { + static_assert( + HopConfig<(kG > 0 ? kG : 4), kRowSplit>::kExpertsPerOwner * kRowsPerCta * kSlices <= Cfg::kThreads); + } + + __shared__ HopStorage hop; + __shared__ std::conditional_t 0 ? kG : 4), (kG > 0 ? kRowSplit : 2)>, EmptyStorage> rs; + + cg::cluster_group cluster = cg::this_cluster(); + int const slice = static_cast(cluster.block_rank()); + int const clusterId = blockIdx.y; + int const expertGroup = kRouter ? clusterId % (kG > 0 ? kG : 1) : 0; + int const rowGroup = kRouter ? clusterId / (kG > 0 ? kG : 1) : clusterId; + int const tid = threadIdx.x; + int const lane = tid & 31; + int const warp = tid >> 5; + int const sliceBase = slice * kSlice; + + // Rows this CTA collects and normalizes: local row j is global row rowGroup + j * rowStride. + constexpr int kRowStride = kRouter ? kRowSplit : 1; + int const numRowsCta = kRouter ? (R - rowGroup + kRowSplit - 1) / kRowSplit : 1; + // This thread's item: one 16-byte vector of one row slice. + int const itemRow = tid / kVecPerSlice; + int const itemVec = tid % kVecPerSlice; + bool const hasItem = itemRow < numRowsCta; + int const row = rowGroup + itemRow * kRowStride; + size_t const rowOff = static_cast(row) * kHiddenDim + sliceBase + itemVec * kEltsPerVec; + // The cluster that writes the normalized rows and the residual. + bool const writesOutputs = !kRouter || expertGroup == 0; + + // ---- Prologue: constant model weights only; may overlap the producer of `input` under PDL. ---- + if (tid < kVecPerSlice) + { + hop.gamma[tid] = loadConstant(reinterpret_cast(params.gamma) + slice * kVecPerSlice + tid); + } + [[maybe_unused]] int const gid = lane >> 2; + [[maybe_unused]] int const tig = lane & 3; + // Transport: the router-weight fragments are loaded after the poll (not here), so they are not live across it; with + // fewer registers a CTA of this kernel can be resident next to a CTA of the producer. The prologue only asks L2 + // for the same lines (no registers held, no architectural effect). + if constexpr (kRouter) + { + { + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + auto const* w = reinterpret_cast<__nv_bfloat16 const*>(params.routerWeight); + int const expert = expertGroup * Cfg::kExpertsPerCluster + tile * 16 + gid; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + int const k = sliceBase + kGroup * Cfg::kKPerGroup + p * 32 + tig * kEltsPerVec; + prefetchL2(w + static_cast(expert) * kHiddenDim + k); + prefetchL2(w + static_cast(expert + 8) * kHiddenDim + k); + } + } + // MMA columns of rows this CTA does not own stay zero. + for (int i = tid; i < kMaxRows * kNormedStride; i += Cfg::kThreads) + { + rs.normed[i / kNormedStride][i % kNormedStride] = make_uint4(0, 0, 0, 0); + } + } + + // Cluster-local completion barriers: every CTA of a cluster has the same rows, so each expects the 8 slice sums + // of its rows and, with the router, the 8 x kKGroups partial tiles of the experts it owns. A split cluster + // barrier publishes the initialization; its wait precedes the first remote store, long after all CTAs arrived. + if (tid == 0) + { + mbarrierInitExpectTx(&hop.normBar, static_cast(numRowsCta * kSlices * sizeof(float))); + if constexpr (kRouter) + { + mbarrierInitExpectTx(&hop.routerBar, numRowsCta > 0 ? static_cast(sizeof(rs.partial)) : 0U); + } + asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory"); + } + asm volatile("barrier.cluster.arrive.release.aligned;" ::: "memory"); + + // Transport: with earlyPoll no wait here. The producer read the protocol state and wrote the transport after its + // own griddepcontrol.wait and triggered this grid only after that wait, so every grid before the producer has + // completed and the protocol state written by the previous hop is final (see postAttentionHopFromPartials.h). + auto const* residualIn = reinterpret_cast<__nv_bfloat16 const*>(params.residualIn); + uint4 res = make_uint4(0, 0, 0, 0); + if (!params.earlyPoll) + { + asm volatile("griddepcontrol.wait;" ::: "memory"); + // As in the post-attention hop: dependents may start their prologue; they still wait for this grid to complete. + asm volatile("griddepcontrol.launch_dependents;"); + // Transport: after the wait the residual is final; its load overlaps the poll instead of following it. + if (hasItem) + { + res = *reinterpret_cast(residualIn + rowOff); + } + } + uint4 const flags = loadVolatile(params.flags); + uint32_t const cur = flags.x; + uint32_t const clearBytes = flags.y; + char* ucBase = reinterpret_cast(params.ucBuffer); + auto* curUc = reinterpret_cast<__nv_bfloat16*>(ucBase + cur * kLamportBufferBytes); + // Lamport layout of one buffer: [row][source rank][kHiddenDim]. + size_t const slotBase = static_cast(row) * kNumRanks * kHiddenDim + sliceBase + itemVec * kEltsPerVec; + + // ---- Transport: collect every rank's partial, this rank's own included, from the transport. ---- + uint4 vals[kNumRanks]; + if (hasItem) + { +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + vals[s] = loadVolatile(curUc + slotBase + static_cast(s) * kHiddenDim); + } + } + + // ---- Reset the previous call's buffer to the sentinel (reused two calls from now), off the poll's path. ---- + { + uint32_t const dirty = (cur + kNumLamportBuffers - 1) % kNumLamportBuffers; + auto* dirtyBuf = reinterpret_cast(ucBase + dirty * kLamportBufferBytes); + uint32_t const numVec = clearBytes / sizeof(uint4); + uint4 const sentinel = make_uint4(kLamportSentinel, kLamportSentinel, kLamportSentinel, kLamportSentinel); + uint32_t const ctaLinear = clusterId * kSlices + slice; + for (uint32_t j = ctaLinear * Cfg::kThreads + tid; j < numVec; j += kGridCtas * Cfg::kThreads) + { + dirtyBuf[j] = sentinel; + } + } + + if (hasItem) + { + bool pending = true; + while (pending) + { + pending = false; +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + if (isDirty(vals[s])) + { + pending = true; + vals[s] = loadVolatile(curUc + slotBase + static_cast(s) * kHiddenDim); + } + } + } + } + + // ---- Transport (earlyPoll): wait for the producer grid before reading the residual or writing any output. ---- + if (params.earlyPoll) + { + asm volatile("griddepcontrol.wait;" ::: "memory"); + // Dependents only start their own prologue; they still wait for this grid to complete. + asm volatile("griddepcontrol.launch_dependents;"); + if (hasItem) + { + res = *reinterpret_cast(residualIn + rowOff); + } + } + + // Transport: router-weight fragments (see kRouterLoad). + constexpr int kWPairs = kRouter ? HopConfig<(kG > 0 ? kG : 4), kRowSplit>::kPairsPerGroup : 1; + [[maybe_unused]] uint4 wLo[kWPairs]; + [[maybe_unused]] uint4 wHi[kWPairs]; + + // ---- Reduce in rank order, add the residual, and form per-vector sums of squares (native arithmetic). ---- + uint4 resOut = make_uint4(0, 0, 0, 0); + if (hasItem) + { + float acc[kEltsPerVec]; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + acc[e] = 0.F; + } +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + BfVec x; + x.packed = vals[s]; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + acc[e] += __bfloat162float(x.elts[e]); + } + } + BfVec rv; + rv.packed = res; + BfVec y; + float sq = 0.F; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + y.elts[e] = __hadd(__float2bfloat16(acc[e]), rv.elts[e]); + sq += __bfloat162float(__hmul(y.elts[e], y.elts[e])); + } + resOut = y.packed; + hop.vecSq[itemRow][itemVec] = sq; + } + if constexpr (kRouter && kRouterLoad == kRouterAfterSum) + { + if (numRowsCta > 0) + { + loadRouterFragments(wLo, wHi, params.routerWeight, expertGroup, warp, gid, tig, sliceBase); + } + } + __syncthreads(); + if (tid == 0) + { + // Every thread of this CTA has read the protocol state; CTA 0 advances it only after all CTAs report. + asm volatile("red.release.gpu.global.add.u32 [%0], %1;" ::"l"(params.flags + 2), "r"(1U) : "memory"); + } + if constexpr (kRouter && kRouterLoad == kRouterAfterBarrier) + { + if (numRowsCta > 0) + { + loadRouterFragments(wLo, wHi, params.routerWeight, expertGroup, warp, gid, tig, sliceBase); + } + } + + // ---- RMS: warp butterfly over 32 vectors, then (w0 + w2) + w1 per slice, then the 8 slices in order. ---- + if (warp < numRowsCta * kNativeWarpsPerSlice) + { + int const j = warp / kNativeWarpsPerSlice; + int const q = warp % kNativeWarpsPerSlice; + float val = hop.vecSq[j][q * 32 + lane]; +#pragma unroll + for (int mask = 16; mask > 0; mask >>= 1) + { + val = val + __shfl_xor_sync(0xffffffffU, val, mask, 32); + } + if (lane == 0) + { + hop.warpSq[j][q] = val; + } + } + __syncthreads(); + asm volatile("barrier.cluster.wait.acquire.aligned;" ::: "memory"); + if (tid < numRowsCta * kSlices) + { + int const j = tid / kSlices; + int const dst = tid % kSlices; + float const blockSum = (hop.warpSq[j][0] + hop.warpSq[j][2]) + hop.warpSq[j][1]; + storeAsync( + clusterAddr(sharedAddr(&hop.blockSq[j][slice]), dst), blockSum, clusterAddr(sharedAddr(&hop.normBar), dst)); + } + mbarrierWaitPhase0(&hop.normBar); + + if (hasItem) + { + float fullSum = 0.F; +#pragma unroll + for (int c = 0; c < kSlices; ++c) + { + fullSum += hop.blockSq[itemRow][c]; + } + float const rcpRms = rsqrtf(fullSum / static_cast(kHiddenDim) + params.eps); + BfVec y; + y.packed = resOut; + BfVec g; + g.packed = hop.gamma[itemVec]; + BfVec n; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + n.elts[e] = __float2bfloat16(__bfloat162float(y.elts[e]) * rcpRms * __bfloat162float(g.elts[e])); + } + if (writesOutputs) + { + *reinterpret_cast(reinterpret_cast<__nv_bfloat16*>(params.normOut) + rowOff) = n.packed; + *reinterpret_cast(reinterpret_cast<__nv_bfloat16*>(params.residualOut) + rowOff) = y.packed; + } + if constexpr (kRouter) + { + rs.normed[itemRow][itemVec] = n.packed; + } + } + + if constexpr (kRouter) + { + if (numRowsCta > 0) + { + __syncthreads(); + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + if constexpr (kRouterLoad == kRouterBeforeMma) + { + loadRouterFragments(wLo, wHi, params.routerWeight, expertGroup, warp, gid, tig, sliceBase); + } + // Independent accumulator chains keep the MMA latency off a single dependency chain. + float acc[kMmaChains][4] = {}; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + // One 16-byte load covers two k16 steps; the k-permutation inside a step is the same for both operands. + uint4 const b = rs.normed[gid][kGroup * (Cfg::kKPerGroup / kEltsPerVec) + p * 4 + tig]; + int const chain = (2 * p) % kMmaChains; + mmaBf16(acc[chain], wLo[p].x, wHi[p].x, wLo[p].y, wHi[p].y, b.x, b.y); + mmaBf16(acc[chain + 1], wLo[p].z, wHi[p].z, wLo[p].w, wHi[p].w, b.z, b.w); + } + float sum4[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) + { + sum4[i] = (acc[0][i] + acc[1][i]) + (acc[2][i] + acc[3][i]); + } + // Expert e (cluster-local) is owned by CTA e / kExpertsPerOwner. + int const e0 = tile * 16 + gid; + int const e1 = e0 + 8; + uint32_t const owner0 = e0 / Cfg::kExpertsPerOwner; + uint32_t const owner1 = e1 / Cfg::kExpertsPerOwner; + storeAsync(clusterAddr(sharedAddr(&rs.partial[slice][kGroup][e0 % Cfg::kExpertsPerOwner][tig * 2]), owner0), + sum4[0], sum4[1], clusterAddr(sharedAddr(&hop.routerBar), owner0)); + storeAsync(clusterAddr(sharedAddr(&rs.partial[slice][kGroup][e1 % Cfg::kExpertsPerOwner][tig * 2]), owner1), + sum4[2], sum4[3], clusterAddr(sharedAddr(&hop.routerBar), owner1)); + // Also keeps this CTA resident until every partial addressed to it has landed. + mbarrierWaitPhase0(&hop.routerBar); + + // This CTA owns experts [slice * kExpertsPerOwner, (slice + 1) * kExpertsPerOwner) of the cluster. Lane + // groups of 8 sum over the 8 slices. + if (tid < Cfg::kExpertsPerOwner * numRowsCta * kSlices) + { + int const c = tid % kSlices; + int const ij = tid / kSlices; + int const ei = ij / numRowsCta; + int const j = ij % numRowsCta; + float sum = 0.F; +#pragma unroll + for (int kg = 0; kg < Cfg::kKGroups; ++kg) + { + sum += rs.partial[c][kg][ei][j]; + } +#pragma unroll + for (int mask = kSlices / 2; mask > 0; mask >>= 1) + { + sum = sum + __shfl_xor_sync(0xffffffffU, sum, mask, kSlices); + } + if (c == 0) + { + int const r = rowGroup + j * kRowStride; + params.routerLogits[r * kNumExperts + expertGroup * Cfg::kExpertsPerCluster + + slice * Cfg::kExpertsPerOwner + ei] + = sum; + } + } + } + } + + // ---- CTA 0 advances the protocol state once every CTA has read it. ---- + if (tid == 0 && slice == 0 && clusterId == 0) + { + uint32_t arrived = 0; + do + { + asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(arrived) : "l"(params.flags + 2) : "memory"); + } while (arrived < static_cast(kGridCtas)); + uint4 const next = make_uint4((cur + 1) % kNumLamportBuffers, + static_cast(R * kNumRanks * kHiddenDim * sizeof(__nv_bfloat16)), 0U, 0U); + asm volatile("st.relaxed.gpu.global.v4.u32 [%0], {%1, %2, %3, %4};" ::"l"(params.flags), "r"(next.x), + "r"(next.y), "r"(next.z), "r"(next.w) + : "memory"); + } +#else + // Clusters, distributed shared memory and PDL need SM90+; the launcher is only reached on supported devices. + __trap(); +#endif +} + +template +cudaError_t launchTyped(PostAttentionHopFromPartialsParams const& params) +{ + using Cfg = HopConfig; + cudaLaunchAttribute attrs[2]; + attrs[0].id = cudaLaunchAttributeClusterDimension; + attrs[0].val.clusterDim.x = kSlices; + attrs[0].val.clusterDim.y = 1; + attrs[0].val.clusterDim.z = 1; + attrs[1].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[1].val.programmaticStreamSerializationAllowed = params.enablePdl ? 1 : 0; + + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kSlices, kG > 0 ? kG * kRowSplit : R, 1); + config.blockDim = dim3(Cfg::kThreads, 1, 1); + config.dynamicSmemBytes = 0; + config.stream = params.stream; + config.attrs = attrs; + config.numAttrs = 2; + return cudaLaunchKernelEx(&config, postAttnHopX1Kernel, params); +} + +template +cudaError_t launchRows(PostAttentionHopFromPartialsParams const& params) +{ + switch (params.numRows) + { + case 1: return launchTyped<1, kG, kRowSplit>(params); + case 2: return launchTyped<2, kG, kRowSplit>(params); + case 3: return launchTyped<3, kG, kRowSplit>(params); + case 4: return launchTyped<4, kG, kRowSplit>(params); + case 5: return launchTyped<5, kG, kRowSplit>(params); + case 6: return launchTyped<6, kG, kRowSplit>(params); + case 7: return launchTyped<7, kG, kRowSplit>(params); + case 8: return launchTyped<8, kG, kRowSplit>(params); + default: return cudaErrorInvalidValue; + } +} + +// Router hop: 4 expert groups x min(R, 3) row groups (at most 12 clusters of 8 CTAs, each CTA <= 3 row slices). +template +cudaError_t launchRouterAt(PostAttentionHopFromPartialsParams const& params) +{ + switch (params.numRows) + { + case 1: return launchTyped<1, 4, 1, kRouterLoad>(params); + case 2: return launchTyped<2, 4, 2, kRouterLoad>(params); + case 3: return launchTyped<3, 4, 3, kRouterLoad>(params); + case 4: return launchTyped<4, 4, 3, kRouterLoad>(params); + case 5: return launchTyped<5, 4, 3, kRouterLoad>(params); + case 6: return launchTyped<6, 4, 3, kRouterLoad>(params); + case 7: return launchTyped<7, 4, 3, kRouterLoad>(params); + case 8: return launchTyped<8, 4, 3, kRouterLoad>(params); + default: return cudaErrorInvalidValue; + } +} + +cudaError_t launchRouter(PostAttentionHopFromPartialsParams const& params) +{ + switch (params.routerLoad) + { + case kRouterAfterBarrier: return launchRouterAt(params); + case kRouterAfterSum: return launchRouterAt(params); + case kRouterBeforeMma: return launchRouterAt(params); + default: return cudaErrorInvalidValue; + } +} + +} // namespace + +cudaError_t invokePostAttentionHopFromPartials(PostAttentionHopFromPartialsParams const& params) +{ + if (params.rank < 0 || params.rank >= kNumRanks || params.flags == nullptr || params.ucBuffer == nullptr) + { + return cudaErrorInvalidValue; + } + if (params.routerWeight != nullptr) + { + if (params.routerLogits == nullptr) + { + return cudaErrorInvalidValue; + } + return launchRouter(params); + } + return launchRows<0, 1>(params); +} + +} // namespace kernels::glm_moe_dsa::attention_output_into_hop + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.h new file mode 100644 index 000000000000..362fa1da20fa --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.h @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.h" + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::attention_output_into_hop +{ + +//! Post-attention hop that takes its input from the transport: the hop of postAttentionHop.h with its input taken +//! from the transport instead of from a partial tensor. +//! +//! The producer of this call (the attention output tail's o_proj kernel, publishing into the hop) has already +//! written every rank's sanitized rank-local partial (bf16 -0.0 mapped to +0.0) into the current Lamport buffer of +//! the post-attention hop's workspace, slot ``[row][rank][kHiddenDim]``, with multicast stores. This kernel polls +//! all ``kNumRanks`` slots of its rows (its own included) and then performs exactly the post-attention hop's +//! arithmetic: the rank sum in rank order 0..7, the bf16 residual add, RMSNorm with the native reduction tree and +//! the optional router logits with the post-attention hop's MMA decomposition and summation order. Given the same +//! partials, ``normed``, ``residualOut`` and ``routerLogits`` are bit-identical to ``postAttnHop`` consuming the +//! ordinary partial (whose own contribution is the same sanitized value, taken from registers there). +//! +//! The workspace, buffer rotation and protocol state are the post-attention hop's +//! (``kernels::glm_moe_dsa::post_attention_hop``), so calls of either kind can follow each other on one workspace. +//! Memory-order contract with the producer (programmatic dependent launch): +//! * the producer reads the protocol state and writes the transport only after its own ``griddepcontrol.wait`` and +//! triggers its dependents only after that wait, so when this kernel starts, every grid before the producer has +//! completed: the protocol state written by the previous hop and ``residualIn`` are final; +//! * with ``earlyPoll`` this kernel reads the protocol state and polls the transport before its +//! ``griddepcontrol.wait`` (the transport is private to the package and Lamport-validated); it reads +//! ``residualIn`` and writes every output only after the wait, i.e. after the producer grid has completed. +//! Without ``earlyPoll`` it waits first. The early read relies on every kernel between two hops waiting on its +//! predecessor before it completes, which holds for the served graph's kernels (each consumes its predecessor's +//! output). +struct PostAttentionHopFromPartialsParams +{ + void const* residualIn = nullptr; //!< [numRows, kHiddenDim] bf16 + void const* gamma = nullptr; //!< [kHiddenDim] bf16 RMSNorm weight + void const* routerWeight = nullptr; //!< [kNumExperts, kHiddenDim] bf16 or nullptr (no router logits) + void* normOut = nullptr; //!< [numRows, kHiddenDim] bf16 + void* residualOut = nullptr; //!< [numRows, kHiddenDim] bf16 + float* routerLogits = nullptr; //!< [numRows, kNumExperts] fp32, required iff routerWeight != nullptr + void* ucBuffer = nullptr; //!< local unicast view of the post-attention hop's multicast workspace + uint32_t* flags = nullptr; //!< [4] protocol state of that workspace + int numRows = 0; + int rank = 0; + float eps = 1e-5F; + bool enablePdl = true; + //! true: read the protocol state and poll the transport before ``griddepcontrol.wait`` (relies on the producer + //! contract above); false: wait first, as postAttentionHop does, then read the state and poll. + bool earlyPoll = true; + //! Where the router-weight MMA fragments are loaded (same values and arithmetic in every case): 0 after the + //! barrier that ends the rank sum, 1 right after the rank sum (default), 2 right before the MMAs. + int routerLoad = 1; + cudaStream_t stream = nullptr; +}; + +using glm_moe_dsa::post_attention_hop::kHiddenDim; +using glm_moe_dsa::post_attention_hop::kLamportBufferBytes; +using glm_moe_dsa::post_attention_hop::kMaxRows; +using glm_moe_dsa::post_attention_hop::kNumExperts; +using glm_moe_dsa::post_attention_hop::kNumLamportBuffers; +using glm_moe_dsa::post_attention_hop::kNumRanks; +using glm_moe_dsa::post_attention_hop::kWorkspaceBytes; + +//! Launch the transport-input hop on ``params.stream``. Returns the launch status. +cudaError_t invokePostAttentionHopFromPartials(PostAttentionHopFromPartialsParams const& params); + +} // namespace kernels::glm_moe_dsa::attention_output_into_hop + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.cu new file mode 100644 index 000000000000..fe8cc14ea457 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.cu @@ -0,0 +1,889 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.h" + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::hop_routing +{ +namespace +{ +namespace cg = cooperative_groups; + +// The partials hop (glmMoeDsa/postAttentionHopFromPartials.cu, router variant) and top-8 routing. Everything up to and +// including the router-logit stores is the partials hop's code; the additions are marked "Routing". +// +// Work decomposition (the partials hop's). Every cluster has kSlices CTAs; CTA c owns hidden slice +// [c * kSlice, (c + 1) * kSlice). There are kG x kRowSplit clusters (kG = 4, kRowSplit = min(R, 3)). Cluster (g, h) +// collects and normalizes the rows r with r % kRowSplit == h and computes their logits for experts [64 g, 64 g + 64); +// CTA c of the cluster owns the 8 experts 64 g + 8 c + [0, 8) and writes their logits. +// +// Routing. The owner of logit (r, e) also writes the score bits of (r, e) into the gather buffer. Row +// r = h + j * kRowSplit (local row j of row group h) is finished by CTA 0 of cluster (g = j, h): its 256 threads poll +// the row's 256 scores, each warp ranks its 32 keys and keeps its top 8 (level 1), the 256 threads rank the 64 +// candidates (level 2, four lanes per candidate), and lanes 0..7 write the row's ids and weights. +constexpr int kSlices = 8; +constexpr int kSlice = kHiddenDim / kSlices; +constexpr int kEltsPerVec = 8; +constexpr int kVecPerSlice = kSlice / kEltsPerVec; +constexpr int kNativeWarpsPerSlice = kVecPerSlice / 32; +// Row stride of the normalized-slice tile in 16-byte vectors; 100 = 96 + 4 keeps the 16-byte B-fragment loads of two +// rows in one quarter-warp phase on disjoint banks. +constexpr int kNormedStride = kVecPerSlice + 4; +constexpr int kMmaChains = 4; +constexpr uint32_t kLamportSentinel = 0x80000000U; +// The empty gather slot: a routing score is never -0.0f (see postAttentionHopRouting.h). +constexpr uint32_t kGatherSentinel = 0x80000000U; +constexpr int kG = 4; +constexpr int kExpertWarps = kNumExperts / 32; +constexpr int kCandidates = kExpertWarps * kTopK; + +static_assert(kHiddenDim % (kSlices * kEltsPerVec * 32) == 0); +static_assert(kVecPerSlice == kNativeWarpsPerSlice * 32); +static_assert(kMaxRows == 8, "the router MMA maps rows onto the n8 dimension"); +static_assert(kNumExperts == 256 && kTopK == 8, "the finisher maps one expert per thread and 8 candidates per warp"); +static_assert(kCandidates * 4 == kNumExperts, "level 2 ranks each candidate on four lanes"); + +template +struct HopConfig +{ + static constexpr int kThreads = 384; + static constexpr int kWarps = kThreads / 32; + static constexpr int kExpertsPerCluster = kNumExperts / kG; + static constexpr int kExpertTiles = kExpertsPerCluster / 16; + static constexpr int kKGroups = kWarps / kExpertTiles; + static constexpr int kKPerGroup = kSlice / kKGroups; + static constexpr int kPairsPerGroup = kKPerGroup / 32; + static constexpr int kExpertsPerOwner = kExpertsPerCluster / kSlices; + static_assert(kExpertsPerCluster % 16 == 0 && kWarps % kExpertTiles == 0 && kSlice % (kKGroups * 32) == 0); + static_assert(kPairsPerGroup % (kMmaChains / 2) == 0); + static_assert(kThreads >= kNumExperts); +}; + +constexpr int kMaxRowsPerCtaAll = kMaxRows / 2; + +struct alignas(16) HopStorage +{ + uint4 gamma[kVecPerSlice]; + float vecSq[kMaxRowsPerCtaAll][kVecPerSlice]; + float warpSq[kMaxRowsPerCtaAll][kNativeWarpsPerSlice]; + float blockSq[kMaxRowsPerCtaAll][kSlices]; + // Completes when all slice sums of this CTA's rows have arrived (st.async complete_tx). + alignas(8) uint64_t normBar; + // Completes when all partial logits of the experts this CTA owns have arrived. + alignas(8) uint64_t routerBar; +}; + +template +struct alignas(16) RouterStorage +{ + using Cfg = HopConfig; + uint4 normed[kMaxRows][kNormedStride]; // local row on the MMA n dimension; unused rows stay zero + float partial[kSlices][Cfg::kKGroups][Cfg::kExpertsPerOwner][kMaxRows]; +}; + +// Routing: state of one CTA. +struct alignas(16) RouteStorage +{ + unsigned long long candKey[kCandidates]; // level-1 survivors: native 64-bit keys + float candSg[kCandidates]; // their score - bias + unsigned long long selKey[kTopK]; // level-2 result in rank order + float selSg[kTopK]; + float biasOwn[8]; // bias of the experts this CTA owns + uint32_t parity; // gather parity of this call + float biasAll[kNumExperts]; // finisher CTAs: bias of every expert +#if defined(TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS) + unsigned long long stamp[kNumStamps]; +#endif +}; + +union BfVec +{ + uint4 packed; + __nv_bfloat16 elts[kEltsPerVec]; +}; + +__device__ __forceinline__ uint4 loadVolatile(void const* ptr) +{ + uint4 v; + asm volatile("ld.volatile.global.v4.u32 {%0, %1, %2, %3}, [%4];" + : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) + : "l"(ptr) + : "memory"); + return v; +} + +__device__ __forceinline__ uint32_t loadVolatileU32(void const* ptr) +{ + uint32_t v; + asm volatile("ld.volatile.global.u32 %0, [%1];" : "=r"(v) : "l"(ptr) : "memory"); + return v; +} + +__device__ __forceinline__ uint4 loadConstant(void const* ptr) +{ + uint4 v; + asm volatile("ld.global.nc.v4.u32 {%0, %1, %2, %3}, [%4];" : "=r"(v.x), "=r"(v.y), "=r"(v.z), "=r"(v.w) : "l"(ptr)); + return v; +} + +__device__ __forceinline__ float loadBiasConstant(void const* bias, int expert) +{ + unsigned short raw; + asm volatile("ld.global.nc.u16 %0, [%1];" : "=h"(raw) : "l"(reinterpret_cast<__nv_bfloat16 const*>(bias) + expert)); + return __bfloat162float(__ushort_as_bfloat16(raw)); +} + +__device__ __forceinline__ void prefetchL2(void const* ptr) +{ + asm volatile("prefetch.global.L2 [%0];" ::"l"(ptr)); +} + +__device__ __forceinline__ void storeRelaxed(uint32_t* ptr, uint32_t v) +{ + asm volatile("st.relaxed.gpu.global.u32 [%0], %1;" ::"l"(ptr), "r"(v) : "memory"); +} + +__device__ __forceinline__ uint32_t loadRelaxed(uint32_t const* ptr) +{ + uint32_t v; + asm volatile("ld.relaxed.gpu.global.u32 %0, [%1];" : "=r"(v) : "l"(ptr) : "memory"); + return v; +} + +__device__ __forceinline__ bool isDirty(uint4 const& v) +{ + return v.x == kLamportSentinel || v.y == kLamportSentinel || v.z == kLamportSentinel || v.w == kLamportSentinel; +} + +__device__ __forceinline__ uint32_t sharedAddr(void const* ptr) +{ + return static_cast(__cvta_generic_to_shared(ptr)); +} + +// Address of the same shared-memory object in CTA `rank` of the cluster. +__device__ __forceinline__ uint32_t clusterAddr(uint32_t addr, uint32_t rank) +{ + uint32_t out; + asm volatile("mapa.shared::cluster.u32 %0, %1, %2;" : "=r"(out) : "r"(addr), "r"(rank)); + return out; +} + +__device__ __forceinline__ void mbarrierInitExpectTx(uint64_t* bar, uint32_t bytes) +{ + asm volatile("mbarrier.init.shared::cta.b64 [%0], 1;" ::"r"(sharedAddr(bar)) : "memory"); + asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;" ::"r"(sharedAddr(bar)), "r"(bytes) + : "memory"); +} + +__device__ __forceinline__ void mbarrierWaitPhase0(uint64_t* bar) +{ + asm volatile( + "{\n" + ".reg .pred done;\n" + "ROUTING_MBAR_WAIT_%=:\n" + "mbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 done, [%0], 0;\n" + "@!done bra ROUTING_MBAR_WAIT_%=;\n" + "}\n" ::"r"(sharedAddr(bar)) + : "memory"); +} + +// Remote shared-memory stores that complete transactions on the destination CTA's mbarrier. +__device__ __forceinline__ void storeAsync(uint32_t remoteAddr, float v, uint32_t remoteBar) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.f32 [%0], %1, [%2];" ::"r"(remoteAddr), "f"(v), + "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void storeAsync(uint32_t remoteAddr, float a, float b, uint32_t remoteBar) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.v2.f32 [%0], {%1, %2}, [%3];" ::"r"(remoteAddr), + "f"(a), "f"(b), "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void mmaBf16( + float (&d)[4], uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t b0, uint32_t b1) +{ + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.bf16.bf16.f32 {%0, %1, %2, %3}, {%4, %5, %6, %7}, {%8, %9}, " + "{%0, %1, %2, %3};" + : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b0), "r"(b1)); +} + +// One warp's router-weight MMA fragments (the post-attention hop's register layout): experts tile * 16 + gid (+ 8) +// of this cluster's expert group, k pairs of this warp's k group within the CTA's slice. +template +__device__ __forceinline__ void loadRouterFragments(uint4 (&wLo)[Cfg::kPairsPerGroup], + uint4 (&wHi)[Cfg::kPairsPerGroup], void const* routerWeight, int expertGroup, int warp, int gid, int tig, + int sliceBase) +{ + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + auto const* w = reinterpret_cast<__nv_bfloat16 const*>(routerWeight); + int const expert = expertGroup * Cfg::kExpertsPerCluster + tile * 16 + gid; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + int const k = sliceBase + kGroup * Cfg::kKPerGroup + p * 32 + tig * kEltsPerVec; + wLo[p] = loadConstant(w + static_cast(expert) * kHiddenDim + k); + wHi[p] = loadConstant(w + static_cast(expert + 8) * kHiddenDim + k); + } +} + +// Router-weight fragment load points, as in the partials hop (all orders give the same values and arithmetic). +constexpr int kRouterAfterBarrier = 0; +constexpr int kRouterAfterSum = 1; +constexpr int kRouterBeforeMma = 2; + +// Routing: the native routing score (RoutingKernel.cuh sigmoid_accurate plus the SigmoidBias preprocess). +__device__ __forceinline__ float routingScore(float logit, float bias) +{ + float const s = 0.5f * tanhf(0.5f * logit) + 0.5f; + return s + bias; +} + +// Routing: cub::Traits::TwiddleIn, the order-preserving map of FP32 bits onto unsigned integers. +__device__ __forceinline__ uint32_t twiddleIn(uint32_t bits) +{ + return bits ^ ((bits & 0x80000000U) != 0U ? 0xFFFFFFFFU : 0x80000000U); +} + +#if defined(TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS) +__device__ __forceinline__ unsigned long long globalTimer() +{ + unsigned long long t; + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); + return t; +} + +#define ROUTING_STAMP(field) atomicMax(&route.stamp[field], globalTimer()) +#else +#define ROUTING_STAMP(field) \ + do \ + { \ + } while (0) +#endif + +// Stamp fields (diagnostic builds). +constexpr int kStampEntry = 0; +constexpr int kStampWait = 1; +constexpr int kStampLogits = 2; +constexpr int kStampGather = 3; +constexpr int kStampPolled = 4; +constexpr int kStampLevel1 = 5; +constexpr int kStampRouted = 6; +constexpr int kStampEnd = 7; +static_assert(kStampEnd < kNumStamps); + +// The register cap of the partials hop, under which one CTA of it fits on an SM next to a CTA of the publishing +// o_proj. The routing code after the MMA phase would raise the allocation above it; the cap keeps the partials hop's +// allocation without spills. +constexpr int kMaxRegisters = 112; + +template +__global__ void __maxnreg__(kMaxRegisters) postAttnHopZ2Kernel(PostAttentionHopRoutingParams const params) +{ + static_assert(R >= 1 && R <= kMaxRows); +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + using Cfg = HopConfig; + constexpr int kClusters = kG * kRowSplit; + constexpr int kGridCtas = kClusters * kSlices; + static_assert(kGridCtas <= kMaxGridCtas); + // Rows one CTA collects, normalizes and multiplies. + constexpr int kRowsPerCta = (R + kRowSplit - 1) / kRowSplit; + static_assert(kRowsPerCta <= kMaxRowsPerCtaAll && kRowsPerCta <= kG); + static_assert(kRowsPerCta * kVecPerSlice <= Cfg::kThreads && kRowsPerCta * kNativeWarpsPerSlice <= Cfg::kWarps); + static_assert(Cfg::kExpertsPerOwner * kRowsPerCta * kSlices <= Cfg::kThreads); + + __shared__ HopStorage hop; + __shared__ RouterStorage rs; + __shared__ RouteStorage route; + + cg::cluster_group cluster = cg::this_cluster(); + int const slice = static_cast(cluster.block_rank()); + int const clusterId = blockIdx.y; + int const expertGroup = clusterId % kG; + int const rowGroup = clusterId / kG; + int const tid = threadIdx.x; + int const lane = tid & 31; + int const warp = tid >> 5; + int const sliceBase = slice * kSlice; + uint32_t const ctaLinear = clusterId * kSlices + slice; + + // Rows this CTA collects and normalizes: local row j is global row rowGroup + j * kRowSplit. + constexpr int kRowStride = kRowSplit; + int const numRowsCta = (R - rowGroup + kRowSplit - 1) / kRowSplit; + // This thread's item: one 16-byte vector of one row slice. + int const itemRow = tid / kVecPerSlice; + int const itemVec = tid % kVecPerSlice; + bool const hasItem = itemRow < numRowsCta; + int const row = rowGroup + itemRow * kRowStride; + size_t const rowOff = static_cast(row) * kHiddenDim + sliceBase + itemVec * kEltsPerVec; + // The cluster that writes the normalized rows and the residual. + bool const writesOutputs = expertGroup == 0; + // Routing: this CTA finishes local row `expertGroup` of its row group, if that row exists. + bool const finisher = slice == 0 && expertGroup < numRowsCta; + +#if defined(TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS) + if (tid < kNumStamps) + { + route.stamp[tid] = 0ULL; + } + __syncthreads(); + if (tid == 0) + { + ROUTING_STAMP(kStampEntry); + } +#endif + + // ---- Prologue: constant model weights only; may overlap the producer of `input` under PDL. ---- + if (tid < kVecPerSlice) + { + hop.gamma[tid] = loadConstant(reinterpret_cast(params.gamma) + slice * kVecPerSlice + tid); + } + // Routing: router bias of the owned experts and, on finishers, of every expert (a model parameter). + if (tid < Cfg::kExpertsPerOwner) + { + route.biasOwn[tid] = loadBiasConstant( + params.routingBias, expertGroup * Cfg::kExpertsPerCluster + slice * Cfg::kExpertsPerOwner + tid); + } + if (finisher && tid < kNumExperts) + { + route.biasAll[tid] = loadBiasConstant(params.routingBias, tid); + } + int const gid = lane >> 2; + int const tig = lane & 3; + // The router-weight fragments are loaded after the poll (not here), so they are not live across it; the prologue + // only asks L2 for the same lines (no registers held, no architectural effect). + { + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + auto const* w = reinterpret_cast<__nv_bfloat16 const*>(params.routerWeight); + int const expert = expertGroup * Cfg::kExpertsPerCluster + tile * 16 + gid; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + int const k = sliceBase + kGroup * Cfg::kKPerGroup + p * 32 + tig * kEltsPerVec; + prefetchL2(w + static_cast(expert) * kHiddenDim + k); + prefetchL2(w + static_cast(expert + 8) * kHiddenDim + k); + } + } + // MMA columns of rows this CTA does not own stay zero. + for (int i = tid; i < kMaxRows * kNormedStride; i += Cfg::kThreads) + { + rs.normed[i / kNormedStride][i % kNormedStride] = make_uint4(0, 0, 0, 0); + } + + // Cluster-local completion barriers: every CTA of a cluster has the same rows, so each expects the 8 slice sums + // of its rows and the 8 x kKGroups partial tiles of the experts it owns. A split cluster barrier publishes the + // initialization; its wait precedes the first remote store, long after all CTAs arrived. + if (tid == 0) + { + mbarrierInitExpectTx(&hop.normBar, static_cast(numRowsCta * kSlices * sizeof(float))); + mbarrierInitExpectTx(&hop.routerBar, numRowsCta > 0 ? static_cast(sizeof(rs.partial)) : 0U); + asm volatile("fence.mbarrier_init.release.cluster;" ::: "memory"); + } + asm volatile("barrier.cluster.arrive.release.aligned;" ::: "memory"); + + // With earlyPoll no wait here (the protocol state and the transport may be read early, see + // postAttentionHopFromPartials.h). + auto const* residualIn = reinterpret_cast<__nv_bfloat16 const*>(params.residualIn); + uint4 res = make_uint4(0, 0, 0, 0); + if (!params.earlyPoll) + { + asm volatile("griddepcontrol.wait;" ::: "memory"); + // Dependents may start their prologue; they still wait for this grid to complete. + asm volatile("griddepcontrol.launch_dependents;"); + if (hasItem) + { + res = *reinterpret_cast(residualIn + rowOff); + } + } + uint4 const flags = loadVolatile(params.flags); + uint32_t const cur = flags.x; + uint32_t const clearBytes = flags.y; + char* ucBase = reinterpret_cast(params.ucBuffer); + auto* curUc = reinterpret_cast<__nv_bfloat16*>(ucBase + cur * kLamportBufferBytes); + // Lamport layout of one buffer: [row][source rank][kHiddenDim]. + size_t const slotBase = static_cast(row) * kNumRanks * kHiddenDim + sliceBase + itemVec * kEltsPerVec; + + // ---- Collect every rank's partial, this rank's own included, from the transport. ---- + uint4 vals[kNumRanks]; + if (hasItem) + { +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + vals[s] = loadVolatile(curUc + slotBase + static_cast(s) * kHiddenDim); + } + } + + // ---- Reset the previous call's buffer to the sentinel (reused two calls from now), off the poll's path. ---- + { + uint32_t const dirty = (cur + kNumLamportBuffers - 1) % kNumLamportBuffers; + auto* dirtyBuf = reinterpret_cast(ucBase + dirty * kLamportBufferBytes); + uint32_t const numVec = clearBytes / sizeof(uint4); + uint4 const sentinel = make_uint4(kLamportSentinel, kLamportSentinel, kLamportSentinel, kLamportSentinel); + for (uint32_t j = ctaLinear * Cfg::kThreads + tid; j < numVec; j += kGridCtas * Cfg::kThreads) + { + dirtyBuf[j] = sentinel; + } + } + + if (hasItem) + { + bool pending = true; + while (pending) + { + pending = false; +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + if (isDirty(vals[s])) + { + pending = true; + vals[s] = loadVolatile(curUc + slotBase + static_cast(s) * kHiddenDim); + } + } + } + } + + // ---- earlyPoll: wait for the producer grid before reading the residual or writing any output. ---- + if (params.earlyPoll) + { + asm volatile("griddepcontrol.wait;" ::: "memory"); + // Dependents only start their own prologue; they still wait for this grid to complete. + asm volatile("griddepcontrol.launch_dependents;"); + if (hasItem) + { + res = *reinterpret_cast(residualIn + rowOff); + } + } +#if defined(TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS) + if (tid == 0) + { + ROUTING_STAMP(kStampWait); + } +#endif + + // Router-weight fragments (see kRouterLoad). + uint4 wLo[Cfg::kPairsPerGroup]; + uint4 wHi[Cfg::kPairsPerGroup]; + + // ---- Reduce in rank order, add the residual, and form per-vector sums of squares (native arithmetic). ---- + uint4 resOut = make_uint4(0, 0, 0, 0); + if (hasItem) + { + float acc[kEltsPerVec]; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + acc[e] = 0.F; + } +#pragma unroll + for (int s = 0; s < kNumRanks; ++s) + { + BfVec x; + x.packed = vals[s]; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + acc[e] += __bfloat162float(x.elts[e]); + } + } + BfVec rv; + rv.packed = res; + BfVec y; + float sq = 0.F; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + y.elts[e] = __hadd(__float2bfloat16(acc[e]), rv.elts[e]); + sq += __bfloat162float(__hmul(y.elts[e], y.elts[e])); + } + resOut = y.packed; + hop.vecSq[itemRow][itemVec] = sq; + } + if constexpr (kRouterLoad == kRouterAfterSum) + { + if (numRowsCta > 0) + { + loadRouterFragments(wLo, wHi, params.routerWeight, expertGroup, warp, gid, tig, sliceBase); + } + } + if (tid == 0) + { + // Routing: the gather parity of this call (the routing state is read only after the dependency wait). It is + // kept in shared memory, published by the barrier below, so no register holds it across the MMA phase. + route.parity = loadVolatileU32(params.zstate) & 1U; + } + __syncthreads(); + if (tid == 0) + { + // Every thread of this CTA has read the protocol state and thread 0 the gather parity; CTA 0 advances both only + // after all CTAs report. + asm volatile("red.release.gpu.global.add.u32 [%0], %1;" ::"l"(params.flags + 2), "r"(1U) : "memory"); + } + // ---- Routing: reset the previous call's gather buffer (its readers belong to a completed grid), off the routing + // path. ---- + { + uint4* const other = reinterpret_cast(params.gather + (route.parity ^ 1U) * kGatherBufferWords); + uint4 const sentinel = make_uint4(kGatherSentinel, kGatherSentinel, kGatherSentinel, kGatherSentinel); + for (uint32_t j = ctaLinear * Cfg::kThreads + tid; j < kGatherBufferWords / 4; j += kGridCtas * Cfg::kThreads) + { + other[j] = sentinel; + } + } + if constexpr (kRouterLoad == kRouterAfterBarrier) + { + if (numRowsCta > 0) + { + loadRouterFragments(wLo, wHi, params.routerWeight, expertGroup, warp, gid, tig, sliceBase); + } + } + + // ---- RMS: warp butterfly over 32 vectors, then (w0 + w2) + w1 per slice, then the 8 slices in order. ---- + if (warp < numRowsCta * kNativeWarpsPerSlice) + { + int const j = warp / kNativeWarpsPerSlice; + int const q = warp % kNativeWarpsPerSlice; + float val = hop.vecSq[j][q * 32 + lane]; +#pragma unroll + for (int mask = 16; mask > 0; mask >>= 1) + { + val = val + __shfl_xor_sync(0xffffffffU, val, mask, 32); + } + if (lane == 0) + { + hop.warpSq[j][q] = val; + } + } + __syncthreads(); + asm volatile("barrier.cluster.wait.acquire.aligned;" ::: "memory"); + if (tid < numRowsCta * kSlices) + { + int const j = tid / kSlices; + int const dst = tid % kSlices; + float const blockSum = (hop.warpSq[j][0] + hop.warpSq[j][2]) + hop.warpSq[j][1]; + storeAsync( + clusterAddr(sharedAddr(&hop.blockSq[j][slice]), dst), blockSum, clusterAddr(sharedAddr(&hop.normBar), dst)); + } + mbarrierWaitPhase0(&hop.normBar); + + if (hasItem) + { + float fullSum = 0.F; +#pragma unroll + for (int c = 0; c < kSlices; ++c) + { + fullSum += hop.blockSq[itemRow][c]; + } + float const rcpRms = rsqrtf(fullSum / static_cast(kHiddenDim) + params.eps); + BfVec y; + y.packed = resOut; + BfVec g; + g.packed = hop.gamma[itemVec]; + BfVec n; +#pragma unroll + for (int e = 0; e < kEltsPerVec; ++e) + { + n.elts[e] = __float2bfloat16(__bfloat162float(y.elts[e]) * rcpRms * __bfloat162float(g.elts[e])); + } + if (writesOutputs) + { + *reinterpret_cast(reinterpret_cast<__nv_bfloat16*>(params.normOut) + rowOff) = n.packed; + *reinterpret_cast(reinterpret_cast<__nv_bfloat16*>(params.residualOut) + rowOff) = y.packed; + } + rs.normed[itemRow][itemVec] = n.packed; + } + + if (numRowsCta > 0) + { + __syncthreads(); + int const tile = warp % Cfg::kExpertTiles; + int const kGroup = warp / Cfg::kExpertTiles; + if constexpr (kRouterLoad == kRouterBeforeMma) + { + loadRouterFragments(wLo, wHi, params.routerWeight, expertGroup, warp, gid, tig, sliceBase); + } + // Independent accumulator chains keep the MMA latency off a single dependency chain. + float acc[kMmaChains][4] = {}; +#pragma unroll + for (int p = 0; p < Cfg::kPairsPerGroup; ++p) + { + // One 16-byte load covers two k16 steps; the k-permutation inside a step is the same for both operands. + uint4 const b = rs.normed[gid][kGroup * (Cfg::kKPerGroup / kEltsPerVec) + p * 4 + tig]; + int const chain = (2 * p) % kMmaChains; + mmaBf16(acc[chain], wLo[p].x, wHi[p].x, wLo[p].y, wHi[p].y, b.x, b.y); + mmaBf16(acc[chain + 1], wLo[p].z, wHi[p].z, wLo[p].w, wHi[p].w, b.z, b.w); + } + float sum4[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) + { + sum4[i] = (acc[0][i] + acc[1][i]) + (acc[2][i] + acc[3][i]); + } + // Expert e (cluster-local) is owned by CTA e / kExpertsPerOwner. + int const e0 = tile * 16 + gid; + int const e1 = e0 + 8; + uint32_t const owner0 = e0 / Cfg::kExpertsPerOwner; + uint32_t const owner1 = e1 / Cfg::kExpertsPerOwner; + storeAsync(clusterAddr(sharedAddr(&rs.partial[slice][kGroup][e0 % Cfg::kExpertsPerOwner][tig * 2]), owner0), + sum4[0], sum4[1], clusterAddr(sharedAddr(&hop.routerBar), owner0)); + storeAsync(clusterAddr(sharedAddr(&rs.partial[slice][kGroup][e1 % Cfg::kExpertsPerOwner][tig * 2]), owner1), + sum4[2], sum4[3], clusterAddr(sharedAddr(&hop.routerBar), owner1)); + // Also keeps this CTA resident until every partial addressed to it has landed. + mbarrierWaitPhase0(&hop.routerBar); + + // This CTA owns experts [slice * kExpertsPerOwner, (slice + 1) * kExpertsPerOwner) of the cluster. Lane + // groups of 8 sum over the 8 slices. + if (tid < Cfg::kExpertsPerOwner * numRowsCta * kSlices) + { + int const c = tid % kSlices; + int const ij = tid / kSlices; + int const ei = ij / numRowsCta; + int const j = ij % numRowsCta; + float sum = 0.F; +#pragma unroll + for (int kg = 0; kg < Cfg::kKGroups; ++kg) + { + sum += rs.partial[c][kg][ei][j]; + } +#pragma unroll + for (int mask = kSlices / 2; mask > 0; mask >>= 1) + { + sum = sum + __shfl_xor_sync(0xffffffffU, sum, mask, kSlices); + } + if (c == 0) + { + int const r = rowGroup + j * kRowStride; + int const e = expertGroup * Cfg::kExpertsPerCluster + slice * Cfg::kExpertsPerOwner + ei; + params.routerLogits[r * kNumExperts + e] = sum; + ROUTING_STAMP(kStampLogits); + // Routing: the score of (r, e) into this call's gather buffer. + float const logit = kLogitsIn ? params.logitsIn[r * kNumExperts + e] : sum; + storeRelaxed(params.gather + route.parity * kGatherBufferWords + r * kNumExperts + e, + __float_as_uint(routingScore(logit, route.biasOwn[ei]))); + ROUTING_STAMP(kStampGather); + } + } + } + + // ---- Routing: finisher CTAs select the top 8 of their row. ---- + if (finisher) + { + int const rf = rowGroup + expertGroup * kRowStride; + if (tid < kNumExperts) + { + uint32_t const* slot = params.gather + route.parity * kGatherBufferWords + rf * kNumExperts + tid; + uint32_t bits = loadRelaxed(slot); + while (bits == kGatherSentinel) + { + bits = loadRelaxed(slot); + } +#if defined(TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS) + __syncwarp(); + if (lane == 0) + { + ROUTING_STAMP(kStampPolled); + } +#endif + float const sg = __uint_as_float(bits) - route.biasAll[tid]; + uint32_t const hi = twiddleIn(bits); + // Level 1: rank of this key among the warp's 32 (expert 32 * warp + i sits in lane i, so equal scores + // rank the lower lane first, as the native key does). + int rank = 0; +#pragma unroll + for (int i = 0; i < 32; ++i) + { + uint32_t const other = __shfl_sync(0xffffffffU, hi, i); + rank += (other > hi || (other == hi && i < lane)) ? 1 : 0; + } + if (rank < kTopK) + { + route.candKey[warp * kTopK + rank] + = (static_cast(hi) << 32) | static_cast(65535 - tid); + route.candSg[warp * kTopK + rank] = sg; + } + } + __syncthreads(); +#if defined(TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS) + if (tid == 0) + { + ROUTING_STAMP(kStampLevel1); + } +#endif + if (tid < kNumExperts) + { + // Level 2: rank of the 64 candidates by the full native key (all keys are distinct). Four lanes share + // candidate tid / 4; lane q of them counts the larger keys among candidates q, q + 4, ..., q + 60. + int const ci = tid >> 2; + int const q = tid & 3; + unsigned long long const key = route.candKey[ci]; + int rank = 0; +#pragma unroll + for (int i = 0; i < kCandidates / 4; ++i) + { + rank += route.candKey[i * 4 + q] > key ? 1 : 0; + } + rank += __shfl_xor_sync(0xffffffffU, rank, 1); + rank += __shfl_xor_sync(0xffffffffU, rank, 2); + if (q == 0 && rank < kTopK) + { + route.selKey[rank] = key; + route.selSg[rank] = route.candSg[ci]; + } + } + __syncthreads(); + if (warp == 0) + { + if (lane < kTopK) + { + float s[kTopK]; +#pragma unroll + for (int k = 0; k < kTopK; ++k) + { + s[k] = route.selSg[k]; + } + // The native warp sum over lanes 0..7 (zeros above): butterfly offsets 16, 8, 4, 2, 1. + float const total = ((s[0] + s[4]) + (s[2] + s[6])) + ((s[1] + s[5]) + (s[3] + s[7])); + float const weight = route.selSg[lane] * params.routedScaling / total; + int const expert = 65535 - static_cast(route.selKey[lane] & 0xFFFFULL); + params.topkIds[rf * kTopK + lane] = expert; + params.topkWeights[rf * kTopK + lane] = __bfloat162float(__float2bfloat16(weight)); + ROUTING_STAMP(kStampRouted); + } + } + } + + // ---- CTA 0 advances the protocol state and the gather parity once every CTA has read them. ---- + if (tid == 0 && slice == 0 && clusterId == 0) + { + uint32_t arrived = 0; + do + { + asm volatile("ld.acquire.gpu.global.u32 %0, [%1];" : "=r"(arrived) : "l"(params.flags + 2) : "memory"); + } while (arrived < static_cast(kGridCtas)); + uint4 const next = make_uint4((cur + 1) % kNumLamportBuffers, + static_cast(R * kNumRanks * kHiddenDim * sizeof(__nv_bfloat16)), 0U, 0U); + asm volatile("st.relaxed.gpu.global.v4.u32 [%0], {%1, %2, %3, %4};" ::"l"(params.flags), "r"(next.x), + "r"(next.y), "r"(next.z), "r"(next.w) + : "memory"); + storeRelaxed(params.zstate, route.parity ^ 1U); + } +#if defined(TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS) + __syncthreads(); + if (tid == 0) + { + ROUTING_STAMP(kStampEnd); + } + __syncthreads(); + if (tid < kNumStamps && params.stamps != nullptr) + { + params.stamps[ctaLinear * kNumStamps + tid] = route.stamp[tid]; + } +#endif +#else + // Clusters, distributed shared memory and PDL need SM90+; the launcher is only reached on supported devices. + __trap(); +#endif +} + +template +cudaError_t launchTyped(PostAttentionHopRoutingParams const& params) +{ + using Cfg = HopConfig; + cudaLaunchAttribute attrs[2]; + attrs[0].id = cudaLaunchAttributeClusterDimension; + attrs[0].val.clusterDim.x = kSlices; + attrs[0].val.clusterDim.y = 1; + attrs[0].val.clusterDim.z = 1; + attrs[1].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[1].val.programmaticStreamSerializationAllowed = params.enablePdl ? 1 : 0; + + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kSlices, kG * kRowSplit, 1); + config.blockDim = dim3(Cfg::kThreads, 1, 1); + config.dynamicSmemBytes = 0; + config.stream = params.stream; + config.attrs = attrs; + config.numAttrs = 2; + return cudaLaunchKernelEx(&config, postAttnHopZ2Kernel, params); +} + +// 4 expert groups x min(R, 3) row groups (at most 12 clusters of 8 CTAs, each CTA <= 3 row slices). +template +cudaError_t launchAt(PostAttentionHopRoutingParams const& params) +{ + switch (params.numRows) + { + case 1: return launchTyped<1, 1, kRouterLoad, kLogitsIn>(params); + case 2: return launchTyped<2, 2, kRouterLoad, kLogitsIn>(params); + case 3: return launchTyped<3, 3, kRouterLoad, kLogitsIn>(params); + case 4: return launchTyped<4, 3, kRouterLoad, kLogitsIn>(params); + case 5: return launchTyped<5, 3, kRouterLoad, kLogitsIn>(params); + case 6: return launchTyped<6, 3, kRouterLoad, kLogitsIn>(params); + case 7: return launchTyped<7, 3, kRouterLoad, kLogitsIn>(params); + case 8: return launchTyped<8, 3, kRouterLoad, kLogitsIn>(params); + default: return cudaErrorInvalidValue; + } +} + +} // namespace + +bool stampsCompiled() +{ +#if defined(TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS) + return true; +#else + return false; +#endif +} + +cudaError_t invokePostAttentionHopRouting(PostAttentionHopRoutingParams const& params) +{ + if (params.rank < 0 || params.rank >= kNumRanks || params.flags == nullptr || params.ucBuffer == nullptr + || params.routerWeight == nullptr || params.routingBias == nullptr || params.routerLogits == nullptr + || params.topkIds == nullptr || params.topkWeights == nullptr || params.gather == nullptr + || params.zstate == nullptr) + { + return cudaErrorInvalidValue; + } + if (params.logitsIn != nullptr) + { + // Test instance: the default router-weight load point only. + return params.routerLoad == kRouterAfterSum ? launchAt(params) : cudaErrorInvalidValue; + } + switch (params.routerLoad) + { + case kRouterAfterBarrier: return launchAt(params); + case kRouterAfterSum: return launchAt(params); + case kRouterBeforeMma: return launchAt(params); + default: return cudaErrorInvalidValue; + } +} + +} // namespace kernels::glm_moe_dsa::hop_routing + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.h new file mode 100644 index 000000000000..cb19a3f7f947 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.h @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.h" + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::hop_routing +{ + +//! Pre-MoE hop with routing: the transport-input hop (postAttentionHopFromPartials.h) with router logits, plus +//! the native DeepSeek-V3 top-8 routing of every row computed from those logits. +//! +//! Normalized hidden, residual and router logits are produced exactly as by +//! ``glm_moe_dsa::attention_output_into_hop::invokePostAttentionHopFromPartials`` (same work decomposition, arithmetic +//! and protocol on the same post-attention hop workspace), so they are bit-identical to that hop's. +//! +//! Routing (the native ``SigmoidBias`` + ``ScaledSumNormalize`` routing of the TRT-LLM-Gen routing runner with one +//! expert group): every logit owner computes ``score = 0.5f * tanhf(0.5f * logit) + 0.5f + float(bias[e])`` and stores +//! its bits into the gather buffer; one finisher CTA per row collects the row's 256 scores and selects the 8 largest +//! native keys ``(TwiddleIn(score) << 32) | (65535 - e)`` (ties go to the lower expert) in descending order, and writes +//! ``topkIds[r][k]`` and ``topkWeights[r][k] = bf16((score_k - bias_k) * routedScaling / sum)`` (stored as FP32), where +//! ``sum`` adds the eight ``score_k - bias_k`` in the native warp-butterfly order ((s0+s4)+(s2+s6))+((s1+s5)+(s3+s7)). +//! +//! Gather protocol (private to this kernel): ``gather`` holds two buffers of ``kMaxRows x kNumExperts`` 32-bit words +//! and ``zstate[0]`` the parity ``p`` of the buffer this call uses. A score is never -0.0f, so the word 0x80000000 +//! marks an empty slot. Each call resets buffer ``p ^ 1`` (the previous call's) to that sentinel and CTA 0 flips the +//! parity together with the hop's state advance, after every CTA has read both states. Calls of other hop kinds on the +//! same hop workspace do not touch the routing state. +//! +//! Memory-order contract with the producer (programmatic dependent launch): as +//! ``glm_moe_dsa::attention_output_into_hop``. Before ``griddepcontrol.wait`` the kernel reads only the RMSNorm +//! weight, the router weight (L2 prefetch) and the router bias, which no kernel writes while serving; with +//! ``earlyPoll`` it also reads the protocol state of the post-attention hop workspace and polls the transport early +//! (see postAttentionHopFromPartials.h). The routing state, the gather buffers and every output are read or written +//! only after the wait. Finishers spin on gather words that every CTA writes before it waits on anything outside its +//! own cluster, so the wait-for graph is acyclic. +struct PostAttentionHopRoutingParams +{ + void const* residualIn = nullptr; //!< [numRows, kHiddenDim] bf16 + void const* gamma = nullptr; //!< [kHiddenDim] bf16 RMSNorm weight + void const* routerWeight = nullptr; //!< [kNumExperts, kHiddenDim] bf16 + void const* routingBias = nullptr; //!< [kNumExperts] bf16 score-correction bias + //! Test only: when set, the routing scores are computed from these [numRows, kNumExperts] FP32 logits instead of + //! the computed router logits (which are still written to ``routerLogits``). Read after the dependency wait. + float const* logitsIn = nullptr; + void* normOut = nullptr; //!< [numRows, kHiddenDim] bf16 + void* residualOut = nullptr; //!< [numRows, kHiddenDim] bf16 + float* routerLogits = nullptr; //!< [numRows, kNumExperts] fp32 + int32_t* topkIds = nullptr; //!< [numRows, kTopK] int32, descending native key order + float* topkWeights = nullptr; //!< [numRows, kTopK] fp32 holding bf16-rounded weights + void* ucBuffer = nullptr; //!< local unicast view of the post-attention hop's multicast workspace + uint32_t* flags = nullptr; //!< [4] protocol state of that workspace + uint32_t* gather = nullptr; //!< [kGatherWords] routing gather buffers + uint32_t* zstate = nullptr; //!< [4] routing state; word 0 is the parity + //! Diagnostic builds only (``TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS``): [gridCtas, kNumStamps] globaltimer values. + unsigned long long* stamps = nullptr; + int numRows = 0; + int rank = 0; + float eps = 1e-5F; + float routedScaling = 2.5F; + bool enablePdl = true; + bool earlyPoll = false; + int routerLoad = 1; + cudaStream_t stream = nullptr; +}; + +using glm_moe_dsa::post_attention_hop::kHiddenDim; +using glm_moe_dsa::post_attention_hop::kLamportBufferBytes; +using glm_moe_dsa::post_attention_hop::kMaxRows; +using glm_moe_dsa::post_attention_hop::kNumExperts; +using glm_moe_dsa::post_attention_hop::kNumLamportBuffers; +using glm_moe_dsa::post_attention_hop::kNumRanks; +using glm_moe_dsa::post_attention_hop::kWorkspaceBytes; + +constexpr int kTopK = 8; +//! Words of one gather buffer and of both. +constexpr int kGatherBufferWords = kMaxRows * kNumExperts; +constexpr int kGatherWords = 2 * kGatherBufferWords; +constexpr int kStateWords = 4; +//! Diagnostic stamp fields per CTA: entry, wait, last logit store, last gather store, poll complete, level-1 done, +//! routing stored, end. +constexpr int kNumStamps = 8; +//! Upper bound of the grid size (4 expert groups x 3 row groups x 8 CTAs). +constexpr int kMaxGridCtas = 96; + +//! Whether this build records diagnostic stamps. +bool stampsCompiled(); + +//! Launch the routing hop on ``params.stream``. Returns the launch status. +cudaError_t invokePostAttentionHopRouting(PostAttentionHopRoutingParams const& params); + +} // namespace kernels::glm_moe_dsa::hop_routing + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.cu new file mode 100644 index 000000000000..efe8a1e21e09 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.cu @@ -0,0 +1,515 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.h" + +#include "tensorrt_llm/common/assert.h" +#include "tensorrt_llm/common/cudaUtils.h" +#include "tensorrt_llm/kernels/decoderMaskedMultiheadAttentionUtils.h" + +#include + +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::rope_kv_append +{ + +namespace +{ + +constexpr int32_t kRopeThreads = 256; +constexpr int32_t kBridgeThreads = 128; +constexpr int32_t kMaxGroupSize = 8; +constexpr int32_t kEltsPerVec = 8; // bf16 elements per 16-byte load +constexpr int32_t kNopeVecs = kKvLoraRank / kEltsPerVec; // 64 +constexpr int32_t kRopeVecs = kRopeDim / kEltsPerVec; // 8 +constexpr int32_t kTotalVecsPerHead = kNopeVecs + kRopeVecs; // 72 +constexpr int32_t kIdxBytesPerItem = 4; + +__device__ __forceinline__ void pdlWait() +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + asm volatile("griddepcontrol.wait;" ::: "memory"); +#endif +} + +__device__ __forceinline__ void pdlLaunchDependents() +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + asm volatile("griddepcontrol.launch_dependents;" ::: "memory"); +#endif +} + +//! Same arithmetic as quantCopy<__nv_bfloat16, 8> in mlaKernels.cu: bf16 -> fp32, scale, saturating E4M3. +__device__ __forceinline__ uint2 quantizeBf16x8(uint4 const& src, float scale) +{ + __nv_bfloat162 const* in = reinterpret_cast<__nv_bfloat162 const*>(&src); + uint2 out; + __nv_fp8x2_e4m3* dst = reinterpret_cast<__nv_fp8x2_e4m3*>(&out); +#pragma unroll + for (int32_t j = 0; j < 4; ++j) + { + float2 v = __bfloat1622float2(in[j]); + v.x *= scale; + v.y *= scale; + dst[j] = __nv_fp8x2_e4m3(v); + } + return out; +} + +//! GPT-J RoPE on 8 bf16 values (4 adjacent pairs), identical to the per-pair transform of the native kernel. +__device__ __forceinline__ void rotateBf16x8(uint4& data, float2 const (&coef)[4]) +{ + __nv_bfloat162* pairs = reinterpret_cast<__nv_bfloat162*>(&data); +#pragma unroll + for (int32_t j = 0; j < 4; ++j) + { + pairs[j] = mmha::rotary_embedding_transform(pairs[j], coef[j]); + } +} + +//! Native unravel of a flat byte index over the strided 4-D indexer cache view (indexerKCacheScatter.cu); +//! 32-bit arithmetic when the index fits, same result. +__device__ __forceinline__ int64_t idxFlatToOffset(int64_t flat, int32_t const (&dims)[4], int64_t const (&strides)[4]) +{ + int32_t i0, i1, i2, i3; + if (flat >= 0 && flat <= 0x7fffffff) + { + uint32_t f = static_cast(flat); + i3 = static_cast(f % static_cast(dims[3])); + f /= static_cast(dims[3]); + i2 = static_cast(f % static_cast(dims[2])); + f /= static_cast(dims[2]); + i1 = static_cast(f % static_cast(dims[1])); + f /= static_cast(dims[1]); + i0 = static_cast(f); + } + else + { + i3 = flat % dims[3]; + flat /= dims[3]; + i2 = flat % dims[2]; + flat /= dims[2]; + i1 = flat % dims[1]; + flat /= dims[1]; + i0 = flat; + } + return i0 * strides[0] + i1 * strides[1] + i2 * strides[2] + i3 * strides[3]; +} + +enum class RopeItem : int32_t +{ + kNone = 0, + kQNope, + kQRope, + kKRope, + kKNope, + kIdxData, + kIdxScale, +}; + +//! One thread per 16-byte work item of one token. Per token the item space is +//! [0, H*64) q_nope FP8 quantization (head = i / 64, vec = i % 64) +//! [H*64, H*72) q_pe RoPE + FP8 (head, vec) +//! [H*72, H*72+8) k_pe RoPE + FP8 cache write (vec) +//! [H*72+8, H*72+72) compressed-KV FP8 cache write (vec) +//! then idxHeadBytes/4 indexer K data items (4 bytes each) and one indexer scale item. +//! Per-step metadata (cache lengths, KV block offsets, indexer slot mappings) and the item's source data are read +//! only after griddepcontrol.wait (see the header). +__global__ void __launch_bounds__(kRopeThreads) ropeAppendScatterKernel(RopeAppendParams const p) +{ + int32_t const token = blockIdx.x; + int32_t const item = blockIdx.y * kRopeThreads + threadIdx.x; + int32_t const qNopeEnd = p.numHeads * kNopeVecs; + int32_t const qRopeEnd = qNopeEnd + p.numHeads * kRopeVecs; + int32_t const kRopeEnd = qRopeEnd + kRopeVecs; + int32_t const kNopeEnd = kRopeEnd + kNopeVecs; + int32_t const idxDataEnd = kNopeEnd + p.idxHeadBytes / kIdxBytesPerItem; + + bool const ropeToken = token < p.numTokens; + bool const idxToken = token < p.idxNumTokens; + + RopeItem kind = RopeItem::kNone; + int32_t head = 0; + int32_t vec = 0; + if (item < qNopeEnd) + { + kind = ropeToken ? RopeItem::kQNope : RopeItem::kNone; + head = item / kNopeVecs; + vec = item % kNopeVecs; + } + else if (item < qRopeEnd) + { + kind = ropeToken ? RopeItem::kQRope : RopeItem::kNone; + head = (item - qNopeEnd) / kRopeVecs; + vec = (item - qNopeEnd) % kRopeVecs; + } + else if (item < kRopeEnd) + { + kind = ropeToken ? RopeItem::kKRope : RopeItem::kNone; + vec = item - qRopeEnd; + } + else if (item < kNopeEnd) + { + kind = ropeToken ? RopeItem::kKNope : RopeItem::kNone; + vec = item - kRopeEnd; + } + else if (item < idxDataEnd) + { + kind = idxToken ? RopeItem::kIdxData : RopeItem::kNone; + vec = item - kNopeEnd; + } + else if (item == idxDataEnd) + { + kind = idxToken ? RopeItem::kIdxScale : RopeItem::kNone; + } + + int32_t const batchIdx = token / p.seqLen; + int32_t const localIdx = token % p.seqLen; + bool const needPos = kind == RopeItem::kQRope || kind == RopeItem::kKRope || kind == RopeItem::kKNope; + bool const needCoef = kind == RopeItem::kQRope || kind == RopeItem::kKRope; + bool const needBlock = kind == RopeItem::kKRope || kind == RopeItem::kKNope; + bool const isIdx = kind == RopeItem::kIdxData || kind == RopeItem::kIdxScale; + + // Values that never change while kernels of the step run (the quantization scales) may be read before the wait; + // per-step metadata and producer outputs may not. + float scale = 1.F; + if (needBlock) + { + scale = p.quantScaleKv ? p.quantScaleKv[0] : 1.F; + } + else if (kind == RopeItem::kQNope || kind == RopeItem::kQRope) + { + scale = p.quantScaleQ ? p.quantScaleQ[0] : 1.F; + } + auto loadCoef = [&](int32_t pos, float2 (&c)[4]) + { + float2 const* src = p.cosSin + static_cast(kRopeDim) * pos + (vec * kEltsPerVec) / 2; +#pragma unroll + for (int32_t j = 0; j < 4; ++j) + { + c[j] = src[j]; + } + }; + auto idxOffset = [&](int64_t slotData, int64_t slotScale) -> int64_t + { + if (slotData < 0 || slotScale < 0) + { + return -1; + } + return kind == RopeItem::kIdxData ? idxFlatToOffset(slotData + vec * kIdxBytesPerItem, p.idxDims, p.idxStrides) + : idxFlatToOffset(slotScale, p.idxDims, p.idxStrides); + }; + // Source data of this item; written by producers, so it is read after the wait. + uint4 vdata = make_uint4(0, 0, 0, 0); + uint32_t wdata = 0; + auto loadSource = [&]() + { + switch (kind) + { + case RopeItem::kQNope: + vdata = *reinterpret_cast( + p.fusedQ + (static_cast(token) * p.numHeads + head) * kLatentDim + vec * kEltsPerVec); + break; + case RopeItem::kQRope: + vdata = *reinterpret_cast(p.qPe + static_cast(token) * p.qPeStride + + static_cast(p.qPeLd) * head + vec * kEltsPerVec); + break; + case RopeItem::kKRope: + vdata = *reinterpret_cast( + p.latent + static_cast(token) * kLatentDim + kKvLoraRank + vec * kEltsPerVec); + break; + case RopeItem::kKNope: + vdata = *reinterpret_cast( + p.latent + static_cast(token) * kLatentDim + vec * kEltsPerVec); + break; + case RopeItem::kIdxData: + wdata = *reinterpret_cast( + p.idxK + static_cast(token) * p.idxHeadBytes + vec * kIdxBytesPerItem); + break; + case RopeItem::kIdxScale: + wdata = *reinterpret_cast(p.idxScale + static_cast(token) * kIdxBytesPerItem); + break; + default: break; + } + }; + + int32_t kvPos = 0; + float2 coef[4]; + __nv_fp8_e4m3* kBlock = nullptr; + int64_t idxDst = -1; + pdlWait(); + // Issue the source-data load first so it overlaps the metadata loads and the address math below. + loadSource(); + if (needPos) + { + kvPos = p.kvLens[batchIdx] - p.seqLen + localIdx; + } + if (needCoef) + { + loadCoef(kvPos, coef); + } + if (needBlock) + { + kBlock = reinterpret_cast<__nv_fp8_e4m3*>(p.kvCache.getKBlockPtr(batchIdx, kvPos)); + } + if (isIdx) + { + idxDst = idxOffset(p.idxSlotData[token], p.idxSlotScale[token]); + } + + switch (kind) + { + case RopeItem::kQNope: + { + size_t const off = (static_cast(token) * p.numHeads + head) * kLatentDim + vec * kEltsPerVec; + *reinterpret_cast(p.quantQ + off) = quantizeBf16x8(vdata, scale); + break; + } + case RopeItem::kQRope: + { + uint4 data = vdata; + rotateBf16x8(data, coef); + size_t const dst + = (static_cast(token) * p.numHeads + head) * kLatentDim + kKvLoraRank + vec * kEltsPerVec; + *reinterpret_cast(p.quantQ + dst) = quantizeBf16x8(data, scale); + break; + } + case RopeItem::kKRope: + { + uint4 data = vdata; + rotateBf16x8(data, coef); + int32_t const inBlockIdx = p.kvCache.getKVLocalIdx(kvPos, 0, kTotalVecsPerHead, kNopeVecs + vec); + *reinterpret_cast(kBlock + inBlockIdx * kEltsPerVec) = quantizeBf16x8(data, scale); + break; + } + case RopeItem::kKNope: + { + int32_t const inBlockIdx = p.kvCache.getKVLocalIdx(kvPos, 0, kTotalVecsPerHead, vec); + *reinterpret_cast(kBlock + inBlockIdx * kEltsPerVec) = quantizeBf16x8(vdata, scale); + if (vec == 0 && localIdx == 0) + { + p.cuQSeqlens[batchIdx + 1] = p.numHeads * p.seqLen * (batchIdx + 1); + } + break; + } + case RopeItem::kIdxData: + case RopeItem::kIdxScale: + { + if (idxDst >= 0) + { + *reinterpret_cast(p.idxCache + idxDst) = wdata; + } + break; + } + default: break; + } + + if (blockIdx.x == 0 && blockIdx.y == 0) + { + if (threadIdx.x == 0) + { + p.fmhaTileCounter[0] = 0; + p.cuQSeqlens[0] = 0; + float const dequantQ = p.dequantScaleQ ? p.dequantScaleQ[0] : 1.F; + float const dequantKv = p.dequantScaleKv ? p.dequantScaleKv[0] : 1.F; + float const quantO = p.quantScaleO ? p.quantScaleO[0] : 1.F; + if (p.bmm1Scale) + { + constexpr float kLog2e = 1.4426950408889634074F; + float const bmm1 = dequantQ * dequantKv * p.hostBmm1Scale; + p.bmm1Scale[0] = bmm1; + p.bmm1Scale[1] = bmm1 * kLog2e; + } + if (p.bmm2Scale) + { + p.bmm2Scale[0] = quantO * dequantKv; + } + } + if (threadIdx.x < 32) + { + // Exclusive prefix sum of the cache lengths -> cuKvSeqlens[0..batch]. + int32_t const lane = threadIdx.x; + int32_t const batch = p.numTokens / p.seqLen; + int32_t carry = 0; + for (int32_t base = 0; base <= batch; base += 32) + { + int32_t const idx = base + lane; + int32_t const len = idx < batch ? p.kvLens[idx] : 0; + int32_t incl = len; +#pragma unroll + for (int32_t off = 1; off < 32; off <<= 1) + { + int32_t const n = __shfl_up_sync(0xffffffffU, incl, off); + if (lane >= off) + { + incl += n; + } + } + if (idx <= batch) + { + p.cuKvSeqlens[idx] = incl - len + carry; + } + carry += __shfl_sync(0xffffffffU, incl, 31); + } + } + } + + pdlLaunchDependents(); +} + +__device__ __forceinline__ int32_t remapOne( + int32_t tok, int32_t layerId, int32_t blockSize, int32_t strideFactor, int32_t base) +{ + if (tok < 0 || base < 0) + { + return -1; + } + int32_t const inblockOff = tok % blockSize + layerId * blockSize; + return base * strideFactor + inblockOff; +} + +//! One thread per (row, column) element, like the native remap, so the scattered block-table reads spread over +//! many SMs; the group members' layer offsets are hoisted out of the store loop. Everything is read after the wait. +__global__ void __launch_bounds__(kBridgeThreads) topkBridgeKernel(TopkBridgeParams const p) +{ + int32_t const rowSlot = blockIdx.x; + int32_t const col = blockIdx.y * kBridgeThreads + threadIdx.x; + bool const isOutRow = rowSlot < p.numRows; + + int32_t req = 0; + int32_t srcRow = rowSlot; + int32_t layerIds[kMaxGroupSize]; + pdlWait(); + if (isOutRow) + { + req = p.reqIdx[rowSlot] - p.reqIdxBias; +#pragma unroll + for (int32_t g = 0; g < kMaxGroupSize; ++g) + { + layerIds[g] = g < p.groupSize ? (p.layerIds ? p.layerIds[g] : p.layerId) : 0; + } + } + else + { + int32_t const i = rowSlot - p.numRows; + if (p.numAccepted == nullptr) + { + srcRow = i * p.nextN + p.nextN - 1; + } + else + { + int64_t const acc = p.numAcceptedIs64 + ? static_cast(p.numAccepted)[i] + : static_cast(static_cast(p.numAccepted)[i]); + int64_t off = acc - 1; + off = off < 0 ? 0 : (off > p.nextN - 1 ? p.nextN - 1 : off); + srcRow = static_cast(static_cast(i) * p.nextN + off); + } + } + + if (col < p.numTopk) + { + int32_t const tok = p.topk[static_cast(srcRow) * p.topkStride0 + col]; + if (isOutRow) + { + int32_t base = -1; + if (tok >= 0) + { + int32_t const blockId = tok / p.blockSize; + if (blockId < p.maxBlocksPerReq) + { + base = p.blockTable[req * p.btStride0 + blockId * p.btStride1]; + } + } + int32_t* dst = p.out + static_cast(rowSlot) * p.outStride0 + col; +#pragma unroll + for (int32_t g = 0; g < kMaxGroupSize; ++g) + { + if (g < p.groupSize) + { + dst[g * p.outStrideG] = remapOne(tok, layerIds[g], p.blockSize, p.strideFactor, base); + } + } + } + bool const doCopy = p.copyDst != nullptr + && ((p.copyMode == 0 && isOutRow && rowSlot < p.copyRows) || (p.copyMode == 1 && !isOutRow)); + if (doCopy) + { + int32_t const copyRow = p.copyRowStart + (isOutRow ? rowSlot : rowSlot - p.numRows); + p.copyDst[static_cast(copyRow) * p.copyStride0 + col] = tok; + } + } + + pdlLaunchDependents(); +} + +template +void launchWithOptionalPdl(Kernel kernel, dim3 grid, dim3 block, bool enablePdl, cudaStream_t stream, Params const& p) +{ + cudaLaunchConfig_t config{}; + config.gridDim = grid; + config.blockDim = block; + config.dynamicSmemBytes = 0; + config.stream = stream; + cudaLaunchAttribute attrs[1]; + attrs[0].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[0].val.programmaticStreamSerializationAllowed = enablePdl ? 1 : 0; + config.numAttrs = 1; + config.attrs = attrs; + TLLM_CUDA_CHECK(cudaLaunchKernelEx(&config, kernel, p)); +} + +} // namespace + +void invokeRopeAppendScatter(RopeAppendParams const& params, bool enablePdl, cudaStream_t stream) +{ + int32_t const gridTokens = std::max(params.numTokens, params.idxNumTokens); + if (gridTokens == 0) + { + return; + } + TLLM_CHECK_WITH_INFO(params.seqLen > 0, "seqLen must be positive"); + TLLM_CHECK_WITH_INFO(params.cosSinRows > 0, "the RoPE table must have at least one position"); + TLLM_CHECK_WITH_INFO(params.idxHeadBytes % kIdxBytesPerItem == 0, "indexer head bytes must be a multiple of 4"); + int32_t const itemsPerToken + = params.numHeads * kTotalVecsPerHead + kTotalVecsPerHead + params.idxHeadBytes / kIdxBytesPerItem + 1; + dim3 const grid(gridTokens, (itemsPerToken + kRopeThreads - 1) / kRopeThreads); + dim3 const block(kRopeThreads); + launchWithOptionalPdl(ropeAppendScatterKernel, grid, block, enablePdl, stream, params); +} + +void invokeTopkBridge(TopkBridgeParams const& params, bool enablePdl, cudaStream_t stream) +{ + int32_t const extraRows = params.copyMode == 1 ? params.copyRows : 0; + int32_t const rows = params.numRows + extraRows; + if (rows == 0 || params.numTopk == 0) + { + return; + } + TLLM_CHECK_WITH_INFO(params.groupSize >= 1 && params.groupSize <= kMaxGroupSize, + "glm_moe_dsa_topk_index_bridge: group sizes must be 1..%d, got %d", kMaxGroupSize, params.groupSize); + dim3 const grid(rows, (params.numTopk + kBridgeThreads - 1) / kBridgeThreads); + dim3 const block(kBridgeThreads); + launchWithOptionalPdl(topkBridgeKernel, grid, block, enablePdl, stream, params); +} + +} // namespace kernels::glm_moe_dsa::rope_kv_append + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.h new file mode 100644 index 000000000000..8f12c8b6cd61 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.h @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/kvCacheUtils.h" + +#include +#include + +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::rope_kv_append +{ + +//! MLA absorption-mode latent width and RoPE width supported by the fused kernels. +constexpr int32_t kKvLoraRank = 512; +constexpr int32_t kRopeDim = 64; +constexpr int32_t kLatentDim = kKvLoraRank + kRopeDim; + +//! Memory order of both kernels: they may start early as programmatic dependents (PDL) and read everything a +//! producer writes, including per-step metadata (cache lengths, KV block offsets, indexer slot mappings, request +//! indices, MTP accepted counts), only after griddepcontrol.wait. Only the quantization scales, which no kernel of the +//! step writes, are read before the wait. + +//! Generation-phase MLA RoPE + FP8 query quantization + FP8 latent KV append, optionally fused with the DSA +//! indexer K-cache scatter. With an FP8 MLA cache the outputs are byte-identical to +//! applyMLARopeAndAssignQKVKernelGeneration (absorption mode, rope_append, no helix) followed by +//! indexerKCacheScatterUnifiedKernel. +struct RopeAppendParams +{ + // Query side. + __nv_bfloat16 const* fusedQ{nullptr}; //!< [numTokens, numHeads, kLatentDim]; the first kKvLoraRank are read. + __nv_bfloat16 const* qPe{nullptr}; //!< [numTokens, numHeads, kRopeDim] with strides (qPeStride, qPeLd, 1). + int32_t qPeLd{0}; + int32_t qPeStride{0}; + uint8_t* quantQ{nullptr}; //!< FP8 E4M3 [numTokens, numHeads, kLatentDim]. + float const* quantScaleQ{nullptr}; //!< Optional; 1.0 when null. + + // KV side. + __nv_bfloat16 const* latent{nullptr}; //!< [numTokens, kLatentDim] rows (packed). + float2 const* cosSin{nullptr}; //!< Rotary table addressed as cosSin[kRopeDim * position + d / 2]. + int32_t cosSinRows{0}; //!< Positions in the rotary table. + int32_t const* kvLens{nullptr}; //!< [batch] cache lengths after this step's append. + float const* quantScaleKv{nullptr}; //!< Optional; 1.0 when null. + KVBlockArray kvCache; + + // Scheduler buffers consumed by the FP8 MLA generation FMHA. + int32_t* cuQSeqlens{nullptr}; + int32_t* cuKvSeqlens{nullptr}; + uint32_t* fmhaTileCounter{nullptr}; + float* bmm1Scale{nullptr}; //!< Optional [2]. + float* bmm2Scale{nullptr}; //!< Optional [1]. + float const* dequantScaleQ{nullptr}; //!< Optional; 1.0 when null. + float const* dequantScaleKv{nullptr}; //!< Optional; 1.0 when null. + float const* quantScaleO{nullptr}; //!< Optional; 1.0 when null. + float hostBmm1Scale{1.F}; + + int32_t numTokens{0}; + int32_t seqLen{1}; + int32_t numHeads{0}; + + // Optional DSA indexer K-cache scatter (skipped when idxNumTokens == 0). + uint8_t const* idxK{nullptr}; //!< [idxNumTokens, idxHeadBytes] bytes. + uint8_t const* idxScale{nullptr}; //!< [idxNumTokens, 4] bytes. + uint8_t* idxCache{nullptr}; //!< Strided 4-D byte view of one layer's indexer pool. + int64_t const* idxSlotData{nullptr}; + int64_t const* idxSlotScale{nullptr}; + int32_t idxNumTokens{0}; + int32_t idxHeadBytes{128}; + int32_t idxDims[4]{0, 0, 0, 0}; + int64_t idxStrides[4]{0, 0, 0, 0}; +}; + +//! Fused bridge from per-row local top-k token ids to global KV-pool indices, optionally writing the native +//! top-k reuse copy in the same pass. Output values are identical to convertReqIndexToGlobal[Grouped]Kernel. +struct TopkBridgeParams +{ + int32_t const* topk{nullptr}; //!< Source rows [srcRows, numTopk] with row stride topkStride0, unit column stride. + int64_t topkStride0{0}; + int32_t numRows{0}; //!< Rows remapped to global indices. + int32_t numTopk{0}; + + int32_t const* reqIdx{nullptr}; //!< [numRows] request index per row, before subtracting reqIdxBias. + int32_t reqIdxBias{0}; + int32_t const* blockTable{nullptr}; + int64_t btStride0{0}; + int64_t btStride1{1}; + int32_t maxBlocksPerReq{0}; + int32_t blockSize{0}; + int32_t strideFactor{0}; + + int32_t const* layerIds{nullptr}; //!< Optional [groupSize]; when null the single layerId is used. + int32_t layerId{0}; + int32_t groupSize{1}; + int32_t* out{nullptr}; //!< [groupSize, numRows, numTopk] with strides (outStrideG, outStride0, 1). + int64_t outStrideG{0}; + int64_t outStride0{0}; + + //! Copy rows: dst[copyRowStart + i, :numTopk] = topk[srcRow(i), :] for i in [0, copyRows). + //! srcRow(i) = i (copyMode 0) or the MTP last-accepted row i * nextN + clamp(numAccepted[i] - 1, 0, nextN - 1) + //! (copyMode 1; i * nextN + nextN - 1 when numAccepted is null). + int32_t* copyDst{nullptr}; + int64_t copyStride0{0}; + int32_t copyRows{0}; + int32_t copyRowStart{0}; + int32_t copyMode{0}; + int32_t nextN{1}; + void const* numAccepted{nullptr}; //!< Already offset to the first generation request. + bool numAcceptedIs64{false}; +}; + +void invokeRopeAppendScatter(RopeAppendParams const& params, bool enablePdl, cudaStream_t stream); + +void invokeTopkBridge(TopkBridgeParams const& params, bool enablePdl, cudaStream_t stream); + +} // namespace kernels::glm_moe_dsa::rope_kv_append + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.cu b/cpp/tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.cu new file mode 100644 index 000000000000..b9c0a82f04ec --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.cu @@ -0,0 +1,714 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/cudaUtils.h" +#include "tensorrt_llm/common/envUtils.h" +#include "tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.h" + +#include +#include + +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::sparse_mla_decode +{ +namespace +{ + +// Work split: one CTA handles one query row and 128 consecutive positions of its index list. +// The 16 CTAs of a row form one thread-block cluster. Each CTA pushes its unnormalized partial +// output and softmax statistics into the shared memory of the CTA that finalizes those output +// dims with asynchronous stores that complete on that CTA's mbarrier. +int constexpr kKeysPerCta = 128; +int constexpr kNumSplits = kTopK / kKeysPerCta; +int constexpr kNumWarps = 8; +int constexpr kThreads = kNumWarps * 32; +int constexpr kKeysPerWarp = kKeysPerCta / kNumWarps; +int constexpr kDimsPerWarp = kHeadDimV / kNumWarps; +int constexpr kDimsPerCtaOut = kHeadDimV / kNumSplits; +[[maybe_unused]] int constexpr kQkSteps = kHeadDimQk / 32; +[[maybe_unused]] int constexpr kPvSteps = kKeysPerCta / 32; +[[maybe_unused]] int constexpr kPvTilesPerWarp = kDimsPerWarp / 16; +[[maybe_unused]] int constexpr kQChunks = kNumHeads * kHeadDimQk / 16; +// Probabilities are scaled to the e4m3 range before rounding, as in the native kernel. +[[maybe_unused]] float constexpr kProbScale = 448.F; + +// Shared-memory pitches, padded so that ldmatrix and fragment loads are bank-conflict free: K rows +// shift by 20 banks (eight ldmatrix rows hit disjoint 16-byte bank groups); the 64-bit query +// fragment loads of rows g = 0-3 cover banks 8g..8g+7 (pitch = 8 banks mod 32); the 32-bit +// probability fragment loads of rows g = 0-7 cover banks 4g..4g+3. +int constexpr kKRowBytes = kHeadDimQk + 16; +int constexpr kQRowHalves = kHeadDimQk + 16; +int constexpr kPRowHalves = kKeysPerCta + 8; + +int constexpr kKTileOffset = 0; +int constexpr kQTileOffset = kKTileOffset + kKeysPerCta * kKRowBytes; +int constexpr kPTileOffset = kQTileOffset + kNumHeads * kQRowHalves * 2; +// [source split][head][dim within this CTA's 32 output dims] FP32 partial outputs. +int constexpr kRecvOffset = kPTileOffset + kNumHeads * kPRowHalves * 2; +// [max of heads 0-7, sum of heads 0-7][source split] FP32. +int constexpr kRecvStatsOffset = kRecvOffset + kNumSplits * kNumHeads * kDimsPerCtaOut * 4; +int constexpr kRedMaxOffset = kRecvStatsOffset + kNumSplits * 2 * kNumHeads * 4; +int constexpr kRedSumOffset = kRedMaxOffset + kNumWarps * kNumHeads * 4; +// mbarriers: one per warp for its row gather, one for the pushed partials of this CTA. +int constexpr kBarrierOffset = kRedSumOffset + kNumWarps * kNumHeads * 4; +int constexpr kSmemBytes = kBarrierOffset + (kNumWarps + 1) * 8; +// Bytes each CTA receives: 16 sources x (8 heads x 32 dims + 16 statistics) FP32 values. +[[maybe_unused]] uint32_t constexpr kRecvBytes = kNumSplits * (kNumHeads * kDimsPerCtaOut + 2 * kNumHeads) * 4; + +static_assert(kTopK % kKeysPerCta == 0, "the index list must split evenly"); +static_assert(kNumSplits <= 16, "the merge cluster holds at most 16 CTAs"); +static_assert(kKeysPerWarp == 16, "one m16 tile of keys per warp"); +static_assert(kNumHeads == 8, "the eight heads form the n8 side of the MMAs"); +static_assert(kDimsPerCtaOut == 32 && kDimsPerWarp == 2 * kDimsPerCtaOut, "each warp feeds two merge CTAs"); +static_assert(kDimsPerCtaOut * kNumHeads == kThreads, "one merged output element per thread"); +static_assert(kKRowBytes % 16 == 0 && kQRowHalves % 8 == 0 && kPRowHalves % 8 == 0, "16-byte aligned shared rows"); +static_assert( + kKRowBytes / 4 % 32 == 20 && kQRowHalves / 2 % 32 == 8 && kPRowHalves / 2 % 32 == 4, "bank-conflict-free pitches"); +static_assert(kBarrierOffset % 8 == 0, "mbarrier alignment"); +static_assert(kQChunks <= 2 * kThreads, "two query chunks per thread at most"); +static_assert(2 * kNumHeads * kNumSplits == kThreads, "one statistics store per thread"); +static_assert( + kNumWarps == kNumHeads && (kNumSplits & (kNumSplits - 1)) == 0, "merge: one warp per head, one lane per split"); + +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + +__device__ __forceinline__ uint32_t smemAddr(void const* ptr) +{ + return static_cast(__cvta_generic_to_shared(ptr)); +} + +__device__ __forceinline__ void mbarrierInit(uint32_t bar, uint32_t count) +{ + asm volatile("mbarrier.init.shared::cta.b64 [%0], %1;\n" ::"r"(bar), "r"(count) : "memory"); +} + +__device__ __forceinline__ void fenceBarrierInit() +{ + asm volatile("fence.mbarrier_init.release.cluster;\n" ::: "memory"); +} + +__device__ __forceinline__ void mbarrierArriveExpectTx(uint32_t bar, uint32_t bytes) +{ + asm volatile("mbarrier.arrive.expect_tx.shared::cta.b64 _, [%0], %1;\n" ::"r"(bar), "r"(bytes) : "memory"); +} + +__device__ __forceinline__ void mbarrierArrive(uint32_t bar) +{ + asm volatile("mbarrier.arrive.shared::cta.b64 _, [%0];\n" ::"r"(bar) : "memory"); +} + +__device__ __forceinline__ void mbarrierWait(uint32_t bar, uint32_t parity) +{ + uint32_t done = 0; + while (!done) + { + asm volatile( + "{\n" + ".reg .pred p;\n" + "mbarrier.try_wait.parity.shared::cta.b64 p, [%1], %2;\n" + "selp.u32 %0, 1, 0, p;\n" + "}\n" + : "=r"(done) + : "r"(bar), "r"(parity) + : "memory"); + } +} + +// The same wait with cluster-scope acquire: the arrivals and transaction bytes it waits for come +// from asynchronous stores issued by other CTAs of the cluster. +__device__ __forceinline__ void mbarrierWaitCluster(uint32_t bar, uint32_t parity) +{ + uint32_t done = 0; + while (!done) + { + asm volatile( + "{\n" + ".reg .pred p;\n" + "mbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 p, [%1], %2;\n" + "selp.u32 %0, 1, 0, p;\n" + "}\n" + : "=r"(done) + : "r"(bar), "r"(parity) + : "memory"); + } +} + +__device__ __forceinline__ void bulkCopyG2S(uint32_t dst, void const* src, uint32_t bytes, uint32_t bar) +{ + asm volatile("cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes [%0], [%1], %2, [%3];\n" ::"r"(dst), + "l"(src), "r"(bytes), "r"(bar) + : "memory"); +} + +__device__ __forceinline__ void gridDependencyWait() +{ + asm volatile("griddepcontrol.wait;\n" ::: "memory"); +} + +__device__ __forceinline__ void gridDependencyLaunchDependents() +{ + asm volatile("griddepcontrol.launch_dependents;\n" ::: "memory"); +} + +__device__ __forceinline__ void clusterArriveRelaxed() +{ + asm volatile("barrier.cluster.arrive.relaxed.aligned;\n" ::: "memory"); +} + +__device__ __forceinline__ void clusterWait() +{ + asm volatile("barrier.cluster.wait.acquire.aligned;\n" ::: "memory"); +} + +__device__ __forceinline__ uint32_t mapToRank(uint32_t localAddr, uint32_t rank) +{ + uint32_t remote; + asm volatile("mapa.shared::cluster.u32 %0, %1, %2;\n" : "=r"(remote) : "r"(localAddr), "r"(rank)); + return remote; +} + +// Asynchronous stores into a peer CTA's shared memory that complete-tx on the peer's mbarrier. +__device__ __forceinline__ void storeAsync(uint32_t remoteAddr, float value, uint32_t remoteBar) +{ + asm volatile("st.async.shared::cluster.mbarrier::complete_tx::bytes.f32 [%0], %1, [%2];\n" ::"r"(remoteAddr), + "f"(value), "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void storeAsync2(uint32_t remoteAddr, float a, float b, uint32_t remoteBar) +{ + asm volatile( + "st.async.shared::cluster.mbarrier::complete_tx::bytes.v2.f32 [%0], {%1, %2}, [%3];\n" ::"r"(remoteAddr), + "f"(a), "f"(b), "r"(remoteBar) + : "memory"); +} + +__device__ __forceinline__ void ldmatrixX4(uint32_t (&r)[4], uint32_t addr) +{ + asm volatile("ldmatrix.sync.aligned.m8n8.x4.shared.b16 {%0, %1, %2, %3}, [%4];\n" + : "=r"(r[0]), "=r"(r[1]), "=r"(r[2]), "=r"(r[3]) + : "r"(addr)); +} + +__device__ __forceinline__ void ldmatrixX4Trans(uint32_t (&r)[4], uint32_t addr) +{ + asm volatile("ldmatrix.sync.aligned.m8n8.x4.trans.shared.b16 {%0, %1, %2, %3}, [%4];\n" + : "=r"(r[0]), "=r"(r[1]), "=r"(r[2]), "=r"(r[3]) + : "r"(addr)); +} + +// D += A * B with A 16x16 f16 (row), B 16x8 f16 (col), FP32 accumulation. +__device__ __forceinline__ void mmaF16( + float (&d)[4], uint32_t a0, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t b0, uint32_t b1) +{ + asm volatile( + "mma.sync.aligned.m16n8k16.row.col.f32.f16.f16.f32 {%0, %1, %2, %3}, {%4, %5, %6, %7}, {%8, %9}, " + "{%0, %1, %2, %3};\n" + : "+f"(d[0]), "+f"(d[1]), "+f"(d[2]), "+f"(d[3]) + : "r"(a0), "r"(a1), "r"(a2), "r"(a3), "r"(b0), "r"(b1)); +} + +__device__ __forceinline__ float fastExp2(float x) +{ + float y; + asm volatile("ex2.approx.ftz.f32 %0, %1;\n" : "=f"(y) : "f"(x)); + return y; +} + +// Two e4m3 values (low byte first) to two FP16 values; exact. +__device__ __forceinline__ uint32_t e4m3x2ToHalf2(uint32_t pair) +{ + __half2_raw const h = __nv_cvt_fp8x2_to_halfraw2(static_cast<__nv_fp8x2_storage_t>(pair & 0xffffU), __NV_E4M3); + return static_cast(h.x) | (static_cast(h.y) << 16); +} + +// e4m3(448 * p) as an exact FP16 value. +__device__ __forceinline__ __half roundProbability(float p) +{ + __nv_fp8_storage_t const q = __nv_cvt_float_to_fp8(p * kProbScale, __NV_SATFINITE, __NV_E4M3); + return __half(__nv_cvt_fp8_to_halfraw(q, __NV_E4M3)); +} + +#endif // defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + +#ifdef GLM_MOE_DSA_SPARSE_MLA_DECODE_TIMING +// Development timeline: thread 0 of each CTA records the SM clock at phase boundaries and the +// global timer at entry and exit. +__device__ unsigned long long gTimeline[kTimelineMaxCtas][kTimelineSlots]; +#endif + +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) +#ifdef GLM_MOE_DSA_SPARSE_MLA_DECODE_TIMING +__device__ __forceinline__ void markTimeline(int slot) +{ + if (threadIdx.x == 0 && blockIdx.x < kTimelineMaxCtas) + { + gTimeline[blockIdx.x][slot] = clock64(); + } +} + +__device__ __forceinline__ void markGlobalTime(int slot) +{ + if (threadIdx.x == 0 && blockIdx.x < kTimelineMaxCtas) + { + unsigned long long t; + asm volatile("mov.u64 %0, %%globaltimer;" : "=l"(t)); + gTimeline[blockIdx.x][slot] = t; + } +} +#else +__device__ __forceinline__ void markTimeline(int) {} + +__device__ __forceinline__ void markGlobalTime(int) {} +#endif +#endif // defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + +__global__ void __launch_bounds__(kThreads) sparseMlaDecodeKernel(SparseMlaDecodeParams const params) +{ +#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900) + extern __shared__ __align__(128) uint8_t smem[]; + + int const tid = threadIdx.x; + int const warp = tid >> 5; + int const lane = tid & 31; + int const groupId = lane >> 2; + int const tig = lane & 3; + int const row = blockIdx.x / kNumSplits; + int const split = blockIdx.x % kNumSplits; + + uint8_t* kTile = smem + kKTileOffset; + __half* qTile = reinterpret_cast<__half*>(smem + kQTileOffset); + __half* pTile = reinterpret_cast<__half*>(smem + kPTileOffset); + float* recv = reinterpret_cast(smem + kRecvOffset); + float* recvStats = reinterpret_cast(smem + kRecvStatsOffset); + float* redMax = reinterpret_cast(smem + kRedMaxOffset); + float* redSum = reinterpret_cast(smem + kRedSumOffset); + uint32_t const gatherBar = smemAddr(smem + kBarrierOffset + warp * 8); + uint32_t const recvBar = smemAddr(smem + kBarrierOffset + kNumWarps * 8); + + markGlobalTime(0); + markTimeline(1); + if (lane == 0) + { + mbarrierInit(gatherBar, kKeysPerWarp); + } + if (tid == 0) + { + mbarrierInit(recvBar, 1); + } + // The initializations must be visible to the bulk-copy engine and to the peers' async stores. + fenceBarrierInit(); + __syncthreads(); + clusterArriveRelaxed(); + + // Everything below reads tensors written by preceding kernels in the stream. The kernel can be + // resident before those kernels finish, so these tensors are read with coherent loads, not + // through the non-coherent read-only path. + gridDependencyWait(); + markTimeline(2); + + // The query does not depend on the index list: start its loads first. + uint8_t const* qRow = static_cast(params.q) + static_cast(row) * kNumHeads * kHeadDimQk; + uint4 qRaw[2]; +#pragma unroll + for (int i = 0; i < 2; ++i) + { + int const chunk = tid + i * kThreads; + if (chunk < kQChunks) + { + qRaw[i] = reinterpret_cast(qRow)[chunk]; + } + } + + int32_t poolRow = -1; + if (lane < kKeysPerWarp) + { + int const seq = row / params.seqLenQ; + int const rowInSeq = row - seq * params.seqLenQ; + int const key = split * kKeysPerCta + warp * kKeysPerWarp + lane; + int32_t const listed = params.indices[static_cast(row) * params.indicesRowStride + key]; + int const usable = params.seqLensKv[seq] - (params.seqLenQ - 1 - rowInSeq); + poolRow = key < usable ? listed : -1; + } + uint32_t const validBits = __ballot_sync(0xffffffffU, poolRow >= 0); + markTimeline(3); + + // Gather this warp's 16 rows with one 576-byte bulk copy each; unusable rows are zeroed so that + // zero probabilities never meet non-finite bytes. + if (lane < kKeysPerWarp) + { + uint8_t* dst = kTile + (warp * kKeysPerWarp + lane) * kKRowBytes; + if (poolRow >= 0) + { + mbarrierArriveExpectTx(gatherBar, kHeadDimQk); + bulkCopyG2S(smemAddr(dst), + static_cast(params.kvPool) + static_cast(poolRow) * kHeadDimQk, kHeadDimQk, + gatherBar); + } + else + { +#pragma unroll 4 + for (int c = 0; c < kHeadDimQk / 16; ++c) + { + *reinterpret_cast(dst + c * 16) = make_uint4(0U, 0U, 0U, 0U); + } + mbarrierArrive(gatherBar); + } + } + + // FP16 copy of the query, shared by all warps. +#pragma unroll + for (int i = 0; i < 2; ++i) + { + int const chunk = tid + i * kThreads; + if (chunk < kQChunks) + { + int const head = chunk / (kHeadDimQk / 16); + int const col = (chunk - head * (kHeadDimQk / 16)) * 16; + uint4 lo; + uint4 hi; + lo.x = e4m3x2ToHalf2(qRaw[i].x); + lo.y = e4m3x2ToHalf2(qRaw[i].x >> 16); + lo.z = e4m3x2ToHalf2(qRaw[i].y); + lo.w = e4m3x2ToHalf2(qRaw[i].y >> 16); + hi.x = e4m3x2ToHalf2(qRaw[i].z); + hi.y = e4m3x2ToHalf2(qRaw[i].z >> 16); + hi.z = e4m3x2ToHalf2(qRaw[i].w); + hi.w = e4m3x2ToHalf2(qRaw[i].w >> 16); + uint4* dst = reinterpret_cast(qTile + head * kQRowHalves + col); + dst[0] = lo; + dst[1] = hi; + } + } + float const scaleLog2 = params.bmm1Scale[1]; + float const outScale = params.bmm2Scale[0]; + + gridDependencyLaunchDependents(); + __syncthreads(); + + // Every peer has initialized its barriers once phase 0 of the cluster barrier completes; waiting + // here overlaps that with the gather. Pushes into peers start after the softmax. + clusterWait(); + mbarrierWait(gatherBar, 0); + __syncwarp(); + markTimeline(4); + + // S^T = K * Q^T for this warp's 16 keys: keys on the M side, heads on the N side, FP16 MMAs on + // converted e4m3 keys. Within each 16-value k chunk the MMA k-slot order is permuted so that + // the two halves of an ldmatrix register map straight onto the A fragment: slots 2t, 2t+1 hold + // k = 4t, 4t+1 and slots 2t+8, 2t+9 hold k = 4t+2, 4t+3. The FP16 query is read in that order. + float sAcc[4]; + { + float acc[2][4] = {}; + uint32_t const aAddr = smemAddr(kTile + (warp * kKeysPerWarp + (lane & 15)) * kKRowBytes + (lane >> 4) * 16); + __half const* qFrag = qTile + groupId * kQRowHalves + 4 * tig; +#pragma unroll + for (int kk = 0; kk < kQkSteps; ++kk) + { + uint32_t r[4]; + ldmatrixX4(r, aAddr + kk * 32); + uint2 const b0 = *reinterpret_cast(qFrag + kk * 32); + uint2 const b1 = *reinterpret_cast(qFrag + kk * 32 + 16); + mmaF16(acc[0], e4m3x2ToHalf2(r[0]), e4m3x2ToHalf2(r[1]), e4m3x2ToHalf2(r[0] >> 16), + e4m3x2ToHalf2(r[1] >> 16), b0.x, b0.y); + mmaF16(acc[1], e4m3x2ToHalf2(r[2]), e4m3x2ToHalf2(r[3]), e4m3x2ToHalf2(r[2] >> 16), + e4m3x2ToHalf2(r[3] >> 16), b1.x, b1.y); + } +#pragma unroll + for (int i = 0; i < 4; ++i) + { + sAcc[i] = acc[0][i] + acc[1][i]; + } + } + markTimeline(5); + + // Fragment element (key, head): 0 -> (groupId, 2*tig), 1 -> (groupId, 2*tig+1), 2/3 -> key + 8. + bool const valid0 = (validBits >> groupId) & 1U; + bool const valid1 = (validBits >> (groupId + 8)) & 1U; + float s[4]; + s[0] = valid0 ? sAcc[0] * scaleLog2 : -INFINITY; + s[1] = valid0 ? sAcc[1] * scaleLog2 : -INFINITY; + s[2] = valid1 ? sAcc[2] * scaleLog2 : -INFINITY; + s[3] = valid1 ? sAcc[3] * scaleLog2 : -INFINITY; + + float maxA = fmaxf(s[0], s[2]); + float maxB = fmaxf(s[1], s[3]); +#pragma unroll + for (int offset = 4; offset < 32; offset <<= 1) + { + maxA = fmaxf(maxA, __shfl_xor_sync(0xffffffffU, maxA, offset)); + maxB = fmaxf(maxB, __shfl_xor_sync(0xffffffffU, maxB, offset)); + } + if (lane < 4) + { + redMax[warp * kNumHeads + 2 * tig] = maxA; + redMax[warp * kNumHeads + 2 * tig + 1] = maxB; + } + __syncthreads(); + + float headMaxA = -INFINITY; + float headMaxB = -INFINITY; +#pragma unroll + for (int w = 0; w < kNumWarps; ++w) + { + headMaxA = fmaxf(headMaxA, redMax[w * kNumHeads + 2 * tig]); + headMaxB = fmaxf(headMaxB, redMax[w * kNumHeads + 2 * tig + 1]); + } + float p[4]; + p[0] = s[0] == -INFINITY ? 0.F : fastExp2(s[0] - headMaxA); + p[1] = s[1] == -INFINITY ? 0.F : fastExp2(s[1] - headMaxB); + p[2] = s[2] == -INFINITY ? 0.F : fastExp2(s[2] - headMaxA); + p[3] = s[3] == -INFINITY ? 0.F : fastExp2(s[3] - headMaxB); + + float sumA = p[0] + p[2]; + float sumB = p[1] + p[3]; +#pragma unroll + for (int offset = 4; offset < 32; offset <<= 1) + { + sumA += __shfl_xor_sync(0xffffffffU, sumA, offset); + sumB += __shfl_xor_sync(0xffffffffU, sumB, offset); + } + if (lane < 4) + { + redSum[warp * kNumHeads + 2 * tig] = sumA; + redSum[warp * kNumHeads + 2 * tig + 1] = sumB; + } + { + int const key = warp * kKeysPerWarp + groupId; + pTile[(2 * tig) * kPRowHalves + key] = roundProbability(p[0]); + pTile[(2 * tig + 1) * kPRowHalves + key] = roundProbability(p[1]); + pTile[(2 * tig) * kPRowHalves + key + 8] = roundProbability(p[2]); + pTile[(2 * tig + 1) * kPRowHalves + key + 8] = roundProbability(p[3]); + } + __syncthreads(); + + // This CTA's softmax statistics to every CTA of the cluster: thread t sends value t / 16 + // (maxima of heads 0-7, then sums) to rank t % 16. + { + int const index = tid / kNumSplits; + int const dest = tid % kNumSplits; + int const head = index & (kNumHeads - 1); + bool const isMax = index < kNumHeads; + float value = isMax ? -INFINITY : 0.F; +#pragma unroll + for (int w = 0; w < kNumWarps; ++w) + { + value = isMax ? fmaxf(value, redMax[w * kNumHeads + head]) : value + redSum[w * kNumHeads + head]; + } + storeAsync(mapToRank(smemAddr(recvStats + index * kNumSplits + split), dest), value, mapToRank(recvBar, dest)); + } + markTimeline(6); + + // O^T = V^T * P^T for this warp's 64 output dims, one 16-dim tile at a time; each finished tile + // is pushed to the CTA that finalizes its dims while the next tile is computed. Transposing the + // e4m3 V rows with b16 ldmatrix yields keys (2t, 2t+1) x dims (2g, 2g+1) per register; a byte + // permute splits it into the two dims, so M row g of a tile is dim 2g and M row g+8 is dim 2g+1. + { + uint32_t pFrag[kPvSteps][2][2]; + __half const* pRow = pTile + groupId * kPRowHalves + 2 * tig; +#pragma unroll + for (int ks = 0; ks < kPvSteps; ++ks) + { +#pragma unroll + for (int half = 0; half < 2; ++half) + { + pFrag[ks][half][0] = *reinterpret_cast(pRow + ks * 32 + half * 16); + pFrag[ks][half][1] = *reinterpret_cast(pRow + ks * 32 + half * 16 + 8); + } + } + uint32_t const vAddr = smemAddr(kTile + lane * kKRowBytes + warp * kDimsPerWarp); + uint32_t const recvBase = smemAddr(recv); + float const invProbScale = 1.F / kProbScale; +#pragma unroll + for (int mt = 0; mt < kPvTilesPerWarp; ++mt) + { + // Independent accumulators per MMA keep the tensor pipe busy within one tile. + float part[kPvSteps][2][4] = {}; +#pragma unroll + for (int ks = 0; ks < kPvSteps; ++ks) + { + uint32_t r[4]; + ldmatrixX4Trans(r, vAddr + ks * 32 * kKRowBytes + mt * 16); + uint32_t dimEven[4]; + uint32_t dimOdd[4]; +#pragma unroll + for (int j = 0; j < 4; ++j) + { + uint32_t const pairs = __byte_perm(r[j], 0, 0x3120); + dimEven[j] = e4m3x2ToHalf2(pairs); + dimOdd[j] = e4m3x2ToHalf2(pairs >> 16); + } + mmaF16(part[ks][0], dimEven[0], dimOdd[0], dimEven[1], dimOdd[1], pFrag[ks][0][0], pFrag[ks][0][1]); + mmaF16(part[ks][1], dimEven[2], dimOdd[2], dimEven[3], dimOdd[3], pFrag[ks][1][0], pFrag[ks][1][1]); + } + float o[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) + { + o[i] = ((part[0][0][i] + part[0][1][i]) + (part[1][0][i] + part[1][1][i])) + + ((part[2][0][i] + part[2][1][i]) + (part[3][0][i] + part[3][1][i])); + } + int const dim = warp * kDimsPerWarp + mt * 16 + 2 * groupId; + uint32_t const dest = dim / kDimsPerCtaOut; + int const localDim = dim - static_cast(dest) * kDimsPerCtaOut; + uint32_t const addrA + = recvBase + ((split * kNumHeads + 2 * tig) * kDimsPerCtaOut + localDim) * sizeof(float); + uint32_t const addrB = addrA + kDimsPerCtaOut * sizeof(float); + uint32_t const remoteBar = mapToRank(recvBar, dest); + storeAsync2(mapToRank(addrA, dest), o[0] * invProbScale, o[2] * invProbScale, remoteBar); + storeAsync2(mapToRank(addrB, dest), o[1] * invProbScale, o[3] * invProbScale, remoteBar); + } + } + markTimeline(7); + markTimeline(8); + if (tid == 0) + { + mbarrierArriveExpectTx(recvBar, kRecvBytes); + } + markTimeline(9); + mbarrierWaitCluster(recvBar, 0); + markTimeline(10); + + // Finalize 32 of the 512 output dims of all eight heads from the pushed partials: warp h + // handles head h, lane i < 16 the statistics of source split i, every lane one output dim. + { + int const head = warp; + float const peerMax = lane < kNumSplits ? recvStats[head * kNumSplits + lane] : -INFINITY; + float const peerSum = lane < kNumSplits ? recvStats[(kNumHeads + head) * kNumSplits + lane] : 0.F; + float globalMax = peerMax; +#pragma unroll + for (int offset = kNumSplits / 2; offset > 0; offset >>= 1) + { + globalMax = fmaxf(globalMax, __shfl_xor_sync(0xffffffffU, globalMax, offset)); + } + float const weight = peerMax == -INFINITY ? 0.F : fastExp2(peerMax - globalMax); + float sum = weight * peerSum; +#pragma unroll + for (int offset = kNumSplits / 2; offset > 0; offset >>= 1) + { + sum += __shfl_xor_sync(0xffffffffU, sum, offset); + } + sum = __shfl_sync(0xffffffffU, sum, 0); + float acc = 0.F; +#pragma unroll + for (int i = 0; i < kNumSplits; ++i) + { + acc += __shfl_sync(0xffffffffU, weight, i) * recv[(i * kNumHeads + head) * kDimsPerCtaOut + lane]; + } + float const result = sum > 0.F ? acc * (outScale / sum) : 0.F; + params.out[static_cast(row) * kNumHeads * kHeadDimV + head * kHeadDimV + split * kDimsPerCtaOut + lane] + = __float2bfloat16_rn(result); + } + markTimeline(11); + markGlobalTime(12); +#else + __trap(); +#endif +} + +void prepareKernelAttributes() +{ + TLLM_CUDA_CHECK( + cudaFuncSetAttribute(sparseMlaDecodeKernel, cudaFuncAttributeMaxDynamicSharedMemorySize, kSmemBytes)); + TLLM_CUDA_CHECK(cudaFuncSetAttribute(sparseMlaDecodeKernel, cudaFuncAttributeNonPortableClusterSizeAllowed, 1)); +} + +cudaLaunchAttribute clusterAttribute() +{ + cudaLaunchAttribute attr; + attr.id = cudaLaunchAttributeClusterDimension; + attr.val.clusterDim.x = kNumSplits; + attr.val.clusterDim.y = 1; + attr.val.clusterDim.z = 1; + return attr; +} + +} // namespace + +#ifdef GLM_MOE_DSA_SPARSE_MLA_DECODE_TIMING +void copySparseMlaDecodeTimeline(unsigned long long* host) +{ + TLLM_CUDA_CHECK(cudaMemcpyFromSymbol(host, gTimeline, sizeof(gTimeline))); +} +#endif + +size_t getSparseMlaDecodeSmemBytes() +{ + return static_cast(kSmemBytes); +} + +bool isSparseMlaDecodeDeviceSupported(int device) +{ + int major = 0; + int maxSmem = 0; + TLLM_CUDA_CHECK(cudaDeviceGetAttribute(&major, cudaDevAttrComputeCapabilityMajor, device)); + TLLM_CUDA_CHECK(cudaDeviceGetAttribute(&maxSmem, cudaDevAttrMaxSharedMemoryPerBlockOptin, device)); + int constexpr kBlackwellDatacenterMajor = 10; + if (major != kBlackwellDatacenterMajor || maxSmem < kSmemBytes) + { + return false; + } + int currentDevice = 0; + TLLM_CUDA_CHECK(cudaGetDevice(¤tDevice)); + TLLM_CUDA_CHECK(cudaSetDevice(device)); + prepareKernelAttributes(); + cudaLaunchConfig_t config{}; + config.gridDim = dim3(kNumSplits); + config.blockDim = dim3(kThreads); + config.dynamicSmemBytes = kSmemBytes; + cudaLaunchAttribute attr = clusterAttribute(); + config.attrs = &attr; + config.numAttrs = 1; + int numClusters = 0; + cudaError_t const status = cudaOccupancyMaxActiveClusters(&numClusters, sparseMlaDecodeKernel, &config); + TLLM_CUDA_CHECK(cudaSetDevice(currentDevice)); + if (status != cudaSuccess) + { + // Clear the error recorded by the failed query. + cudaGetLastError(); + return false; + } + return numClusters > 0; +} + +void invokeSparseMlaDecode(SparseMlaDecodeParams const& params, cudaStream_t stream) +{ + if (params.numRows <= 0) + { + return; + } + prepareKernelAttributes(); + cudaLaunchConfig_t config{}; + config.gridDim = dim3(params.numRows * kNumSplits); + config.blockDim = dim3(kThreads); + config.dynamicSmemBytes = kSmemBytes; + config.stream = stream; + cudaLaunchAttribute attrs[2]; + attrs[0] = clusterAttribute(); + attrs[1].id = cudaLaunchAttributeProgrammaticStreamSerialization; + attrs[1].val.programmaticStreamSerializationAllowed = tensorrt_llm::common::getEnvEnablePDL() ? 1 : 0; + config.attrs = attrs; + config.numAttrs = 2; + TLLM_CUDA_CHECK(cudaLaunchKernelEx(&config, sparseMlaDecodeKernel, params)); +} + +} // namespace kernels::glm_moe_dsa::sparse_mla_decode + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.h b/cpp/tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.h new file mode 100644 index 000000000000..18dadfa6a556 --- /dev/null +++ b/cpp/tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.h @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tensorrt_llm/common/config.h" + +#include +#include + +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace kernels::glm_moe_dsa::sparse_mla_decode +{ + +// Sparse MLA generation attention over an FP8 (e4m3) latent KV pool. +// +// Each query row attends to the pool rows listed in its `kTopK`-entry index list. A pool row +// holds the 512-dim latent followed by the 64-dim rotary part, 576 e4m3 values in total; the +// value vector is the first 512 values of the same row. Eight query heads share the row. +// +// Rules of the TRT-LLM generation FMHA for this layout, with the one difference marked below: +// * Index-list position j of query row r is used only when j < min(kTopK, kvLen - (seqLenQ - 1 - r % seqLenQ)), +// where kvLen counts every token of the sequence including this step's rows. Repeated pool rows +// count once per occurrence. +// * Difference: a negative entry at a used position is skipped. The native kernel does not check +// it and reads the pool row that -1 addresses. The native producers (indexer top-k, local-to-global +// remap) write -1 only at positions past the causal length, so the outputs agree on their inputs. +// * Scores are FP32 dot products of the e4m3 query and key times bmm1Scale[1] (log2 domain). +// * Probabilities relative to a running maximum are multiplied by 448 and rounded to e4m3 before +// the P*V product; row sums use the unrounded FP32 probabilities. +// * A row without a usable entry produces zeros. +// +// Programmatic dependent launch (PDL), active when TRTLLM_ENABLE_PDL allows it: +// * The kernel is launched with programmatic stream serialization. Before griddepcontrol.wait it +// only initializes its own shared-memory barriers and arrives on its cluster barrier; it accesses +// no global memory there, so it may start while the preceding kernel still writes its inputs. +// * After the wait it reads q, indices, seqLensKv, bmm1Scale and bmm2Scale and issues the +// asynchronous gathers of the pool rows. It then executes griddepcontrol.launch_dependents, before +// those gathers complete and before any compute, so the next kernel of the stream can become +// resident while this one still runs. +// * Its only global write is out, stored at the end of each CTA. +// * Obligation of a PDL dependent: execute griddepcontrol.wait (cudaGridDependencySynchronize) +// before reading out and before writing any buffer read here (q, indices, seqLensKv, the scales, +// the pool rows). Work before that wait may access only memory this kernel neither reads nor +// writes, such as weights streamed into shared memory or L2; such work competes with this +// kernel's gathers for memory bandwidth and SM resources. + +int constexpr kNumHeads = 8; +int constexpr kHeadDimQk = 576; +int constexpr kHeadDimV = 512; +int constexpr kTopK = 2048; + +struct SparseMlaDecodeParams +{ + // [numRows, kNumHeads, kHeadDimQk] e4m3 query, rows contiguous. + void const* q; + // Base of the FP8 latent pool; pool row i starts at kvPool + i * kHeadDimQk bytes. + void const* kvPool; + // [numRows, indicesRowStride] int32 pool rows. + int32_t const* indices; + int64_t indicesRowStride; + // [2] device scales; element 1 is the dequantization scale times the softmax scale times log2(e). + float const* bmm1Scale; + // [1] device scale applied to the normalized P*V output. + float const* bmm2Scale; + // [numRows / seqLenQ] KV lengths including this step's rows. + int32_t const* seqLensKv; + int32_t seqLenQ; + // [numRows, kNumHeads, kHeadDimV] BF16 output. + __nv_bfloat16* out; + int32_t numRows; +}; + +// Dynamic shared memory needed by one CTA. +size_t getSparseMlaDecodeSmemBytes(); + +// Whether the current device can launch the kernel (architecture and 16-CTA clusters). +bool isSparseMlaDecodeDeviceSupported(int device); + +void invokeSparseMlaDecode(SparseMlaDecodeParams const& params, cudaStream_t stream); + +#ifdef GLM_MOE_DSA_SPARSE_MLA_DECODE_TIMING +// Development builds only: per-CTA phase timeline of the most recent launches. +int constexpr kTimelineMaxCtas = 4096; +int constexpr kTimelineSlots = 13; +void copySparseMlaDecodeTimeline(unsigned long long* host); +#endif + +} // namespace kernels::glm_moe_dsa::sparse_mla_decode + +TRTLLM_NAMESPACE_END diff --git a/cpp/tensorrt_llm/thop/CMakeLists.txt b/cpp/tensorrt_llm/thop/CMakeLists.txt index ab424318c862..228d096f7022 100644 --- a/cpp/tensorrt_llm/thop/CMakeLists.txt +++ b/cpp/tensorrt_llm/thop/CMakeLists.txt @@ -161,6 +161,13 @@ add_th_common_family( mhcOp.cpp compressorOp.cpp) +add_th_common_family(th_common_glm_moe_dsa) + +# GLM-MoE-DSA torch ops live in glmMoeDsa/ (kernels in +# cpp/tensorrt_llm/kernels/glmMoeDsa/); that directory adds them to +# th_common_glm_moe_dsa with target_sources(). +add_subdirectory(glmMoeDsa) + add_library( th_common SHARED $ @@ -169,7 +176,8 @@ add_library( $ $ $ - $) + $ + $) set_property(TARGET th_common PROPERTY POSITION_INDEPENDENT_CODE ON) set_target_properties(th_common PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON) diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/CMakeLists.txt b/cpp/tensorrt_llm/thop/glmMoeDsa/CMakeLists.txt new file mode 100644 index 000000000000..7f8bc2d1593c --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/CMakeLists.txt @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. +# All rights reserved. SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +# torch.ops.trtllm.glm_moe_dsa_* ops, one file per kernel of +# cpp/tensorrt_llm/kernels/glmMoeDsa/. The parent defines th_common_glm_moe_dsa. +file(GLOB_RECURSE SRC_CPP CONFIGURE_DEPENDS *.cpp) +target_sources(th_common_glm_moe_dsa PRIVATE ${SRC_CPP}) diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkClusterMergeOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkClusterMergeOp.cpp new file mode 100644 index 000000000000..d53886b3d03f --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkClusterMergeOp.cpp @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/crossRankTopkClusterMerge.h" + +#include +#include + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ +namespace +{ +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::cross_rank_topk_cluster_merge; + +void crossRankTopkClusterMerge(at::Tensor const& logits, at::Tensor const& kvLens, at::Tensor& out, at::Tensor& state, + int64_t mcWorkspace, int64_t ucWorkspace, int64_t rank, int64_t worldSize, int64_t nextN, + std::optional const& debugTimes) +{ + TORCH_CHECK(logits.is_cuda() && kvLens.is_cuda() && out.is_cuda() && state.is_cuda(), "CUDA tensors required"); + TORCH_CHECK(logits.scalar_type() == at::kFloat && logits.dim() == 2 && logits.stride(1) == 1, + "logits must be a row-major [rows, local_capacity] float32 view"); + TORCH_CHECK(kvLens.scalar_type() == at::kInt && kvLens.numel() >= 1, "kv_lens must be int32 with >= 1 element"); + TORCH_CHECK(out.scalar_type() == at::kInt && out.dim() == 2 && out.is_contiguous() && out.size(1) == tk::kTopK, + "out must be a contiguous int32 [rows, ", tk::kTopK, "] tensor"); + TORCH_CHECK(out.size(0) == logits.size(0), "out and logits must have the same rows"); + TORCH_CHECK(logits.size(0) <= tk::kMaxRows, "at most ", tk::kMaxRows, " rows"); + TORCH_CHECK(state.scalar_type() == at::kInt && state.numel() >= tk::kStateWords && state.is_contiguous(), + "state must be a contiguous int32 tensor of >= ", tk::kStateWords, " words"); + TORCH_CHECK(worldSize >= 1 && worldSize <= tk::kMaxRanks && rank >= 0 && rank < worldSize, "bad rank/world"); + TORCH_CHECK(nextN >= 1 && nextN == logits.size(0), "next_n must equal the number of rows (one request)"); + TORCH_CHECK(logits.size(1) <= tk::cpTopkMaxLocalCapacity(static_cast(worldSize)), "local capacity ", + logits.size(1), " exceeds the kernel limit ", tk::cpTopkMaxLocalCapacity(static_cast(worldSize))); + TORCH_CHECK(mcWorkspace != 0 && ucWorkspace != 0 && (mcWorkspace % 16) == 0 && (ucWorkspace % 16) == 0, + "workspace addresses must be non-null and 16-byte aligned"); + + tk::CpTopkParams params{}; + params.logits = logits.data_ptr(); + params.logitsStride = logits.stride(0); + params.localCapacity = static_cast(logits.size(1)); + params.kvLens = kvLens.data_ptr(); + params.out = out.data_ptr(); + params.numRows = static_cast(logits.size(0)); + params.nextN = static_cast(nextN); + params.rank = static_cast(rank); + params.worldSize = static_cast(worldSize); + params.mcWorkspace = reinterpret_cast(mcWorkspace); + params.ucWorkspace = reinterpret_cast(ucWorkspace); + params.state = state.data_ptr(); + if (debugTimes.has_value()) + { + TORCH_CHECK(debugTimes->is_cuda() && debugTimes->scalar_type() == at::kLong && debugTimes->is_contiguous() + && debugTimes->numel() >= logits.size(0) * tk::kSourcesPerRank * tk::kDebugSlots, + "debug_times must be a contiguous int64 CUDA tensor of rows x ", tk::kSourcesPerRank, " x ", + tk::kDebugSlots); + params.debugTimes = debugTimes->data_ptr(); + } + tk::invokeCpTopk(params, at::cuda::getCurrentCUDAStream(logits.get_device())); +} + +//! [workspace words, top-k, max ranks, max rows, page tokens, sources per rank, first-round slot capacity, max local +//! capacity, state words, Lamport buffers, default timeout (ms), resident clusters of kMaxRows-row launches] +std::vector crossRankTopkClusterMergeWorkspaceInfo(int64_t worldSize) +{ + return {tk::kWorkspaceWords, tk::kTopK, tk::kMaxRanks, tk::kMaxRows, tk::kPageTokens, tk::kSourcesPerRank, + tk::kSourceCapacity, tk::cpTopkMaxLocalCapacity(static_cast(worldSize)), tk::kStateWords, + tk::kNumLamportBuffers, tk::cpTopkDefaultTimeoutMs(), + tk::cpTopkMaxActiveClusters(static_cast(worldSize))}; +} + +} // namespace +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_cross_rank_topk_cluster_merge(Tensor logits, Tensor kv_lens, Tensor(a!) out, Tensor(b!) state, " + "int mc_workspace, " + "int uc_workspace, int rank, int world_size, int next_n, Tensor(c!)? debug_times=None) -> ()"); + m.def("glm_moe_dsa_cross_rank_topk_cluster_merge_workspace_info(int world_size) -> int[]", + &tensorrt_llm::torch_ext::glm_moe_dsa::crossRankTopkClusterMergeWorkspaceInfo); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl( + "glm_moe_dsa_cross_rank_topk_cluster_merge", &tensorrt_llm::torch_ext::glm_moe_dsa::crossRankTopkClusterMerge); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkGlobalEmitOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkGlobalEmitOp.cpp new file mode 100644 index 000000000000..0796f5bd96ee --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkGlobalEmitOp.cpp @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/crossRankTopkGlobalEmit.h" + +#include +#include +#include + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ +namespace +{ +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::cross_rank_topk_global_emit; + +//! The cluster-merge cross-rank top-k (same arguments as glm_moe_dsa_cross_rank_topk_cluster_merge) that also writes +//! the outputs of the top-k index bridge (trtllm::glm_moe_dsa_topk_index_bridge with the same bridge arguments, applied +//! to `out`): returns the pool rows, [G, rows, top-k] with layer_ids (G = its length) or [rows, top-k] without, and +//! writes the copy rows into copy_dst. +at::Tensor crossRankTopkGlobalEmit(at::Tensor const& logits, at::Tensor const& kvLens, at::Tensor& out, + at::Tensor& state, int64_t mcWorkspace, int64_t ucWorkspace, int64_t rank, int64_t worldSize, int64_t nextN, + at::Tensor const& reqIdx, int64_t reqIdxBias, at::Tensor const& blockTable, int64_t blockSize, int64_t strideFactor, + std::optional const& layerIds, int64_t layerId, std::optional const& copyDst, + int64_t copyRowStart, int64_t copyRows, int64_t copyMode, int64_t copyNextN, + std::optional const& numAccepted, std::optional const& debugTimes) +{ + TORCH_CHECK(logits.is_cuda() && kvLens.is_cuda() && out.is_cuda() && state.is_cuda(), "CUDA tensors required"); + TORCH_CHECK(logits.scalar_type() == at::kFloat && logits.dim() == 2 && logits.stride(1) == 1, + "logits must be a row-major [rows, local_capacity] float32 view"); + TORCH_CHECK(kvLens.scalar_type() == at::kInt && kvLens.numel() >= 1, "kv_lens must be int32 with >= 1 element"); + TORCH_CHECK(out.scalar_type() == at::kInt && out.dim() == 2 && out.is_contiguous() && out.size(1) == tk::kTopK, + "out must be a contiguous int32 [rows, ", tk::kTopK, "] tensor"); + TORCH_CHECK(out.size(0) == logits.size(0), "out and logits must have the same rows"); + TORCH_CHECK(logits.size(0) <= tk::kMaxRows, "at most ", tk::kMaxRows, " rows"); + TORCH_CHECK(state.scalar_type() == at::kInt && state.numel() >= tk::kStateWords && state.is_contiguous(), + "state must be a contiguous int32 tensor of >= ", tk::kStateWords, " words"); + TORCH_CHECK(worldSize >= 1 && worldSize <= tk::kMaxRanks && rank >= 0 && rank < worldSize, "bad rank/world"); + TORCH_CHECK(nextN >= 1 && nextN == logits.size(0), "next_n must equal the number of rows (one request)"); + TORCH_CHECK(logits.size(1) <= tk::cpTopkMaxLocalCapacity(static_cast(worldSize)), "local capacity ", + logits.size(1), " exceeds the kernel limit ", tk::cpTopkMaxLocalCapacity(static_cast(worldSize))); + TORCH_CHECK(mcWorkspace != 0 && ucWorkspace != 0 && (mcWorkspace % 16) == 0 && (ucWorkspace % 16) == 0, + "workspace addresses must be non-null and 16-byte aligned"); + + // The bridge inputs, with the rules of trtllm::glm_moe_dsa_topk_index_bridge (topk = out, num_rows = rows). + int64_t const rows = out.size(0); + TORCH_CHECK(reqIdx.is_cuda() && blockTable.is_cuda(), "req_idx and block_table must be CUDA tensors"); + TORCH_CHECK(reqIdx.scalar_type() == at::kInt && reqIdx.dim() == 1 && reqIdx.is_contiguous(), + "req_idx must be packed int32"); + TORCH_CHECK(reqIdx.size(0) >= rows, "req_idx shorter than the rows"); + TORCH_CHECK(blockTable.scalar_type() == at::kInt && blockTable.dim() == 2, "block_table must be int32 2-D"); + TORCH_CHECK(blockSize == tk::kPageTokens, "block_size must be ", tk::kPageTokens, " (the CP page)"); + TORCH_CHECK(copyMode == 0 || copyMode == 1, "copy_mode must be 0 or 1"); + TORCH_CHECK(copyNextN >= 1 && copyRows >= 0 && copyRowStart >= 0, "bad copy rows"); + TORCH_CHECK(copyMode == 1 || copyRows <= rows, "copy_rows exceeds the rows"); + TORCH_CHECK(copyMode == 0 || copyRows * copyNextN <= rows, "copy_rows * copy_next_n exceeds the rows"); + + int32_t groupSize = 1; + int32_t const* layerIdsPtr = nullptr; + if (layerIds.has_value()) + { + TORCH_CHECK(layerIds->is_cuda() && layerIds->scalar_type() == at::kInt && layerIds->dim() == 1 + && layerIds->is_contiguous(), + "layer_ids must be packed int32 on the GPU"); + groupSize = static_cast(layerIds->size(0)); + TORCH_CHECK(groupSize >= 1 && groupSize <= tk::kMaxGroupSize, + "glm_moe_dsa_cross_rank_topk_global_emit: remap group sizes must be 1..", tk::kMaxGroupSize, ", got ", + groupSize); + layerIdsPtr = layerIds->data_ptr(); + } + int64_t const topK = tk::kTopK; + at::Tensor pool = layerIds.has_value() ? at::empty({static_cast(groupSize), rows, topK}, out.options()) + : at::empty({rows, topK}, out.options()); + + tk::CpTopkParams params{}; + params.logits = logits.data_ptr(); + params.logitsStride = logits.stride(0); + params.localCapacity = static_cast(logits.size(1)); + params.kvLens = kvLens.data_ptr(); + params.out = out.data_ptr(); + params.numRows = static_cast(rows); + params.nextN = static_cast(nextN); + params.rank = static_cast(rank); + params.worldSize = static_cast(worldSize); + params.mcWorkspace = reinterpret_cast(mcWorkspace); + params.ucWorkspace = reinterpret_cast(ucWorkspace); + params.state = state.data_ptr(); + + tk::CpTopkBridgeParams& b = params.bridge; + b.out = pool.data_ptr(); + b.outStrideG = layerIds.has_value() ? pool.stride(0) : 0; + b.outStride0 = layerIds.has_value() ? pool.stride(1) : pool.stride(0); + b.groupSize = groupSize; + b.layerIds = layerIdsPtr; + b.layerId = static_cast(layerId); + b.reqIdx = reqIdx.data_ptr(); + b.reqIdxBias = static_cast(reqIdxBias); + b.blockTable = blockTable.data_ptr(); + b.btStride0 = blockTable.stride(0); + b.btStride1 = blockTable.stride(1); + b.maxBlocksPerReq = static_cast(blockTable.size(1)); + b.strideFactor = static_cast(strideFactor); + if (copyDst.has_value() && copyRows > 0) + { + auto const& dst = copyDst.value(); + TORCH_CHECK(dst.is_cuda() && dst.scalar_type() == at::kInt && dst.dim() == 2 && dst.stride(1) == 1 + && dst.size(1) >= tk::kTopK && dst.size(0) >= copyRowStart + copyRows, + "copy_dst must be int32 [>= start + rows, >= top-k] with unit column stride"); + b.copyDst = dst.data_ptr(); + b.copyStride0 = dst.stride(0); + b.copyRows = static_cast(copyRows); + b.copyRowStart = static_cast(copyRowStart); + b.copyMode = static_cast(copyMode); + b.copyNextN = static_cast(copyNextN); + if (copyMode == 1 && numAccepted.has_value()) + { + auto const& acc = numAccepted.value(); + TORCH_CHECK(acc.is_cuda() && (acc.scalar_type() == at::kInt || acc.scalar_type() == at::kLong) + && acc.dim() == 1 && acc.is_contiguous() && acc.size(0) >= copyRows, + "num_accepted must be packed int32/int64 with at least copy_rows entries"); + b.numAccepted = acc.data_ptr(); + b.numAcceptedIs64 = acc.scalar_type() == at::kLong; + } + } + if (debugTimes.has_value()) + { + TORCH_CHECK(debugTimes->is_cuda() && debugTimes->scalar_type() == at::kLong && debugTimes->is_contiguous() + && debugTimes->numel() >= logits.size(0) * tk::kSourcesPerRank * tk::kDebugSlots, + "debug_times must be a contiguous int64 CUDA tensor of rows x ", tk::kSourcesPerRank, " x ", + tk::kDebugSlots); + params.debugTimes = debugTimes->data_ptr(); + } + tk::invokeCpTopk(params, at::cuda::getCurrentCUDAStream(logits.get_device())); + return pool; +} + +//! [workspace words, top-k, max ranks, max rows, page tokens, sources per rank, first-round slot capacity, max local +//! capacity, state words, Lamport buffers, default timeout (ms), resident clusters of kMaxRows-row launches, max remap +//! group size] +std::vector crossRankTopkGlobalEmitWorkspaceInfo(int64_t worldSize) +{ + return {tk::kWorkspaceWords, tk::kTopK, tk::kMaxRanks, tk::kMaxRows, tk::kPageTokens, tk::kSourcesPerRank, + tk::kSourceCapacity, tk::cpTopkMaxLocalCapacity(static_cast(worldSize)), tk::kStateWords, + tk::kNumLamportBuffers, tk::cpTopkDefaultTimeoutMs(), + tk::cpTopkMaxActiveClusters(static_cast(worldSize)), tk::kMaxGroupSize}; +} + +} // namespace +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_cross_rank_topk_global_emit(Tensor logits, Tensor kv_lens, Tensor(a!) out, Tensor(b!) state, int " + "mc_workspace, " + "int uc_workspace, int rank, int world_size, int next_n, Tensor req_idx, int req_idx_bias, " + "Tensor block_table, int block_size, int stride_factor, Tensor? layer_ids, int layer_id, " + "Tensor(c!)? copy_dst, int copy_row_start, int copy_rows, int copy_mode, int copy_next_n, " + "Tensor? num_accepted, Tensor(d!)? debug_times=None) -> Tensor"); + m.def("glm_moe_dsa_cross_rank_topk_global_emit_workspace_info(int world_size) -> int[]", + &tensorrt_llm::torch_ext::glm_moe_dsa::crossRankTopkGlobalEmitWorkspaceInfo); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_cross_rank_topk_global_emit", &tensorrt_llm::torch_ext::glm_moe_dsa::crossRankTopkGlobalEmit); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkOp.cpp new file mode 100644 index 000000000000..710b0e716b88 --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/crossRankTopkOp.cpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/crossRankTopk.h" + +#include +#include + +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ +namespace +{ +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::cross_rank_topk; + +void crossRankTopk(at::Tensor const& logits, at::Tensor const& kvLens, at::Tensor& out, at::Tensor& state, + int64_t mcWorkspace, int64_t ucWorkspace, int64_t rank, int64_t worldSize, int64_t nextN, + std::optional const& debugTimes) +{ + TORCH_CHECK(logits.is_cuda() && kvLens.is_cuda() && out.is_cuda() && state.is_cuda(), "CUDA tensors required"); + TORCH_CHECK(logits.scalar_type() == at::kFloat && logits.dim() == 2 && logits.stride(1) == 1, + "logits must be a row-major [rows, local_capacity] float32 view"); + TORCH_CHECK(kvLens.scalar_type() == at::kInt && kvLens.numel() >= 1, "kv_lens must be int32 with >= 1 element"); + TORCH_CHECK(out.scalar_type() == at::kInt && out.dim() == 2 && out.is_contiguous() && out.size(1) == tk::kTopK, + "out must be a contiguous int32 [rows, ", tk::kTopK, "] tensor"); + TORCH_CHECK(out.size(0) == logits.size(0), "out and logits must have the same rows"); + TORCH_CHECK(logits.size(0) <= tk::kMaxRows, "at most ", tk::kMaxRows, " rows"); + TORCH_CHECK(state.scalar_type() == at::kInt && state.numel() >= tk::kStateWords && state.is_contiguous(), + "state must be a contiguous int32 tensor of >= ", tk::kStateWords, " words"); + TORCH_CHECK(worldSize >= 1 && worldSize <= tk::kMaxRanks && rank >= 0 && rank < worldSize, "bad rank/world"); + TORCH_CHECK(nextN >= 1 && nextN == logits.size(0), "next_n must equal the number of rows (one request)"); + TORCH_CHECK(logits.size(1) <= tk::cpTopkMaxLocalCapacity(static_cast(worldSize)), "local capacity ", + logits.size(1), " exceeds the kernel limit ", tk::cpTopkMaxLocalCapacity(static_cast(worldSize))); + TORCH_CHECK(mcWorkspace != 0 && ucWorkspace != 0 && (mcWorkspace % 16) == 0 && (ucWorkspace % 16) == 0, + "workspace addresses must be non-null and 16-byte aligned"); + + tk::CpTopkParams params{}; + params.logits = logits.data_ptr(); + params.logitsStride = logits.stride(0); + params.localCapacity = static_cast(logits.size(1)); + params.kvLens = kvLens.data_ptr(); + params.out = out.data_ptr(); + params.numRows = static_cast(logits.size(0)); + params.nextN = static_cast(nextN); + params.rank = static_cast(rank); + params.worldSize = static_cast(worldSize); + params.mcWorkspace = reinterpret_cast(mcWorkspace); + params.ucWorkspace = reinterpret_cast(ucWorkspace); + params.state = state.data_ptr(); + if (debugTimes.has_value()) + { + TORCH_CHECK(debugTimes->is_cuda() && debugTimes->scalar_type() == at::kLong && debugTimes->is_contiguous() + && debugTimes->numel() >= logits.size(0) * tk::kSourcesPerRank * tk::kDebugSlots, + "debug_times must be a contiguous int64 CUDA tensor of rows x ", tk::kSourcesPerRank, " x ", + tk::kDebugSlots); + params.debugTimes = debugTimes->data_ptr(); + } + tk::invokeCpTopk(params, at::cuda::getCurrentCUDAStream(logits.get_device())); +} + +std::vector crossRankTopkWorkspaceInfo(int64_t worldSize) +{ + return {tk::kWorkspaceWords, tk::kTopK, tk::kMaxRanks, tk::kMaxRows, tk::kPageTokens, tk::kSourcesPerRank, + tk::kSourceCapacity, tk::cpTopkMaxLocalCapacity(static_cast(worldSize)), tk::kStateWords, + tk::kNumLamportBuffers, tk::cpTopkDefaultTimeoutMs()}; +} + +} // namespace +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_cross_rank_topk(Tensor logits, Tensor kv_lens, Tensor(a!) out, Tensor(b!) state, int " + "mc_workspace, " + "int uc_workspace, int rank, int world_size, int next_n, Tensor(c!)? debug_times=None) -> ()"); + m.def("glm_moe_dsa_cross_rank_topk_workspace_info(int world_size) -> int[]", + &tensorrt_llm::torch_ext::glm_moe_dsa::crossRankTopkWorkspaceInfo); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_cross_rank_topk", &tensorrt_llm::torch_ext::glm_moe_dsa::crossRankTopk); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/denseMlpOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/denseMlpOp.cpp new file mode 100644 index 000000000000..dd335256942d --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/denseMlpOp.cpp @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/cudaUtils.h" +#include "tensorrt_llm/common/envUtils.h" +#include "tensorrt_llm/kernels/glmMoeDsa/denseMlp.h" + +#include +#include +#include + +#include + +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::dense_mlp; + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ + +constexpr int64_t kVecAlignBytes = 16; + +void checkBf16Matrix(at::Tensor const& t, char const* name) +{ + TORCH_CHECK(t.is_cuda(), name, " must be a CUDA tensor"); + TORCH_CHECK(t.scalar_type() == at::kBFloat16, name, " must be BF16"); + TORCH_CHECK(t.dim() == 2, name, " must be 2-D"); + TORCH_CHECK(t.is_contiguous(), name, " must be contiguous"); + TORCH_CHECK(reinterpret_cast(t.data_ptr()) % kVecAlignBytes == 0, name, " must be 16-byte aligned"); +} + +void launchGateUp(at::Tensor const& x, at::Tensor const& gateUpWeight, at::Tensor& h, int64_t variant, bool enablePdl) +{ + auto const stream = at::cuda::getCurrentCUDAStream(x.get_device()); + TLLM_CUDA_CHECK(tk::invokeGateUpSwiglu(reinterpret_cast<__nv_bfloat16 const*>(x.data_ptr()), + reinterpret_cast<__nv_bfloat16 const*>(gateUpWeight.data_ptr()), reinterpret_cast<__nv_bfloat16*>(h.data_ptr()), + static_cast(x.size(0)), static_cast(x.size(1)), static_cast(h.size(1)), + static_cast(variant), enablePdl, stream)); +} + +void launchDownProj( + at::Tensor const& h, at::Tensor const& downWeight, at::Tensor& out, int64_t variant, bool enablePdl) +{ + auto const stream = at::cuda::getCurrentCUDAStream(h.get_device()); + TLLM_CUDA_CHECK(tk::invokeDown(reinterpret_cast<__nv_bfloat16 const*>(h.data_ptr()), + reinterpret_cast<__nv_bfloat16 const*>(downWeight.data_ptr()), reinterpret_cast<__nv_bfloat16*>(out.data_ptr()), + static_cast(h.size(0)), static_cast(h.size(1)), static_cast(out.size(1)), + static_cast(variant), enablePdl, stream)); +} + +} // namespace + +//! SwiGLU of the fused gate/up projection: x [M, H], gate_up_weight [2I, H] -> h [M, I]. +at::Tensor denseMlpGateUpSwiglu(at::Tensor const& x, at::Tensor const& gateUpWeight, int64_t variant) +{ + checkBf16Matrix(x, "x"); + checkBf16Matrix(gateUpWeight, "gate_up_weight"); + int64_t const numRows = x.size(0); + int64_t const hidden = x.size(1); + TORCH_CHECK(gateUpWeight.size(1) == hidden, "gate_up_weight must be [2 * inter, hidden]"); + TORCH_CHECK(gateUpWeight.size(0) % 2 == 0, "gate_up_weight rows must be even"); + int64_t const inter = gateUpWeight.size(0) / 2; + TORCH_CHECK(tk::isGateUpSupported(static_cast(numRows), static_cast(hidden), static_cast(inter), + static_cast(variant)), + "glm_moe_dsa_dense_mlp_gate_up_swiglu: unsupported shape or variant"); + at::Tensor h = at::empty({numRows, inter}, x.options()); + launchGateUp(x, gateUpWeight, h, variant, tensorrt_llm::common::getEnvEnablePDL()); + return h; +} + +//! Down projection: h [M, I], down_weight [H, I] -> out [M, H] (rank-local partial, no reduction). +at::Tensor denseMlpDown(at::Tensor const& h, at::Tensor const& downWeight, int64_t variant) +{ + checkBf16Matrix(h, "h"); + checkBf16Matrix(downWeight, "down_weight"); + int64_t const numRows = h.size(0); + int64_t const inter = h.size(1); + TORCH_CHECK(downWeight.size(1) == inter, "down_weight must be [hidden, inter]"); + int64_t const hidden = downWeight.size(0); + TORCH_CHECK(tk::isDownSupported(static_cast(numRows), static_cast(inter), static_cast(hidden), + static_cast(variant)), + "glm_moe_dsa_dense_mlp_down: unsupported shape or variant"); + at::Tensor out = at::empty({numRows, hidden}, h.options()); + launchDownProj(h, downWeight, out, variant, tensorrt_llm::common::getEnvEnablePDL()); + return out; +} + +//! Dense gated MLP without the output all-reduce: x [M, H] -> rank-local partial [M, H]. +at::Tensor denseMlp(at::Tensor const& x, at::Tensor const& gateUpWeight, at::Tensor const& downWeight, + int64_t gateUpVariant, int64_t downVariant) +{ + at::Tensor const h = denseMlpGateUpSwiglu(x, gateUpWeight, gateUpVariant); + return denseMlpDown(h, downWeight, downVariant); +} + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def("glm_moe_dsa_dense_mlp_gate_up_swiglu(Tensor x, Tensor gate_up_weight, int variant=1) -> Tensor"); + m.def("glm_moe_dsa_dense_mlp_down(Tensor h, Tensor down_weight, int variant=0) -> Tensor"); + m.def( + "glm_moe_dsa_dense_mlp(Tensor x, Tensor gate_up_weight, Tensor down_weight, int gate_up_variant=1, " + "int down_variant=0) -> Tensor"); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_dense_mlp_gate_up_swiglu", &tensorrt_llm::torch_ext::glm_moe_dsa::denseMlpGateUpSwiglu); + m.impl("glm_moe_dsa_dense_mlp_down", &tensorrt_llm::torch_ext::glm_moe_dsa::denseMlpDown); + m.impl("glm_moe_dsa_dense_mlp", &tensorrt_llm::torch_ext::glm_moe_dsa::denseMlp); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/draftLiveRowHopOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/draftLiveRowHopOp.cpp new file mode 100644 index 000000000000..b439f5d003fd --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/draftLiveRowHopOp.cpp @@ -0,0 +1,150 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/draftLiveRowHop.h" + +#include +#include +#include +#include + +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ + +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::draft_live_rows; + +// Same switch as the native kernels: PDL is on unless TRTLLM_ENABLE_PDL=0. +bool pdlEnabled() +{ + static bool const enabled = []() + { + char const* env = std::getenv("TRTLLM_ENABLE_PDL"); + return !(env != nullptr && std::strcmp(env, "0") == 0); + }(); + return enabled; +} + +void checkRows(at::Tensor const& t, char const* name) +{ + TORCH_CHECK(t.is_cuda() && t.scalar_type() == at::kBFloat16, "[glm_moe_dsa_draft_live_row_hop] ", name, + " must be a CUDA bf16 tensor"); + TORCH_CHECK(t.is_contiguous(), "[glm_moe_dsa_draft_live_row_hop] ", name, " must be contiguous"); + TORCH_CHECK(t.dim() >= 1 && t.size(-1) == tk::kHiddenDim, "[glm_moe_dsa_draft_live_row_hop] ", name, + " must have hidden size ", tk::kHiddenDim); +} + +} // namespace + +//! Live-row post-attention hop: for the rows ``row_ids`` of ``input`` / ``residual`` only, the one-shot cross-rank +//! sum + residual + RMSNorm + router logits of the post-attention hop, on that hop's workspace. +//! Returns ``[normed, residual_out, router_logits]`` with ``row_ids.numel()`` rows (compact, in ``row_ids`` order). +//! See kernels/glmMoeDsa/draftLiveRowHop.h for the arithmetic and the workspace protocol. +std::vector draftLiveRowHop(at::Tensor const& input, at::Tensor const& residual, at::Tensor const& rowIds, + at::Tensor const& gamma, double eps, at::Tensor const& routerWeight, at::Tensor const& ucBuffer, int64_t mcPtr, + at::Tensor const& flags, int64_t rank, int64_t nRanks) +{ + checkRows(input, "input"); + checkRows(residual, "residual"); + TORCH_CHECK( + residual.sizes() == input.sizes(), "[glm_moe_dsa_draft_live_row_hop] residual must have the shape of input"); + checkRows(gamma, "gamma"); + TORCH_CHECK(gamma.dim() == 1, "[glm_moe_dsa_draft_live_row_hop] gamma must be 1-D"); + int64_t const numInputRows = input.numel() / tk::kHiddenDim; + TORCH_CHECK(numInputRows >= 1 && numInputRows <= tk::kMaxRows, + "[glm_moe_dsa_draft_live_row_hop] number of input rows ", numInputRows, " is outside the supported range [1, ", + tk::kMaxRows, "]"); + TORCH_CHECK(rowIds.is_cuda() && rowIds.scalar_type() == at::kLong && rowIds.dim() == 1 && rowIds.is_contiguous(), + "[glm_moe_dsa_draft_live_row_hop] row_ids must be a contiguous 1-D CUDA int64 tensor"); + int64_t const numRows = rowIds.numel(); + TORCH_CHECK(numRows >= 1 && numRows <= numInputRows, "[glm_moe_dsa_draft_live_row_hop] number of live rows ", + numRows, " is outside [1, ", numInputRows, "]"); + TORCH_CHECK(nRanks == tk::kNumRanks, "[glm_moe_dsa_draft_live_row_hop] only TP", tk::kNumRanks, + " is supported, got ", nRanks); + TORCH_CHECK(rank >= 0 && rank < nRanks, "[glm_moe_dsa_draft_live_row_hop] invalid rank ", rank); + TORCH_CHECK(ucBuffer.is_cuda() && ucBuffer.is_contiguous() + && static_cast(ucBuffer.numel() * ucBuffer.element_size()) >= tk::kWorkspaceBytes, + "[glm_moe_dsa_draft_live_row_hop] uc_buffer must be a contiguous CUDA tensor of at least ", tk::kWorkspaceBytes, + " bytes"); + TORCH_CHECK( + mcPtr != 0, "[glm_moe_dsa_draft_live_row_hop] mc_ptr must be the multicast address of uc_buffer's workspace"); + TORCH_CHECK(flags.is_cuda() && flags.is_contiguous() && flags.scalar_type() == at::kInt && flags.numel() >= 4, + "[glm_moe_dsa_draft_live_row_hop] flags must be a contiguous CUDA int32 tensor with at least 4 elements"); + TORCH_CHECK(routerWeight.is_cuda() && routerWeight.scalar_type() == at::kBFloat16 && routerWeight.is_contiguous() + && routerWeight.dim() == 2 && routerWeight.size(0) == tk::kNumExperts + && routerWeight.size(1) == tk::kHiddenDim, + "[glm_moe_dsa_draft_live_row_hop] router_weight must be a contiguous CUDA bf16 tensor of shape [", + tk::kNumExperts, ", ", tk::kHiddenDim, "]"); + TORCH_CHECK(input.get_device() == ucBuffer.get_device() && input.get_device() == flags.get_device() + && input.get_device() == rowIds.get_device(), + "[glm_moe_dsa_draft_live_row_hop] all tensors must be on the same device"); + + c10::cuda::CUDAGuard const deviceGuard(input.device()); + auto const rowsOpts = input.options(); + at::Tensor normOut = at::empty({numRows, tk::kHiddenDim}, rowsOpts); + at::Tensor residualOut = at::empty({numRows, tk::kHiddenDim}, rowsOpts); + at::Tensor routerLogits = at::empty({numRows, tk::kNumExperts}, rowsOpts.dtype(at::kFloat)); + + tk::LiveRowHopParams params; + params.input = input.const_data_ptr(); + params.residualIn = residual.const_data_ptr(); + params.rowIds = rowIds.const_data_ptr(); + params.gamma = gamma.const_data_ptr(); + params.routerWeight = routerWeight.const_data_ptr(); + params.normOut = normOut.mutable_data_ptr(); + params.residualOut = residualOut.mutable_data_ptr(); + params.routerLogits = routerLogits.mutable_data_ptr(); + params.ucBuffer = ucBuffer.mutable_data_ptr(); + params.mcBuffer = reinterpret_cast(mcPtr); + params.flags = reinterpret_cast(flags.mutable_data_ptr()); + params.numRows = static_cast(numRows); + params.numInputRows = static_cast(numInputRows); + params.rank = static_cast(rank); + params.eps = static_cast(eps); + params.enablePdl = pdlEnabled(); + params.stream = at::cuda::getCurrentCUDAStream(input.get_device()); + + cudaError_t const status = tk::invokeDraftLiveRowHop(params); + TORCH_CHECK( + status == cudaSuccess, "[glm_moe_dsa_draft_live_row_hop] kernel launch failed: ", cudaGetErrorString(status)); + return {normOut, residualOut, routerLogits}; +} + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_draft_live_row_hop(Tensor input, Tensor residual, Tensor row_ids, Tensor gamma, float eps, " + "Tensor router_weight, Tensor uc_buffer, int mc_ptr, Tensor flags, int rank, int nranks) -> Tensor[]"); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_draft_live_row_hop", &tensorrt_llm::torch_ext::glm_moe_dsa::draftLiveRowHop); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/draftRoutedMoeOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/draftRoutedMoeOp.cpp new file mode 100644 index 000000000000..15d02bced53a --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/draftRoutedMoeOp.cpp @@ -0,0 +1,246 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/draftRoutedMoe.h" + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ + +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::draft_routed_moe; + +#ifndef GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE +#define GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE 0 +#endif +//! Probe builds compile the kernels with their event record (see draftRoutedMoe.cu). +constexpr bool kTimelineBuild = GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE != 0; + +void checkTensor(at::Tensor const& t, char const* name, at::ScalarType dtype, std::initializer_list shape, + c10::DeviceIndex device) +{ + TORCH_CHECK(t.is_cuda() && t.get_device() == device, "[glm_moe_dsa_draft_routed_moe] ", name, + " must be on the activation's device"); + TORCH_CHECK(t.scalar_type() == dtype, "[glm_moe_dsa_draft_routed_moe] ", name, " has dtype ", t.scalar_type()); + TORCH_CHECK(t.is_contiguous(), "[glm_moe_dsa_draft_routed_moe] ", name, " must be contiguous"); + TORCH_CHECK(t.sizes().vec() == std::vector(shape), "[glm_moe_dsa_draft_routed_moe] ", name, " has shape ", + t.sizes()); +} + +//! ``cuTensorMapEncodeTiled`` through the runtime's driver entry point (no link-time driver dependency). +PFN_cuTensorMapEncodeTiled_v12000 encodeTiledFn() +{ + static PFN_cuTensorMapEncodeTiled_v12000 fn = [] + { + void* ptr = nullptr; + cudaDriverEntryPointQueryResult query{}; + cudaError_t const status + = cudaGetDriverEntryPointByVersion("cuTensorMapEncodeTiled", &ptr, 12000, cudaEnableDefault, &query); + TORCH_CHECK(status == cudaSuccess && query == cudaDriverEntryPointSuccess && ptr != nullptr, + "[glm_moe_dsa_draft_routed_moe] cuTensorMapEncodeTiled is not available"); + return reinterpret_cast(ptr); + }(); + return fn; +} + +//! Tensor map of a BlockMajorK weight buffer viewed as [slabs][slabRows][64] BF16 (a slab: one K chunk of one +//! expert) with boxes of 64 x ``boxRows`` x ``boxSlabs`` and the 128-byte swizzle. Cached per (device, address, +//! shape, box): the expert buffers are fixed model parameters. +CUtensorMap const& weightMap( + int device, void const* base, uint64_t slabRows, uint64_t slabs, uint32_t boxRows, uint32_t boxSlabs) +{ + static std::mutex mutex; + static std::map, CUtensorMap> cache; + std::lock_guard lock(mutex); + auto const key = std::make_tuple(device, base, slabRows, slabs, boxRows, boxSlabs); + auto it = cache.find(key); + if (it != cache.end()) + { + return it->second; + } + CUtensorMap map{}; + cuuint64_t const rowBytes = static_cast(tk::kKChunk) * 2; + cuuint64_t const dims[3] = {static_cast(tk::kKChunk), slabRows, slabs}; + cuuint64_t const strides[2] = {rowBytes, rowBytes * slabRows}; + cuuint32_t const box[3] = {static_cast(tk::kKChunk), boxRows, boxSlabs}; + cuuint32_t const elementStrides[3] = {1, 1, 1}; + CUresult const result = encodeTiledFn()(&map, CU_TENSOR_MAP_DATA_TYPE_BFLOAT16, 3, const_cast(base), dims, + strides, box, elementStrides, CU_TENSOR_MAP_INTERLEAVE_NONE, CU_TENSOR_MAP_SWIZZLE_128B, + CU_TENSOR_MAP_L2_PROMOTION_L2_256B, CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE); + TORCH_CHECK(result == CUDA_SUCCESS, + "[glm_moe_dsa_draft_routed_moe] cuTensorMapEncodeTiled failed: ", static_cast(result)); + return cache.emplace(key, map).first->second; +} + +} // namespace + +//! Routed experts of the MTP draft layer (see kernels/glmMoeDsa/draftRoutedMoe.h): native DeepSeek-V3 +//! routing of ``router_logits`` with ``routing_bias``, FC1 + SwiGLU + FC2 + finalize on the FlashInfer TRT-LLM-Gen +//! BF16 layouts of ``w3_w1`` / ``w2``. Returns the finalized routed output ``[R, 6144]`` (BF16). ``h_scratch`` and +//! ``route_scratch`` are written (declared mutated in the schema) and reused by every call; calls sharing them must +//! be ordered on one stream. ``debug_fc2`` (optional, ``[R, 8, 6144]`` BF16) +//! receives the FC2 row of every (token, slot). ``timeline`` (optional, int64 ``[2, kTimelineCtas, kTimelineSlots]``) +//! receives the per-CTA event record of the TMA-fed kernels; only probe builds +//! (``GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE=1``) accept it. +at::Tensor draftRoutedMoe(at::Tensor const& x, at::Tensor const& routerLogits, at::Tensor const& routingBias, + at::Tensor const& w3w1, at::Tensor const& w2, at::Tensor const& hScratch, at::Tensor const& routeScratch, + double routedScaling, int64_t fc1Ctas, int64_t fc1Warps, int64_t fc2Ctas, int64_t fc2Warps, int64_t flags, + std::optional const& debugFc2, std::optional const& timeline) +{ + TORCH_CHECK(x.is_cuda() && x.scalar_type() == at::kBFloat16 && x.is_contiguous() && x.dim() == 2 + && x.size(1) == tk::kHidden, + "[glm_moe_dsa_draft_routed_moe] x must be a contiguous CUDA bf16 tensor [R, ", tk::kHidden, "]"); + int64_t const rows = x.size(0); + TORCH_CHECK(rows >= 1 && rows <= tk::kMaxRows, "[glm_moe_dsa_draft_routed_moe] rows ", rows, " outside [1, ", + tk::kMaxRows, "]"); + TORCH_CHECK(reinterpret_cast(x.const_data_ptr()) % 16 == 0, + "[glm_moe_dsa_draft_routed_moe] x must be 16-byte aligned"); + auto const device = x.get_device(); + checkTensor(routerLogits, "router_logits", at::kFloat, {rows, tk::kNumExperts}, device); + checkTensor(routingBias, "routing_bias", at::kBFloat16, {tk::kNumExperts}, device); + TORCH_CHECK(reinterpret_cast(routerLogits.const_data_ptr()) % 16 == 0 + && reinterpret_cast(routingBias.const_data_ptr()) % 16 == 0, + "[glm_moe_dsa_draft_routed_moe] router_logits and routing_bias must be 16-byte aligned"); + checkTensor(w3w1, "w3_w1", at::kBFloat16, {tk::kNumExperts, tk::kFc1Chunks, tk::kFc1Rows, tk::kKChunk}, device); + checkTensor(w2, "w2", at::kBFloat16, {tk::kNumExperts, tk::kFc2Chunks, tk::kHidden, tk::kKChunk}, device); + TORCH_CHECK(hScratch.is_cuda() && hScratch.get_device() == device && hScratch.scalar_type() == at::kBFloat16 + && hScratch.is_contiguous() && hScratch.numel() >= tk::kScratchHElems, + "[glm_moe_dsa_draft_routed_moe] h_scratch must be a contiguous CUDA bf16 tensor with at least ", + tk::kScratchHElems, " elements"); + TORCH_CHECK(routeScratch.is_cuda() && routeScratch.get_device() == device && routeScratch.scalar_type() == at::kInt + && routeScratch.is_contiguous() && routeScratch.numel() >= tk::kRouteWords, + "[glm_moe_dsa_draft_routed_moe] route_scratch must be a contiguous CUDA int32 tensor with at least ", + tk::kRouteWords, " elements"); + TORCH_CHECK( + fc1Warps == 1 || fc1Warps == 2 || fc1Warps == 4, "[glm_moe_dsa_draft_routed_moe] fc1_warps must be 1, 2 or 4"); + TORCH_CHECK(fc2Warps == 1 || fc2Warps == 2 || fc2Warps == 4 || (fc2Warps == 3 && (flags & tk::kFlagTma)), + "[glm_moe_dsa_draft_routed_moe] fc2_warps must be 1, 2 or 4 (or 3 with the TMA flag)"); + TORCH_CHECK(fc1Ctas >= 1 && fc1Ctas <= 1024 && fc2Ctas >= 1 && fc2Ctas <= 1024, + "[glm_moe_dsa_draft_routed_moe] grid sizes"); + if (debugFc2.has_value()) + { + checkTensor(debugFc2.value(), "debug_fc2", at::kBFloat16, {rows, tk::kTopK, tk::kHidden}, device); + } + if (timeline.has_value()) + { + TORCH_CHECK(kTimelineBuild, + "[glm_moe_dsa_draft_routed_moe] timeline needs a build with GLM_MOE_DSA_DRAFT_ROUTED_MOE_TIMELINE=1"); + checkTensor(timeline.value(), "timeline", at::kLong, {2, tk::kTimelineCtas, tk::kTimelineSlots}, device); + } + + c10::cuda::CUDAGuard const deviceGuard(x.device()); + at::Tensor out = at::empty({rows, tk::kHidden}, x.options()); + + tk::DraftRoutedMoeParams params{}; + params.logits = routerLogits.const_data_ptr(); + params.bias = reinterpret_cast<__nv_bfloat16 const*>(routingBias.const_data_ptr()); + params.x = reinterpret_cast<__nv_bfloat16 const*>(x.const_data_ptr()); + params.w13 = reinterpret_cast<__nv_bfloat16 const*>(w3w1.const_data_ptr()); + params.w2 = reinterpret_cast<__nv_bfloat16 const*>(w2.const_data_ptr()); + params.out = reinterpret_cast<__nv_bfloat16*>(out.mutable_data_ptr()); + params.h = reinterpret_cast<__nv_bfloat16*>(hScratch.mutable_data_ptr()); + params.route = routeScratch.mutable_data_ptr(); + params.debugFc2 = debugFc2.has_value() ? reinterpret_cast<__nv_bfloat16*>(debugFc2->mutable_data_ptr()) : nullptr; + params.timeline = timeline.has_value() ? timeline->mutable_data_ptr() : nullptr; + params.rows = static_cast(rows); + params.routedScaling = static_cast(routedScaling); + params.fc1Ctas = static_cast(fc1Ctas); + params.fc1Warps = static_cast(fc1Warps); + params.fc2Ctas = static_cast(fc2Ctas); + params.fc2Warps = static_cast(fc2Warps); + params.flags = static_cast(flags); + + tk::DraftRoutedMoeTensorMaps maps{}; + bool const tma = (flags & tk::kFlagTma) != 0; + if (tma) + { + int const f = static_cast(flags); + int const w1 = static_cast(fc1Warps); + maps.w13 = weightMap(device, w3w1.const_data_ptr(), tk::kFc1Rows, + static_cast(tk::kNumExperts) * tk::kFc1Chunks, static_cast(tk::fc1TmaBoxRows(f, w1)), + static_cast(tk::fc1TmaBoxSlabs(f, w1))); + maps.w2 = weightMap(device, w2.const_data_ptr(), tk::kHidden, + static_cast(tk::kNumExperts) * tk::kFc2Chunks, static_cast(16 * fc2Warps), + static_cast(tk::kFc2Chunks)); + // The triples path of the FC1 kernel (4 MMA warps) reads 64-row boxes; otherwise the map is not read. + maps.w13Triple = ((f & tk::kFlagFc1Triples) && w1 == 4) ? weightMap(device, w3w1.const_data_ptr(), tk::kFc1Rows, + static_cast(tk::kNumExperts) * tk::kFc1Chunks, + static_cast(tk::kTripleRows), static_cast(tk::kTripleStageChunks)) + : maps.w13; + // kFlagStreamX: the call's activation as [token][K chunk][64] (boxes of 8 tokens; rows beyond the call's are + // zero-filled and only feed discarded MMA columns). Encoded per call (``x`` changes between calls). + maps.x = maps.w13; + if (f & tk::kFlagStreamX) + { + cuuint64_t const dims[3] = {static_cast(tk::kKChunk), static_cast(tk::kFc1Chunks), + static_cast(rows)}; + cuuint64_t const strides[2] + = {static_cast(tk::kKChunk) * 2, static_cast(tk::kHidden) * 2}; + cuuint32_t const box[3] = {static_cast(tk::kKChunk), static_cast(tk::kXBoxChunks), + static_cast(tk::kMaxRows)}; + cuuint32_t const elementStrides[3] = {1, 1, 1}; + CUresult const result + = encodeTiledFn()(&maps.x, CU_TENSOR_MAP_DATA_TYPE_BFLOAT16, 3, const_cast(x.const_data_ptr()), + dims, strides, box, elementStrides, CU_TENSOR_MAP_INTERLEAVE_NONE, CU_TENSOR_MAP_SWIZZLE_128B, + CU_TENSOR_MAP_L2_PROMOTION_L2_128B, CU_TENSOR_MAP_FLOAT_OOB_FILL_NONE); + TORCH_CHECK(result == CUDA_SUCCESS, + "[glm_moe_dsa_draft_routed_moe] cuTensorMapEncodeTiled (x) failed: ", static_cast(result)); + } + } + cudaError_t const status + = tk::invokeDraftRoutedMoe(params, tma ? &maps : nullptr, at::cuda::getCurrentCUDAStream(device)); + TORCH_CHECK( + status == cudaSuccess, "[glm_moe_dsa_draft_routed_moe] kernel launch failed: ", cudaGetErrorString(status)); + return out; +} + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_draft_routed_moe(Tensor x, Tensor router_logits, Tensor routing_bias, Tensor w3_w1, Tensor w2, " + "Tensor(a!) h_scratch, Tensor(b!) route_scratch, float routed_scaling, int fc1_ctas, int fc1_warps, " + "int fc2_ctas, int fc2_warps, int flags, Tensor(c!)? debug_fc2=None, Tensor(d!)? timeline=None) -> Tensor"); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_draft_routed_moe", &tensorrt_llm::torch_ext::glm_moe_dsa::draftRoutedMoe); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/draftTailOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/draftTailOp.cpp new file mode 100644 index 000000000000..62c53823f6ba --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/draftTailOp.cpp @@ -0,0 +1,168 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Torch ops of the draft tail (greedy draft-token selection over a +// vocab-sharded LM head). See kernels/glmMoeDsa/draftTail.h. + +#include "tensorrt_llm/kernels/glmMoeDsa/draftTail.h" + +#include +#include + +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::draft_tail; + +void fillCommon(tk::DraftTailParams& p, int64_t rows, int64_t vocabLocal, at::Tensor& tokens, at::Tensor& scratch, + std::optional const& keysOut, int64_t vocabOffset, int64_t tpRank, int64_t tpSize, int64_t mode, + int64_t mcPtr, int64_t ucPtr, std::optional const& peerPtrs, int64_t timeoutCycles, c10::Device device) +{ + TORCH_CHECK(rows >= 1 && rows <= tk::kMaxRows, "glm_moe_dsa_draft_tail: rows must be in [1, ", tk::kMaxRows, "]"); + TORCH_CHECK(tokens.device() == device && tokens.scalar_type() == at::kInt && tokens.is_contiguous() + && tokens.numel() >= rows, + "glm_moe_dsa_draft_tail: tokens must be a contiguous int32 tensor with at least rows elements"); + TORCH_CHECK(scratch.device() == device && scratch.scalar_type() == at::kLong && scratch.is_contiguous() + && scratch.numel() >= tk::kScratchWords, + "glm_moe_dsa_draft_tail: scratch must be a contiguous int64 tensor with ", tk::kScratchWords, " elements"); + TORCH_CHECK(vocabOffset >= 0 && vocabOffset + vocabLocal <= 0xFFFFFFFFll, + "glm_moe_dsa_draft_tail: vocab range out of bounds"); + TORCH_CHECK(tpSize >= 1 && tpSize <= tk::kMaxRanks && tpRank >= 0 && tpRank < tpSize, + "glm_moe_dsa_draft_tail: bad tp rank"); + TORCH_CHECK(mode >= 0 && mode <= 4, "glm_moe_dsa_draft_tail: bad exchange mode"); + p.rows = static_cast(rows); + p.vocabLocal = static_cast(vocabLocal); + p.vocabOffset = vocabOffset; + p.tokens = tokens.data_ptr(); + p.scratch = reinterpret_cast(scratch.data_ptr()); + if (keysOut.has_value()) + { + TORCH_CHECK(keysOut->device() == device && keysOut->scalar_type() == at::kLong && keysOut->is_contiguous() + && keysOut->numel() >= rows, + "glm_moe_dsa_draft_tail: keys_out must be a contiguous int64 tensor with at least rows elements"); + p.keysOut = reinterpret_cast(keysOut->data_ptr()); + } + p.tpRank = static_cast(tpRank); + p.tpSize = static_cast(tpSize); + p.mode = static_cast(mode); + p.timeoutCycles = timeoutCycles; + if (p.mode != tk::ExchangeMode::kNone && tpSize > 1) + { + TORCH_CHECK(ucPtr != 0 && ucPtr % 16 == 0, "glm_moe_dsa_draft_tail: exchange needs the local unicast buffer"); + p.ucSlotsLocal = reinterpret_cast(ucPtr); + if (p.mode == tk::ExchangeMode::kMulticast || p.mode == tk::ExchangeMode::kMulticastDirect) + { + TORCH_CHECK( + mcPtr != 0 && mcPtr % 16 == 0, "glm_moe_dsa_draft_tail: multicast exchange needs the multicast buffer"); + p.mcSlots = reinterpret_cast(mcPtr); + } + else + { + TORCH_CHECK(peerPtrs.has_value() && peerPtrs->device() == device && peerPtrs->scalar_type() == at::kLong + && peerPtrs->is_contiguous() && peerPtrs->numel() >= tpSize, + "glm_moe_dsa_draft_tail: unicast exchange needs a device int64 tensor of tp_size peer addresses"); + p.ucSlotsPeers = reinterpret_cast(peerPtrs->data_ptr()); + } + } +} + +} // namespace + +void draftTail(at::Tensor const& hidden, at::Tensor const& weight, at::Tensor& tokens, at::Tensor& scratch, + std::optional const& keysOut, int64_t vocabOffset, int64_t tpRank, int64_t tpSize, int64_t mode, + int64_t mcPtr, int64_t ucPtr, std::optional const& peerPtrs, int64_t variant, int64_t numCtas, + int64_t keepPermille, bool pdl, int64_t timeoutCycles, std::optional const& logitsOut) +{ + TORCH_CHECK(hidden.is_cuda() && hidden.dim() == 2 && hidden.scalar_type() == at::kBFloat16, + "glm_moe_dsa_draft_tail: hidden must be a 2D BF16 CUDA tensor"); + TORCH_CHECK(weight.device() == hidden.device() && weight.dim() == 2 && weight.scalar_type() == at::kBFloat16 + && weight.is_contiguous(), + "glm_moe_dsa_draft_tail: weight must be a contiguous 2D BF16 tensor on the hidden device"); + TORCH_CHECK(hidden.size(1) == 6144 && weight.size(1) == 6144, "glm_moe_dsa_draft_tail: hidden size must be 6144"); + TORCH_CHECK(hidden.stride(1) == 1 && hidden.stride(0) % 8 == 0 + && reinterpret_cast(hidden.data_ptr()) % 16 == 0 + && reinterpret_cast(weight.data_ptr()) % 32 == 0, + "glm_moe_dsa_draft_tail: hidden rows must be 16-byte aligned and the weight 32-byte aligned"); + tk::DraftTailParams p; + fillCommon(p, hidden.size(0), weight.size(0), tokens, scratch, keysOut, vocabOffset, tpRank, tpSize, mode, mcPtr, + ucPtr, peerPtrs, timeoutCycles, hidden.device()); + p.hidden = reinterpret_cast<__nv_bfloat16 const*>(hidden.data_ptr()); + p.hiddenStride = hidden.stride(0); + p.weight = reinterpret_cast<__nv_bfloat16 const*>(weight.data_ptr()); + p.hiddenDim = static_cast(hidden.size(1)); + TORCH_CHECK( + keepPermille >= 0 && keepPermille <= 1000, "glm_moe_dsa_draft_tail: keep_permille must be in [0, 1000]"); + p.keepPermille = static_cast(keepPermille); + if (logitsOut.has_value()) + { + TORCH_CHECK(logitsOut->device() == hidden.device() && logitsOut->scalar_type() == at::kBFloat16 + && logitsOut->dim() == 2 && logitsOut->size(0) >= hidden.size(0) + && logitsOut->size(1) == weight.size(0) && logitsOut->stride(1) == 1, + "glm_moe_dsa_draft_tail: logits_out must be a BF16 [rows, vocab_local] tensor with unit last stride"); + p.logitsOut = reinterpret_cast<__nv_bfloat16*>(logitsOut->data_ptr()); + p.logitsOutStride = logitsOut->stride(0); + } + tk::FusedLaunchConfig config; + config.numCtas = static_cast(numCtas); + config.variant = static_cast(variant); + config.pdl = pdl; + tk::invokeDraftTailFused(p, config, at::cuda::getCurrentCUDAStream(hidden.get_device())); +} + +void draftTailFromLogits(at::Tensor const& logits, at::Tensor& tokens, at::Tensor& scratch, + std::optional const& keysOut, int64_t vocabOffset, int64_t tpRank, int64_t tpSize, int64_t mode, + int64_t mcPtr, int64_t ucPtr, std::optional const& peerPtrs, bool pdl, int64_t timeoutCycles) +{ + TORCH_CHECK(logits.is_cuda() && logits.dim() == 2 && logits.scalar_type() == at::kBFloat16 && logits.stride(1) == 1, + "glm_moe_dsa_draft_tail: logits must be a 2D BF16 CUDA tensor with unit last stride"); + tk::DraftTailParams p; + fillCommon(p, logits.size(0), logits.size(1), tokens, scratch, keysOut, vocabOffset, tpRank, tpSize, mode, mcPtr, + ucPtr, peerPtrs, timeoutCycles, logits.device()); + p.logits = reinterpret_cast<__nv_bfloat16 const*>(logits.data_ptr()); + p.logitsStride = logits.stride(0); + tk::invokeDraftTailFromLogits(p, pdl, at::cuda::getCurrentCUDAStream(logits.get_device())); +} + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_draft_tail(Tensor hidden, Tensor weight, Tensor(a!) tokens, Tensor(b!) scratch, " + "Tensor(c!)? keys_out, int vocab_offset, int tp_rank, int tp_size, int mode, int mc_ptr, int uc_ptr, " + "Tensor? peer_ptrs, int variant, int num_ctas, int keep_permille, bool pdl, int timeout_cycles, " + "Tensor(d!)? logits_out=None) -> ()"); + m.def( + "glm_moe_dsa_draft_tail_from_logits(Tensor logits, Tensor(a!) tokens, Tensor(b!) scratch, Tensor(c!)? " + "keys_out, " + "int vocab_offset, int tp_rank, int tp_size, int mode, int mc_ptr, int uc_ptr, Tensor? peer_ptrs, bool pdl, " + "int timeout_cycles) -> ()"); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_draft_tail", &tensorrt_llm::torch_ext::glm_moe_dsa::draftTail); + m.impl("glm_moe_dsa_draft_tail_from_logits", &tensorrt_llm::torch_ext::glm_moe_dsa::draftTailFromLogits); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/indexerLayerRopeKvAppendOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/indexerLayerRopeKvAppendOp.cpp new file mode 100644 index 000000000000..d51ca99cf61d --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/indexerLayerRopeKvAppendOp.cpp @@ -0,0 +1,242 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/envUtils.h" +#include "tensorrt_llm/common/quantization.h" +#include "tensorrt_llm/kernels/glmMoeDsa/indexerLayerRopeKvAppend.h" +#include "tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.h" +#include "tensorrt_llm/kernels/kvCacheUtils.h" + +#include +#include + +#include +#include +#include +#include + +namespace tk = tensorrt_llm::kernels; +namespace tc = tensorrt_llm::common; +namespace rope_kv_append = tensorrt_llm::kernels::glm_moe_dsa::rope_kv_append; +namespace indexer_layer_front = tensorrt_llm::kernels::glm_moe_dsa::indexer_layer_front; + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ + +void checkCuda(torch::Tensor const& t, char const* name) +{ + TORCH_CHECK(t.is_cuda(), name, " must be a CUDA tensor"); +} + +//! Paged MLA latent cache of one layer: the KVBlockArray that buildPagedKvCacheBuffers (thop/attentionOp.cpp) +//! builds for an FP8 MLA cache (kvFactor 1, one KV head, 8-bit elements, beam width 1, no FP4 scale pool). +tk::KVBlockArray buildFp8MlaKvBlockArray(torch::Tensor const& kvCacheBlockOffsets, + torch::Tensor const& hostKvCachePoolPointers, torch::Tensor const& hostKvCachePoolMapping, int64_t layerIdx, + int32_t batchSize, int64_t tokensPerBlock, int64_t attentionWindowSize, int64_t seqOffset) +{ + TORCH_CHECK(hostKvCachePoolMapping.device().is_cpu() && hostKvCachePoolMapping.dim() == 2 + && hostKvCachePoolMapping.size(1) >= 2 && hostKvCachePoolMapping.scalar_type() == torch::kInt32, + "host_kv_cache_pool_mapping must be a CPU int32 [layers, >=2] tensor"); + TORCH_CHECK(layerIdx >= 0 && layerIdx < hostKvCachePoolMapping.size(0), "layer index out of the pool mapping"); + auto const* mapping = hostKvCachePoolMapping.data_ptr(); + int32_t const poolIndex = mapping[layerIdx * hostKvCachePoolMapping.stride(0)]; + int32_t const layerIdxInCachePool + = mapping[layerIdx * hostKvCachePoolMapping.stride(0) + hostKvCachePoolMapping.stride(1)]; + + TORCH_CHECK(hostKvCachePoolPointers.device().is_cpu() && hostKvCachePoolPointers.dim() == 2 + && hostKvCachePoolPointers.scalar_type() == torch::kInt64 && poolIndex >= 0 + && poolIndex < hostKvCachePoolPointers.size(0) && hostKvCachePoolPointers.size(1) >= 2, + "host_kv_cache_pool_pointers must be a CPU int64 [pools, 2] tensor"); + TORCH_CHECK(kvCacheBlockOffsets.is_cuda() && kvCacheBlockOffsets.scalar_type() == torch::kInt32 + && kvCacheBlockOffsets.dim() >= 2 && poolIndex < kvCacheBlockOffsets.size(0) + && seqOffset < kvCacheBlockOffsets.size(1), + "kv_cache_block_offsets must be CUDA int32 [pools, seqs, 2, max_blocks]"); + + int64_t constexpr kCacheElemBits = 8; + int64_t const blockSize = tokensPerBlock * /*kv_head_num=*/1 * rope_kv_append::kLatentDim; + int64_t const bytesPerBlock = blockSize * kCacheElemBits / CHAR_BIT; + int64_t const intraPoolOffset = layerIdxInCachePool * /*kvFactor=*/1 * bytesPerBlock; + auto const sizePerToken = static_cast(rope_kv_append::kLatentDim * kCacheElemBits / 8); + auto const* pointers = hostKvCachePoolPointers.data_ptr(); + auto const rowOffset = poolIndex * hostKvCachePoolPointers.stride(0); + void* primaryPoolPtr = reinterpret_cast(pointers[rowOffset]) + intraPoolOffset; + void* secondaryPoolPtr + = reinterpret_cast(pointers[rowOffset + hostKvCachePoolPointers.stride(1)]) + intraPoolOffset; + auto* blockOffsets + = reinterpret_cast(static_cast(kvCacheBlockOffsets.data_ptr()) + + poolIndex * kvCacheBlockOffsets.stride(0) + seqOffset * kvCacheBlockOffsets.stride(1)); + auto const maxBlocksPerSequence = static_cast(kvCacheBlockOffsets.size(-1)); + auto const window = static_cast(attentionWindowSize); + return tk::KVBlockArray(batchSize, maxBlocksPerSequence, static_cast(tokensPerBlock), sizePerToken, window, + window, /*sinkTokenLen=*/0, /*canUseOneMoreBlock=*/false, primaryPoolPtr, secondaryPoolPtr, blockOffsets); +} + +} // namespace + +//! Variant of trtllm::glm_moe_dsa_rope_kv_append: same arguments, checks and written bytes; with early_trigger each +//! kernel thread triggers the programmatic dependents right after the global reads of its work item (otherwise at the +//! end of the kernel, as in trtllm::glm_moe_dsa_rope_kv_append). +void indexerLayerRopeKvAppend(torch::Tensor const& fused_q, torch::Tensor const& q_pe, + torch::Tensor const& latent_cache, torch::Tensor const& rotary_cos_sin, torch::Tensor const& cu_q_seqlens, + torch::Tensor const& cu_kv_seqlens, torch::Tensor const& fmha_scheduler_counter, + torch::Tensor const& mla_bmm1_scale, torch::Tensor const& mla_bmm2_scale, torch::Tensor const& quant_q_buffer, + torch::Tensor const& sequence_length, torch::Tensor const& host_context_lengths, int64_t num_contexts, + torch::Tensor const& kv_cache_block_offsets, torch::Tensor const& host_kv_cache_pool_pointers, + torch::Tensor const& host_kv_cache_pool_mapping, std::optional const& kv_cache_scale_orig_quant, + int64_t layer_idx, int64_t num_heads, int64_t tokens_per_block, int64_t attention_window_size, int64_t quant_mode, + double q_scaling, int64_t qk_nope_head_dim, int64_t qk_rope_head_dim, int64_t kv_lora_rank, + std::optional const& idx_k_fp8, std::optional const& idx_k_scale, + std::optional const& idx_k_cache, std::optional const& idx_slot_mapping_fp8, + std::optional const& idx_slot_mapping_scale, int64_t idx_num_tokens, bool early_trigger) +{ + TORCH_CHECK(kv_lora_rank == rope_kv_append::kKvLoraRank && qk_rope_head_dim == rope_kv_append::kRopeDim, + "glm_moe_dsa_indexer_layer_rope_kv_append: supports kv_lora_rank 512 and qk_rope_head_dim 64 only"); + auto const kvQuantMode = tc::QuantMode(static_cast(quant_mode)); + TORCH_CHECK(kvQuantMode.hasFp8KvCache() && !kvQuantMode.hasFp4KvCache(), + "glm_moe_dsa_indexer_layer_rope_kv_append: needs an FP8 MLA cache"); + checkCuda(fused_q, "fused_q"); + checkCuda(q_pe, "q_pe"); + checkCuda(latent_cache, "latent_cache"); + TORCH_CHECK(fused_q.scalar_type() == torch::kBFloat16 && q_pe.scalar_type() == torch::kBFloat16 + && latent_cache.scalar_type() == torch::kBFloat16, + "glm_moe_dsa_indexer_layer_rope_kv_append: needs BF16 fused_q, q_pe and latent_cache"); + TORCH_CHECK(rotary_cos_sin.scalar_type() == torch::kFloat32 && rotary_cos_sin.is_contiguous(), + "rotary_cos_sin must be packed float32"); + int64_t const cosSinRows = rotary_cos_sin.numel() / (2 * rope_kv_append::kRopeDim); + TORCH_CHECK(cosSinRows > 0 && cosSinRows <= INT32_MAX, "rotary_cos_sin has no complete position row"); + TORCH_CHECK(q_pe.dim() == 3 && q_pe.stride(2) == 1, "q_pe must be [tokens, heads, rope] with unit inner stride"); + TORCH_CHECK(latent_cache.dim() == 2 && latent_cache.size(1) == rope_kv_append::kLatentDim + && latent_cache.stride(1) == 1 && latent_cache.stride(0) == rope_kv_append::kLatentDim, + "latent_cache must be packed [tokens, 576]"); + TORCH_CHECK(fused_q.is_contiguous() && quant_q_buffer.is_contiguous(), "fused_q and quant_q_buffer must be packed"); + TORCH_CHECK(host_kv_cache_pool_mapping.dim() >= 1, "host_kv_cache_pool_mapping is required"); + + int32_t const numTokens = static_cast(latent_cache.size(0)); + int32_t const numSeqs = static_cast(host_context_lengths.size(0)); + int32_t const numGenerations = numSeqs - static_cast(num_contexts); + TORCH_CHECK(numGenerations > 0, "glm_moe_dsa_indexer_layer_rope_kv_append: needs at least one generation request"); + TORCH_CHECK(numTokens % numGenerations == 0, "MLA can only support input sequences with the same sequence length."); + TORCH_CHECK(fused_q.numel() >= static_cast(numTokens) * num_heads * rope_kv_append::kLatentDim, + "fused_q is smaller than [tokens, heads, 576]"); + TORCH_CHECK(quant_q_buffer.numel() * quant_q_buffer.element_size() + >= static_cast(numTokens) * num_heads * rope_kv_append::kLatentDim, + "quant_q_buffer is smaller than [tokens, heads, 576] bytes"); + TORCH_CHECK(mla_bmm1_scale.numel() >= 2 && mla_bmm2_scale.numel() >= 1, "bmm scale buffers too small"); + TORCH_CHECK(cu_q_seqlens.numel() >= numGenerations + 1 && cu_kv_seqlens.numel() >= numGenerations + 1, + "cu_seqlens buffers too small"); + + auto stream = at::cuda::getCurrentCUDAStream(latent_cache.get_device()); + rope_kv_append::RopeAppendParams params{}; + params.fusedQ = reinterpret_cast<__nv_bfloat16 const*>(fused_q.data_ptr()); + params.qPe = reinterpret_cast<__nv_bfloat16 const*>(q_pe.data_ptr()); + params.qPeLd = static_cast(q_pe.stride(1)); + params.qPeStride = static_cast(q_pe.stride(0)); + params.quantQ = reinterpret_cast(quant_q_buffer.data_ptr()); + params.quantScaleQ = nullptr; + params.latent = reinterpret_cast<__nv_bfloat16 const*>(latent_cache.data_ptr()); + params.cosSin = reinterpret_cast(rotary_cos_sin.data_ptr()); + params.cosSinRows = static_cast(cosSinRows); + params.kvLens = sequence_length.slice(0, num_contexts).data_ptr(); + params.quantScaleKv + = kv_cache_scale_orig_quant.has_value() ? kv_cache_scale_orig_quant->data_ptr() : nullptr; + params.kvCache = buildFp8MlaKvBlockArray(kv_cache_block_offsets, host_kv_cache_pool_pointers, + host_kv_cache_pool_mapping, layer_idx, numGenerations, tokens_per_block, attention_window_size, num_contexts); + params.cuQSeqlens = cu_q_seqlens.data_ptr(); + params.cuKvSeqlens = cu_kv_seqlens.data_ptr(); + params.fmhaTileCounter = reinterpret_cast(fmha_scheduler_counter.data_ptr()); + params.bmm1Scale = mla_bmm1_scale.data_ptr(); + params.bmm2Scale = mla_bmm2_scale.data_ptr(); + params.dequantScaleQ = nullptr; + params.dequantScaleKv = nullptr; + params.quantScaleO = nullptr; + // Same expression as the native op so the folded FMHA scales match bit for bit. + params.hostBmm1Scale = 1.F / (q_scaling * std::sqrt(static_cast(qk_nope_head_dim + qk_rope_head_dim))); + params.numTokens = numTokens; + params.seqLen = numTokens / numGenerations; + params.numHeads = static_cast(num_heads); + + if (idx_num_tokens > 0) + { + TORCH_CHECK(idx_k_fp8.has_value() && idx_k_scale.has_value() && idx_k_cache.has_value() + && idx_slot_mapping_fp8.has_value() && idx_slot_mapping_scale.has_value(), + "indexer scatter needs k_fp8, k_scale, k_cache and both slot mappings"); + auto const& kFp8 = idx_k_fp8.value(); + auto const& kScale = idx_k_scale.value(); + auto const& kCache = idx_k_cache.value(); + auto const& slotFp8 = idx_slot_mapping_fp8.value(); + auto const& slotScale = idx_slot_mapping_scale.value(); + checkCuda(kFp8, "k_fp8"); + checkCuda(kScale, "k_scale"); + checkCuda(kCache, "k_cache"); + TORCH_CHECK(kFp8.dim() == 2 && kFp8.element_size() == 1 && kFp8.is_contiguous(), "k_fp8 must be packed bytes"); + TORCH_CHECK(kScale.dim() == 2 && kScale.element_size() == 4 && kScale.is_contiguous() + && kScale.size(1) * kScale.element_size() == 4, + "k_scale must be one packed 4-byte word per token"); + TORCH_CHECK(kCache.dim() == 4 && kCache.size(2) == 1, "k_cache must be [blocks, block_size, 1, bytes]"); + TORCH_CHECK(slotFp8.scalar_type() == torch::kInt64 && slotScale.scalar_type() == torch::kInt64 + && slotFp8.is_contiguous() && slotScale.is_contiguous(), + "slot mappings must be packed int64"); + int32_t const headBytes = static_cast(kFp8.size(1)); + TORCH_CHECK(headBytes == 128 || headBytes == 64, "indexer k head bytes must be 128 or 64"); + TORCH_CHECK(kFp8.size(0) >= idx_num_tokens && kScale.size(0) >= idx_num_tokens + && slotFp8.numel() >= idx_num_tokens && slotScale.numel() >= idx_num_tokens, + "indexer inputs are shorter than idx_num_tokens"); + params.idxK = reinterpret_cast(kFp8.data_ptr()); + params.idxScale = reinterpret_cast(kScale.data_ptr()); + params.idxCache = reinterpret_cast(kCache.data_ptr()); + params.idxSlotData = slotFp8.data_ptr(); + params.idxSlotScale = slotScale.data_ptr(); + params.idxNumTokens = static_cast(idx_num_tokens); + params.idxHeadBytes = headBytes; + for (int32_t d = 0; d < 4; ++d) + { + params.idxDims[d] = static_cast(kCache.size(d)); + params.idxStrides[d] = kCache.stride(d); + } + } + + indexer_layer_front::invokeIndexerLayerRopeKvAppend(params, tc::getEnvEnablePDL(), early_trigger, stream); +} + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_indexer_layer_rope_kv_append(Tensor fused_q, Tensor q_pe, Tensor latent_cache, Tensor " + "rotary_cos_sin, " + "Tensor(a!) cu_q_seqlens, Tensor(b!) cu_kv_seqlens, Tensor(c!) fmha_scheduler_counter, " + "Tensor(d!) mla_bmm1_scale, Tensor(e!) mla_bmm2_scale, Tensor(f!) quant_q_buffer, Tensor sequence_length, " + "Tensor host_context_lengths, int num_contexts, Tensor kv_cache_block_offsets, " + "Tensor host_kv_cache_pool_pointers, Tensor host_kv_cache_pool_mapping, Tensor? kv_cache_scale_orig_quant, " + "int layer_idx, int num_heads, int tokens_per_block, int attention_window_size, int quant_mode, " + "float q_scaling, int qk_nope_head_dim, int qk_rope_head_dim, int kv_lora_rank, Tensor? idx_k_fp8, " + "Tensor? idx_k_scale, Tensor(g!)? idx_k_cache, Tensor? idx_slot_mapping_fp8, Tensor? idx_slot_mapping_scale, " + "int idx_num_tokens, bool early_trigger) -> ()"); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_indexer_layer_rope_kv_append", &tensorrt_llm::torch_ext::glm_moe_dsa::indexerLayerRopeKvAppend); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopFromPartialsOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopFromPartialsOp.cpp new file mode 100644 index 000000000000..7d94a64871ff --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopFromPartialsOp.cpp @@ -0,0 +1,150 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/postAttentionHopFromPartials.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ + +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::attention_output_into_hop; + +// Same switch as the native kernels: PDL is on unless TRTLLM_ENABLE_PDL=0. +bool pdlEnabled() +{ + static bool const enabled = []() + { + char const* env = std::getenv("TRTLLM_ENABLE_PDL"); + return !(env != nullptr && std::strcmp(env, "0") == 0); + }(); + return enabled; +} + +void checkRows(at::Tensor const& t, char const* name) +{ + TORCH_CHECK(t.is_cuda() && t.scalar_type() == at::kBFloat16, "[glm_moe_dsa_post_attention_hop_from_partials] ", + name, " must be a CUDA bf16 tensor"); + TORCH_CHECK(t.is_contiguous(), "[glm_moe_dsa_post_attention_hop_from_partials] ", name, " must be contiguous"); + TORCH_CHECK(t.dim() >= 1 && t.size(-1) == tk::kHiddenDim, "[glm_moe_dsa_post_attention_hop_from_partials] ", name, + " must have hidden size ", tk::kHiddenDim); +} + +} // namespace + +//! Post-attention hop whose rank-local partials were already published into the hop's transport by the attention +//! output tail. ``residual`` gives the row count. Returns ``[normed, residual_out, router_logits]``; ``router_logits`` +//! is an empty tensor when ``router_weight`` is not given. See kernels/glmMoeDsa/postAttentionHopFromPartials.h. +std::vector postAttentionHopFromPartials(at::Tensor const& residual, at::Tensor const& gamma, double eps, + std::optional const& routerWeight, at::Tensor const& ucBuffer, at::Tensor const& flags, int64_t rank, + int64_t nRanks, bool earlyPoll, int64_t routerLoad) +{ + checkRows(residual, "residual"); + checkRows(gamma, "gamma"); + TORCH_CHECK(gamma.dim() == 1, "[glm_moe_dsa_post_attention_hop_from_partials] gamma must be 1-D"); + int64_t const numRows = residual.numel() / tk::kHiddenDim; + TORCH_CHECK(numRows >= 1 && numRows <= tk::kMaxRows, + "[glm_moe_dsa_post_attention_hop_from_partials] number of rows ", numRows, + " is outside the supported range [1, ", tk::kMaxRows, "]"); + TORCH_CHECK(nRanks == tk::kNumRanks, "[glm_moe_dsa_post_attention_hop_from_partials] only TP", tk::kNumRanks, + " is supported, got ", nRanks); + TORCH_CHECK(rank >= 0 && rank < nRanks, "[glm_moe_dsa_post_attention_hop_from_partials] invalid rank ", rank); + TORCH_CHECK(routerLoad >= 0 && routerLoad <= 2, + "[glm_moe_dsa_post_attention_hop_from_partials] router_load must be 0, 1 or 2, got ", routerLoad); + TORCH_CHECK(ucBuffer.is_cuda() && ucBuffer.is_contiguous() + && static_cast(ucBuffer.numel() * ucBuffer.element_size()) >= tk::kWorkspaceBytes, + "[glm_moe_dsa_post_attention_hop_from_partials] uc_buffer must be a contiguous CUDA tensor of at least ", + tk::kWorkspaceBytes, " bytes"); + TORCH_CHECK(flags.is_cuda() && flags.is_contiguous() && flags.scalar_type() == at::kInt && flags.numel() >= 4, + "[glm_moe_dsa_post_attention_hop_from_partials] flags must be a contiguous CUDA int32 tensor with at least 4 " + "elements"); + TORCH_CHECK(residual.get_device() == ucBuffer.get_device() && residual.get_device() == flags.get_device(), + "[glm_moe_dsa_post_attention_hop_from_partials] all tensors must be on the same device"); + + at::Tensor routerLogits; + if (routerWeight.has_value()) + { + auto const& w = routerWeight.value(); + TORCH_CHECK(w.is_cuda() && w.scalar_type() == at::kBFloat16 && w.is_contiguous() && w.dim() == 2 + && w.size(0) == tk::kNumExperts && w.size(1) == tk::kHiddenDim, + "[glm_moe_dsa_post_attention_hop_from_partials] router_weight must be a contiguous CUDA bf16 tensor of " + "shape [", + tk::kNumExperts, ", ", tk::kHiddenDim, "]"); + routerLogits = at::empty({numRows, tk::kNumExperts}, residual.options().dtype(at::kFloat)); + } + else + { + routerLogits = at::empty({0}, residual.options().dtype(at::kFloat)); + } + + c10::cuda::CUDAGuard const deviceGuard(residual.device()); + at::Tensor normOut = at::empty_like(residual); + at::Tensor residualOut = at::empty_like(residual); + + tk::PostAttentionHopFromPartialsParams params; + params.residualIn = residual.const_data_ptr(); + params.gamma = gamma.const_data_ptr(); + params.routerWeight = routerWeight.has_value() ? routerWeight->const_data_ptr() : nullptr; + params.normOut = normOut.mutable_data_ptr(); + params.residualOut = residualOut.mutable_data_ptr(); + params.routerLogits = routerWeight.has_value() ? routerLogits.mutable_data_ptr() : nullptr; + params.ucBuffer = ucBuffer.mutable_data_ptr(); + params.flags = reinterpret_cast(flags.mutable_data_ptr()); + params.numRows = static_cast(numRows); + params.rank = static_cast(rank); + params.eps = static_cast(eps); + params.enablePdl = pdlEnabled(); + params.earlyPoll = earlyPoll; + params.routerLoad = static_cast(routerLoad); + params.stream = at::cuda::getCurrentCUDAStream(residual.get_device()); + + cudaError_t const status = tk::invokePostAttentionHopFromPartials(params); + TORCH_CHECK(status == cudaSuccess, + "[glm_moe_dsa_post_attention_hop_from_partials] kernel launch failed: ", cudaGetErrorString(status)); + return {normOut, residualOut, routerLogits}; +} + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_post_attention_hop_from_partials(Tensor residual, Tensor gamma, float eps, Tensor? router_weight, " + "Tensor uc_buffer, Tensor flags, int rank, int nranks, bool early_poll, int router_load=1) -> Tensor[]"); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_post_attention_hop_from_partials", + &tensorrt_llm::torch_ext::glm_moe_dsa::postAttentionHopFromPartials); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopOp.cpp new file mode 100644 index 000000000000..de99e3968c6b --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopOp.cpp @@ -0,0 +1,170 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/postAttentionHop.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ + +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::post_attention_hop; + +// Same switch as the native kernels: PDL is on unless TRTLLM_ENABLE_PDL=0. +bool pdlEnabled() +{ + static bool const enabled = []() + { + char const* env = std::getenv("TRTLLM_ENABLE_PDL"); + return !(env != nullptr && std::strcmp(env, "0") == 0); + }(); + return enabled; +} + +void checkRows(at::Tensor const& t, char const* name) +{ + TORCH_CHECK(t.is_cuda() && t.scalar_type() == at::kBFloat16, "[glm_moe_dsa_post_attention_hop] ", name, + " must be a CUDA bf16 tensor"); + TORCH_CHECK(t.is_contiguous(), "[glm_moe_dsa_post_attention_hop] ", name, " must be contiguous"); + TORCH_CHECK(t.dim() >= 1 && t.size(-1) == tk::kHiddenDim, "[glm_moe_dsa_post_attention_hop] ", name, + " must have hidden size ", tk::kHiddenDim); +} + +} // namespace + +//! Fused post-attention hop: one-shot cross-rank sum of ``input`` + residual + RMSNorm (+ router logits). +//! Returns ``[normed, residual_out, router_logits]``; ``router_logits`` is an empty tensor when ``router_weight`` is +//! not given. See kernels/glmMoeDsa/postAttentionHop.h for the arithmetic and the workspace protocol. +std::vector postAttentionHop(at::Tensor const& input, at::Tensor const& residual, at::Tensor const& gamma, + double eps, std::optional const& routerWeight, at::Tensor const& ucBuffer, int64_t mcPtr, + at::Tensor const& flags, int64_t rank, int64_t nRanks) +{ + checkRows(input, "input"); + checkRows(residual, "residual"); + TORCH_CHECK( + residual.sizes() == input.sizes(), "[glm_moe_dsa_post_attention_hop] residual must have the shape of input"); + checkRows(gamma, "gamma"); + TORCH_CHECK(gamma.dim() == 1, "[glm_moe_dsa_post_attention_hop] gamma must be 1-D"); + int64_t const numRows = input.numel() / tk::kHiddenDim; + TORCH_CHECK(numRows >= 1 && numRows <= tk::kMaxRows, "[glm_moe_dsa_post_attention_hop] number of rows ", numRows, + " is outside the supported range [1, ", tk::kMaxRows, "]"); + TORCH_CHECK(nRanks == tk::kNumRanks, "[glm_moe_dsa_post_attention_hop] only TP", tk::kNumRanks, + " is supported, got ", nRanks); + TORCH_CHECK(rank >= 0 && rank < nRanks, "[glm_moe_dsa_post_attention_hop] invalid rank ", rank); + TORCH_CHECK(ucBuffer.is_cuda() && ucBuffer.is_contiguous() + && static_cast(ucBuffer.numel() * ucBuffer.element_size()) >= tk::kWorkspaceBytes, + "[glm_moe_dsa_post_attention_hop] uc_buffer must be a contiguous CUDA tensor of at least ", tk::kWorkspaceBytes, + " bytes"); + TORCH_CHECK( + mcPtr != 0, "[glm_moe_dsa_post_attention_hop] mc_ptr must be the multicast address of uc_buffer's workspace"); + TORCH_CHECK(flags.is_cuda() && flags.is_contiguous() && flags.scalar_type() == at::kInt && flags.numel() >= 4, + "[glm_moe_dsa_post_attention_hop] flags must be a contiguous CUDA int32 tensor with at least 4 elements"); + TORCH_CHECK(input.get_device() == ucBuffer.get_device() && input.get_device() == flags.get_device(), + "[glm_moe_dsa_post_attention_hop] all tensors must be on the same device"); + + at::Tensor routerLogits; + if (routerWeight.has_value()) + { + auto const& w = routerWeight.value(); + TORCH_CHECK(w.is_cuda() && w.scalar_type() == at::kBFloat16 && w.is_contiguous() && w.dim() == 2 + && w.size(0) == tk::kNumExperts && w.size(1) == tk::kHiddenDim, + "[glm_moe_dsa_post_attention_hop] router_weight must be a contiguous CUDA bf16 tensor of shape [", + tk::kNumExperts, ", ", tk::kHiddenDim, "]"); + routerLogits = at::empty({numRows, tk::kNumExperts}, input.options().dtype(at::kFloat)); + } + else + { + routerLogits = at::empty({0}, input.options().dtype(at::kFloat)); + } + + c10::cuda::CUDAGuard const deviceGuard(input.device()); + at::Tensor normOut = at::empty_like(input); + at::Tensor residualOut = at::empty_like(residual); + + tk::PostAttnHopParams params; + params.input = input.const_data_ptr(); + params.residualIn = residual.const_data_ptr(); + params.gamma = gamma.const_data_ptr(); + params.routerWeight = routerWeight.has_value() ? routerWeight->const_data_ptr() : nullptr; + params.normOut = normOut.mutable_data_ptr(); + params.residualOut = residualOut.mutable_data_ptr(); + params.routerLogits = routerWeight.has_value() ? routerLogits.mutable_data_ptr() : nullptr; + params.ucBuffer = ucBuffer.mutable_data_ptr(); + params.mcBuffer = reinterpret_cast(mcPtr); + params.flags = reinterpret_cast(flags.mutable_data_ptr()); + params.numRows = static_cast(numRows); + params.rank = static_cast(rank); + params.eps = static_cast(eps); + params.enablePdl = pdlEnabled(); + params.stream = at::cuda::getCurrentCUDAStream(input.get_device()); + + cudaError_t const status = tk::invokePostAttentionHop(params); + TORCH_CHECK( + status == cudaSuccess, "[glm_moe_dsa_post_attention_hop] kernel launch failed: ", cudaGetErrorString(status)); + return {normOut, residualOut, routerLogits}; +} + +#ifdef TLLM_GLM_LL_W2_TIMING +//! Development builds only: phase timestamps of the most recent hop launch. +at::Tensor postAttentionHopTiming() +{ + at::Tensor out = at::empty({tk::kTimingMaxCtas, tk::kTimingStamps}, at::TensorOptions().dtype(at::kLong)); + auto stream = at::cuda::getCurrentCUDAStream(); + TORCH_CHECK(tk::copyPostAttnHopTiming(reinterpret_cast(out.data_ptr()), stream) + == cudaSuccess); + TORCH_CHECK(cudaStreamSynchronize(stream) == cudaSuccess); + return out; +} +#endif + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_post_attention_hop(Tensor input, Tensor residual, Tensor gamma, float eps, Tensor? router_weight, " + "Tensor uc_buffer, int mc_ptr, Tensor flags, int rank, int nranks) -> Tensor[]"); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_post_attention_hop", &tensorrt_llm::torch_ext::glm_moe_dsa::postAttentionHop); +} + +#ifdef TLLM_GLM_LL_W2_TIMING +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def("glm_moe_dsa_post_attention_hop_timing() -> Tensor", + &tensorrt_llm::torch_ext::glm_moe_dsa::postAttentionHopTiming); +} +#endif diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopRoutingOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopRoutingOp.cpp new file mode 100644 index 000000000000..0d71f2b34496 --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/postAttentionHopRoutingOp.cpp @@ -0,0 +1,189 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/config.h" +#include "tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ + +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::hop_routing; + +// Same switch as the native kernels: PDL is on unless TRTLLM_ENABLE_PDL=0. +bool routingPdlEnabled() +{ + static bool const enabled = []() + { + char const* env = std::getenv("TRTLLM_ENABLE_PDL"); + return !(env != nullptr && std::strcmp(env, "0") == 0); + }(); + return enabled; +} + +void checkBf16(at::Tensor const& t, char const* name, int64_t lastDim) +{ + TORCH_CHECK(t.is_cuda() && t.scalar_type() == at::kBFloat16, "[glm_moe_dsa_post_attention_hop_routing] ", name, + " must be a CUDA bf16 tensor"); + TORCH_CHECK(t.is_contiguous(), "[glm_moe_dsa_post_attention_hop_routing] ", name, " must be contiguous"); + TORCH_CHECK(t.dim() >= 1 && t.size(-1) == lastDim, "[glm_moe_dsa_post_attention_hop_routing] ", name, + " must have last dimension ", lastDim); +} + +void checkWords(at::Tensor const& t, char const* name, int64_t minWords) +{ + TORCH_CHECK(t.is_cuda() && t.is_contiguous() && t.scalar_type() == at::kInt && t.numel() >= minWords, + "[glm_moe_dsa_post_attention_hop_routing] ", name, " must be a contiguous CUDA int32 tensor with at least ", + minWords, " elements"); +} + +} // namespace + +//! Pre-MoE hop with top-8 routing: the transport-input hop with router logits, plus the native +//! DeepSeek-V3 routing of every row. Returns ``[normed, residual_out, router_logits, routing]`` where ``routing`` is +//! an int32 ``[2, R, 8]`` tensor: ``routing[0]`` holds the expert ids and ``routing[1]`` the FP32 bits of the +//! bf16-rounded weights, both in descending native key order. See kernels/glmMoeDsa/postAttentionHopRouting.h. +std::vector postAttentionHopRouting(at::Tensor const& residual, at::Tensor const& gamma, double eps, + at::Tensor const& routerWeight, at::Tensor const& routingBias, at::Tensor const& ucBuffer, at::Tensor const& flags, + at::Tensor const& gather, at::Tensor const& zstate, int64_t rank, int64_t nRanks, bool earlyPoll, + int64_t routerLoad, double routedScaling, std::optional const& logitsIn, + std::optional const& stamps) +{ + checkBf16(residual, "residual", tk::kHiddenDim); + checkBf16(gamma, "gamma", tk::kHiddenDim); + TORCH_CHECK(gamma.dim() == 1, "[glm_moe_dsa_post_attention_hop_routing] gamma must be 1-D"); + checkBf16(routerWeight, "router_weight", tk::kHiddenDim); + TORCH_CHECK(routerWeight.dim() == 2 && routerWeight.size(0) == tk::kNumExperts, + "[glm_moe_dsa_post_attention_hop_routing] router_weight must have shape [", tk::kNumExperts, ", ", + tk::kHiddenDim, "]"); + checkBf16(routingBias, "routing_bias", tk::kNumExperts); + TORCH_CHECK(routingBias.dim() == 1, "[glm_moe_dsa_post_attention_hop_routing] routing_bias must be 1-D"); + int64_t const numRows = residual.numel() / tk::kHiddenDim; + TORCH_CHECK(numRows >= 1 && numRows <= tk::kMaxRows, "[glm_moe_dsa_post_attention_hop_routing] number of rows ", + numRows, " is outside the supported range [1, ", tk::kMaxRows, "]"); + TORCH_CHECK(nRanks == tk::kNumRanks, "[glm_moe_dsa_post_attention_hop_routing] only TP", tk::kNumRanks, + " is supported, got ", nRanks); + TORCH_CHECK(rank >= 0 && rank < nRanks, "[glm_moe_dsa_post_attention_hop_routing] invalid rank ", rank); + TORCH_CHECK(routerLoad >= 0 && routerLoad <= 2, + "[glm_moe_dsa_post_attention_hop_routing] router_load must be 0, 1 or 2, got ", routerLoad); + TORCH_CHECK(ucBuffer.is_cuda() && ucBuffer.is_contiguous() + && static_cast(ucBuffer.numel() * ucBuffer.element_size()) >= tk::kWorkspaceBytes, + "[glm_moe_dsa_post_attention_hop_routing] uc_buffer must be a contiguous CUDA tensor of at least ", + tk::kWorkspaceBytes, " bytes"); + checkWords(flags, "flags", 4); + checkWords(gather, "gather", tk::kGatherWords); + checkWords(zstate, "zstate", tk::kStateWords); + auto const device = residual.get_device(); + TORCH_CHECK(gamma.get_device() == device && routerWeight.get_device() == device + && routingBias.get_device() == device && ucBuffer.get_device() == device && flags.get_device() == device + && gather.get_device() == device && zstate.get_device() == device, + "[glm_moe_dsa_post_attention_hop_routing] all tensors must be on the same device"); + if (logitsIn.has_value()) + { + auto const& l = logitsIn.value(); + TORCH_CHECK(l.is_cuda() && l.get_device() == device && l.scalar_type() == at::kFloat && l.is_contiguous() + && l.numel() == numRows * tk::kNumExperts, + "[glm_moe_dsa_post_attention_hop_routing] logits_in must be a contiguous CUDA fp32 tensor of shape [", + numRows, ", ", tk::kNumExperts, "]"); + TORCH_CHECK(routerLoad == 1, "[glm_moe_dsa_post_attention_hop_routing] logits_in needs router_load=1"); + } + if (stamps.has_value()) + { + auto const& s = stamps.value(); + TORCH_CHECK(tk::stampsCompiled(), + "[glm_moe_dsa_post_attention_hop_routing] stamps need a TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS build"); + TORCH_CHECK(s.is_cuda() && s.get_device() == device && s.scalar_type() == at::kLong && s.is_contiguous() + && s.numel() >= tk::kMaxGridCtas * tk::kNumStamps, + "[glm_moe_dsa_post_attention_hop_routing] stamps must be a contiguous CUDA int64 tensor with at least ", + tk::kMaxGridCtas * tk::kNumStamps, " elements"); + } + + c10::cuda::CUDAGuard const deviceGuard(residual.device()); + at::Tensor normOut = at::empty_like(residual); + at::Tensor residualOut = at::empty_like(residual); + at::Tensor routerLogits = at::empty({numRows, tk::kNumExperts}, residual.options().dtype(at::kFloat)); + at::Tensor routing = at::empty({2, numRows, tk::kTopK}, residual.options().dtype(at::kInt)); + + tk::PostAttentionHopRoutingParams params; + params.residualIn = residual.const_data_ptr(); + params.gamma = gamma.const_data_ptr(); + params.routerWeight = routerWeight.const_data_ptr(); + params.routingBias = routingBias.const_data_ptr(); + params.logitsIn = logitsIn.has_value() ? logitsIn->const_data_ptr() : nullptr; + params.normOut = normOut.mutable_data_ptr(); + params.residualOut = residualOut.mutable_data_ptr(); + params.routerLogits = routerLogits.mutable_data_ptr(); + params.topkIds = routing.mutable_data_ptr(); + params.topkWeights = reinterpret_cast(routing.mutable_data_ptr() + numRows * tk::kTopK); + params.ucBuffer = ucBuffer.mutable_data_ptr(); + params.flags = reinterpret_cast(flags.mutable_data_ptr()); + params.gather = reinterpret_cast(gather.mutable_data_ptr()); + params.zstate = reinterpret_cast(zstate.mutable_data_ptr()); + params.stamps + = stamps.has_value() ? reinterpret_cast(stamps->mutable_data_ptr()) : nullptr; + params.numRows = static_cast(numRows); + params.rank = static_cast(rank); + params.eps = static_cast(eps); + params.routedScaling = static_cast(routedScaling); + params.enablePdl = routingPdlEnabled(); + params.earlyPoll = earlyPoll; + params.routerLoad = static_cast(routerLoad); + params.stream = at::cuda::getCurrentCUDAStream(device); + + cudaError_t const status = tk::invokePostAttentionHopRouting(params); + TORCH_CHECK(status == cudaSuccess, + "[glm_moe_dsa_post_attention_hop_routing] kernel launch failed: ", cudaGetErrorString(status)); + return {normOut, residualOut, routerLogits, routing}; +} + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +// A diagnostic build (stamps) registers under its own name, so it can be loaded next to a build that has the op. +#if defined(TLLM_GLM_MOE_DSA_HOP_ROUTING_STAMPS) +#define GLM_MOE_DSA_HOP_ROUTING_OP_NAME "glm_moe_dsa_post_attention_hop_routing_diag" +#else +#define GLM_MOE_DSA_HOP_ROUTING_OP_NAME "glm_moe_dsa_post_attention_hop_routing" +#endif + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def(GLM_MOE_DSA_HOP_ROUTING_OP_NAME + "(Tensor residual, Tensor gamma, float eps, Tensor router_weight, Tensor routing_bias, " + "Tensor uc_buffer, Tensor flags, Tensor gather, Tensor zstate, int rank, int nranks, bool early_poll, " + "int router_load, float routed_scaling, Tensor? logits_in=None, Tensor? stamps=None) -> Tensor[]"); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl(GLM_MOE_DSA_HOP_ROUTING_OP_NAME, &tensorrt_llm::torch_ext::glm_moe_dsa::postAttentionHopRouting); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/ropeKvAppendOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/ropeKvAppendOp.cpp new file mode 100644 index 000000000000..4411b6f88af8 --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/ropeKvAppendOp.cpp @@ -0,0 +1,330 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/common/envUtils.h" +#include "tensorrt_llm/common/quantization.h" +#include "tensorrt_llm/kernels/glmMoeDsa/ropeKvAppend.h" +#include "tensorrt_llm/kernels/kvCacheUtils.h" + +#include +#include + +#include +#include +#include +#include + +namespace tk = tensorrt_llm::kernels; +namespace tc = tensorrt_llm::common; +namespace rope_kv_append = tensorrt_llm::kernels::glm_moe_dsa::rope_kv_append; + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ + +void checkCuda(torch::Tensor const& t, char const* name) +{ + TORCH_CHECK(t.is_cuda(), name, " must be a CUDA tensor"); +} + +//! Paged MLA latent cache of one layer: the KVBlockArray that buildPagedKvCacheBuffers (thop/attentionOp.cpp) +//! builds for an FP8 MLA cache (kvFactor 1, one KV head, 8-bit elements, beam width 1, no FP4 scale pool). +tk::KVBlockArray buildFp8MlaKvBlockArray(torch::Tensor const& kvCacheBlockOffsets, + torch::Tensor const& hostKvCachePoolPointers, torch::Tensor const& hostKvCachePoolMapping, int64_t layerIdx, + int32_t batchSize, int64_t tokensPerBlock, int64_t attentionWindowSize, int64_t seqOffset) +{ + TORCH_CHECK(hostKvCachePoolMapping.device().is_cpu() && hostKvCachePoolMapping.dim() == 2 + && hostKvCachePoolMapping.size(1) >= 2 && hostKvCachePoolMapping.scalar_type() == torch::kInt32, + "host_kv_cache_pool_mapping must be a CPU int32 [layers, >=2] tensor"); + TORCH_CHECK(layerIdx >= 0 && layerIdx < hostKvCachePoolMapping.size(0), "layer index out of the pool mapping"); + auto const* mapping = hostKvCachePoolMapping.data_ptr(); + int32_t const poolIndex = mapping[layerIdx * hostKvCachePoolMapping.stride(0)]; + int32_t const layerIdxInCachePool + = mapping[layerIdx * hostKvCachePoolMapping.stride(0) + hostKvCachePoolMapping.stride(1)]; + + TORCH_CHECK(hostKvCachePoolPointers.device().is_cpu() && hostKvCachePoolPointers.dim() == 2 + && hostKvCachePoolPointers.scalar_type() == torch::kInt64 && poolIndex >= 0 + && poolIndex < hostKvCachePoolPointers.size(0) && hostKvCachePoolPointers.size(1) >= 2, + "host_kv_cache_pool_pointers must be a CPU int64 [pools, 2] tensor"); + TORCH_CHECK(kvCacheBlockOffsets.is_cuda() && kvCacheBlockOffsets.scalar_type() == torch::kInt32 + && kvCacheBlockOffsets.dim() >= 2 && poolIndex < kvCacheBlockOffsets.size(0) + && seqOffset < kvCacheBlockOffsets.size(1), + "kv_cache_block_offsets must be CUDA int32 [pools, seqs, 2, max_blocks]"); + + int64_t constexpr kCacheElemBits = 8; + int64_t const blockSize = tokensPerBlock * /*kv_head_num=*/1 * rope_kv_append::kLatentDim; + int64_t const bytesPerBlock = blockSize * kCacheElemBits / CHAR_BIT; + int64_t const intraPoolOffset = layerIdxInCachePool * /*kvFactor=*/1 * bytesPerBlock; + auto const sizePerToken = static_cast(rope_kv_append::kLatentDim * kCacheElemBits / 8); + auto const* pointers = hostKvCachePoolPointers.data_ptr(); + auto const rowOffset = poolIndex * hostKvCachePoolPointers.stride(0); + void* primaryPoolPtr = reinterpret_cast(pointers[rowOffset]) + intraPoolOffset; + void* secondaryPoolPtr + = reinterpret_cast(pointers[rowOffset + hostKvCachePoolPointers.stride(1)]) + intraPoolOffset; + auto* blockOffsets + = reinterpret_cast(static_cast(kvCacheBlockOffsets.data_ptr()) + + poolIndex * kvCacheBlockOffsets.stride(0) + seqOffset * kvCacheBlockOffsets.stride(1)); + auto const maxBlocksPerSequence = static_cast(kvCacheBlockOffsets.size(-1)); + auto const window = static_cast(attentionWindowSize); + return tk::KVBlockArray(batchSize, maxBlocksPerSequence, static_cast(tokensPerBlock), sizePerToken, window, + window, /*sinkTokenLen=*/0, /*canUseOneMoreBlock=*/false, primaryPoolPtr, secondaryPoolPtr, blockOffsets); +} + +} // namespace + +//! Generation MLA RoPE + FP8 query quantization + FP8 latent-cache append (the FP8-cache, absorption-mode, +//! non-helix subset of trtllm::mla_rope_generation), optionally fused with the DSA indexer K-cache scatter of +//! trtllm::indexer_k_cache_scatter_op. Argument meanings follow those two native ops. +void ropeKvAppend(torch::Tensor const& fused_q, torch::Tensor const& q_pe, torch::Tensor const& latent_cache, + torch::Tensor const& rotary_cos_sin, torch::Tensor const& cu_q_seqlens, torch::Tensor const& cu_kv_seqlens, + torch::Tensor const& fmha_scheduler_counter, torch::Tensor const& mla_bmm1_scale, + torch::Tensor const& mla_bmm2_scale, torch::Tensor const& quant_q_buffer, torch::Tensor const& sequence_length, + torch::Tensor const& host_context_lengths, int64_t num_contexts, torch::Tensor const& kv_cache_block_offsets, + torch::Tensor const& host_kv_cache_pool_pointers, torch::Tensor const& host_kv_cache_pool_mapping, + std::optional const& kv_cache_scale_orig_quant, int64_t layer_idx, int64_t num_heads, + int64_t tokens_per_block, int64_t attention_window_size, int64_t quant_mode, double q_scaling, + int64_t qk_nope_head_dim, int64_t qk_rope_head_dim, int64_t kv_lora_rank, + std::optional const& idx_k_fp8, std::optional const& idx_k_scale, + std::optional const& idx_k_cache, std::optional const& idx_slot_mapping_fp8, + std::optional const& idx_slot_mapping_scale, int64_t idx_num_tokens) +{ + TORCH_CHECK(kv_lora_rank == rope_kv_append::kKvLoraRank && qk_rope_head_dim == rope_kv_append::kRopeDim, + "glm_moe_dsa_rope_kv_append: supports kv_lora_rank 512 and qk_rope_head_dim 64 only"); + auto const kvQuantMode = tc::QuantMode(static_cast(quant_mode)); + TORCH_CHECK(kvQuantMode.hasFp8KvCache() && !kvQuantMode.hasFp4KvCache(), + "glm_moe_dsa_rope_kv_append: needs an FP8 MLA cache"); + checkCuda(fused_q, "fused_q"); + checkCuda(q_pe, "q_pe"); + checkCuda(latent_cache, "latent_cache"); + TORCH_CHECK(fused_q.scalar_type() == torch::kBFloat16 && q_pe.scalar_type() == torch::kBFloat16 + && latent_cache.scalar_type() == torch::kBFloat16, + "glm_moe_dsa_rope_kv_append: needs BF16 fused_q, q_pe and latent_cache"); + TORCH_CHECK(rotary_cos_sin.scalar_type() == torch::kFloat32 && rotary_cos_sin.is_contiguous(), + "rotary_cos_sin must be packed float32"); + int64_t const cosSinRows = rotary_cos_sin.numel() / (2 * rope_kv_append::kRopeDim); + TORCH_CHECK(cosSinRows > 0 && cosSinRows <= INT32_MAX, "rotary_cos_sin has no complete position row"); + TORCH_CHECK(q_pe.dim() == 3 && q_pe.stride(2) == 1, "q_pe must be [tokens, heads, rope] with unit inner stride"); + TORCH_CHECK(latent_cache.dim() == 2 && latent_cache.size(1) == rope_kv_append::kLatentDim + && latent_cache.stride(1) == 1 && latent_cache.stride(0) == rope_kv_append::kLatentDim, + "latent_cache must be packed [tokens, 576]"); + TORCH_CHECK(fused_q.is_contiguous() && quant_q_buffer.is_contiguous(), "fused_q and quant_q_buffer must be packed"); + TORCH_CHECK(host_kv_cache_pool_mapping.dim() >= 1, "host_kv_cache_pool_mapping is required"); + + int32_t const numTokens = static_cast(latent_cache.size(0)); + int32_t const numSeqs = static_cast(host_context_lengths.size(0)); + int32_t const numGenerations = numSeqs - static_cast(num_contexts); + TORCH_CHECK(numGenerations > 0, "glm_moe_dsa_rope_kv_append: needs at least one generation request"); + TORCH_CHECK(numTokens % numGenerations == 0, "MLA can only support input sequences with the same sequence length."); + TORCH_CHECK(fused_q.numel() >= static_cast(numTokens) * num_heads * rope_kv_append::kLatentDim, + "fused_q is smaller than [tokens, heads, 576]"); + TORCH_CHECK(quant_q_buffer.numel() * quant_q_buffer.element_size() + >= static_cast(numTokens) * num_heads * rope_kv_append::kLatentDim, + "quant_q_buffer is smaller than [tokens, heads, 576] bytes"); + TORCH_CHECK(mla_bmm1_scale.numel() >= 2 && mla_bmm2_scale.numel() >= 1, "bmm scale buffers too small"); + TORCH_CHECK(cu_q_seqlens.numel() >= numGenerations + 1 && cu_kv_seqlens.numel() >= numGenerations + 1, + "cu_seqlens buffers too small"); + + auto stream = at::cuda::getCurrentCUDAStream(latent_cache.get_device()); + rope_kv_append::RopeAppendParams params{}; + params.fusedQ = reinterpret_cast<__nv_bfloat16 const*>(fused_q.data_ptr()); + params.qPe = reinterpret_cast<__nv_bfloat16 const*>(q_pe.data_ptr()); + params.qPeLd = static_cast(q_pe.stride(1)); + params.qPeStride = static_cast(q_pe.stride(0)); + params.quantQ = reinterpret_cast(quant_q_buffer.data_ptr()); + params.quantScaleQ = nullptr; + params.latent = reinterpret_cast<__nv_bfloat16 const*>(latent_cache.data_ptr()); + params.cosSin = reinterpret_cast(rotary_cos_sin.data_ptr()); + params.cosSinRows = static_cast(cosSinRows); + params.kvLens = sequence_length.slice(0, num_contexts).data_ptr(); + params.quantScaleKv + = kv_cache_scale_orig_quant.has_value() ? kv_cache_scale_orig_quant->data_ptr() : nullptr; + params.kvCache = buildFp8MlaKvBlockArray(kv_cache_block_offsets, host_kv_cache_pool_pointers, + host_kv_cache_pool_mapping, layer_idx, numGenerations, tokens_per_block, attention_window_size, num_contexts); + params.cuQSeqlens = cu_q_seqlens.data_ptr(); + params.cuKvSeqlens = cu_kv_seqlens.data_ptr(); + params.fmhaTileCounter = reinterpret_cast(fmha_scheduler_counter.data_ptr()); + params.bmm1Scale = mla_bmm1_scale.data_ptr(); + params.bmm2Scale = mla_bmm2_scale.data_ptr(); + params.dequantScaleQ = nullptr; + params.dequantScaleKv = nullptr; + params.quantScaleO = nullptr; + // Same expression as the native op so the folded FMHA scales match bit for bit. + params.hostBmm1Scale = 1.F / (q_scaling * std::sqrt(static_cast(qk_nope_head_dim + qk_rope_head_dim))); + params.numTokens = numTokens; + params.seqLen = numTokens / numGenerations; + params.numHeads = static_cast(num_heads); + + if (idx_num_tokens > 0) + { + TORCH_CHECK(idx_k_fp8.has_value() && idx_k_scale.has_value() && idx_k_cache.has_value() + && idx_slot_mapping_fp8.has_value() && idx_slot_mapping_scale.has_value(), + "indexer scatter needs k_fp8, k_scale, k_cache and both slot mappings"); + auto const& kFp8 = idx_k_fp8.value(); + auto const& kScale = idx_k_scale.value(); + auto const& kCache = idx_k_cache.value(); + auto const& slotFp8 = idx_slot_mapping_fp8.value(); + auto const& slotScale = idx_slot_mapping_scale.value(); + checkCuda(kFp8, "k_fp8"); + checkCuda(kScale, "k_scale"); + checkCuda(kCache, "k_cache"); + TORCH_CHECK(kFp8.dim() == 2 && kFp8.element_size() == 1 && kFp8.is_contiguous(), "k_fp8 must be packed bytes"); + TORCH_CHECK(kScale.dim() == 2 && kScale.element_size() == 4 && kScale.is_contiguous() + && kScale.size(1) * kScale.element_size() == 4, + "k_scale must be one packed 4-byte word per token"); + TORCH_CHECK(kCache.dim() == 4 && kCache.size(2) == 1, "k_cache must be [blocks, block_size, 1, bytes]"); + TORCH_CHECK(slotFp8.scalar_type() == torch::kInt64 && slotScale.scalar_type() == torch::kInt64 + && slotFp8.is_contiguous() && slotScale.is_contiguous(), + "slot mappings must be packed int64"); + int32_t const headBytes = static_cast(kFp8.size(1)); + TORCH_CHECK(headBytes == 128 || headBytes == 64, "indexer k head bytes must be 128 or 64"); + TORCH_CHECK(kFp8.size(0) >= idx_num_tokens && kScale.size(0) >= idx_num_tokens + && slotFp8.numel() >= idx_num_tokens && slotScale.numel() >= idx_num_tokens, + "indexer inputs are shorter than idx_num_tokens"); + params.idxK = reinterpret_cast(kFp8.data_ptr()); + params.idxScale = reinterpret_cast(kScale.data_ptr()); + params.idxCache = reinterpret_cast(kCache.data_ptr()); + params.idxSlotData = slotFp8.data_ptr(); + params.idxSlotScale = slotScale.data_ptr(); + params.idxNumTokens = static_cast(idx_num_tokens); + params.idxHeadBytes = headBytes; + for (int32_t d = 0; d < 4; ++d) + { + params.idxDims[d] = static_cast(kCache.size(d)); + params.idxStrides[d] = kCache.stride(d); + } + } + + rope_kv_append::invokeRopeAppendScatter(params, tc::getEnvEnablePDL(), stream); +} + +//! Local top-k token ids -> global KV-pool indices (values of trtllm::convert_req_index_to_global and its grouped +//! variant), optionally fused with the native top-k reuse copy. Returns [group, rows, topk] when layer_ids is given, +//! otherwise [rows, topk]. +torch::Tensor topkIndexBridge(torch::Tensor const& topk, int64_t num_rows, torch::Tensor const& req_idx, + int64_t req_idx_bias, torch::Tensor const& block_table, int64_t block_size, int64_t stride_factor, + std::optional const& layer_ids, int64_t layer_id, std::optional const& copy_dst, + int64_t copy_row_start, int64_t copy_rows, int64_t copy_mode, int64_t next_n, + std::optional const& num_accepted) +{ + checkCuda(topk, "topk"); + checkCuda(req_idx, "req_idx"); + checkCuda(block_table, "block_table"); + TORCH_CHECK(topk.scalar_type() == torch::kInt32 && topk.dim() == 2 && topk.stride(1) == 1, + "topk must be int32 [rows, topk] with unit column stride"); + TORCH_CHECK(req_idx.scalar_type() == torch::kInt32 && req_idx.dim() == 1 && req_idx.is_contiguous(), + "req_idx must be packed int32"); + TORCH_CHECK(block_table.scalar_type() == torch::kInt32 && block_table.dim() == 2, "block_table must be int32 2-D"); + TORCH_CHECK(copy_mode == 0 || copy_mode == 1, "copy_mode must be 0 or 1"); + int64_t const numTopk = topk.size(1); + TORCH_CHECK(req_idx.size(0) >= num_rows, "req_idx shorter than num_rows"); + TORCH_CHECK(copy_mode == 1 || topk.size(0) >= num_rows, "topk shorter than num_rows"); + TORCH_CHECK(copy_mode == 0 || topk.size(0) >= copy_rows * next_n, "topk shorter than copy_rows * next_n"); + TORCH_CHECK(copy_mode == 1 || copy_rows <= num_rows, "copy_rows exceeds num_rows"); + + int32_t groupSize = 1; + int32_t const* layerIdsPtr = nullptr; + if (layer_ids.has_value()) + { + TORCH_CHECK(layer_ids->scalar_type() == torch::kInt32 && layer_ids->dim() == 1 && layer_ids->is_contiguous(), + "layer_ids must be packed int32"); + groupSize = static_cast(layer_ids->size(0)); + layerIdsPtr = layer_ids->data_ptr(); + } + torch::Tensor out = layer_ids.has_value() ? torch::empty({groupSize, num_rows, numTopk}, topk.options()) + : torch::empty({num_rows, numTopk}, topk.options()); + + rope_kv_append::TopkBridgeParams params{}; + params.topk = topk.data_ptr(); + params.topkStride0 = topk.stride(0); + params.numRows = static_cast(num_rows); + params.numTopk = static_cast(numTopk); + params.reqIdx = req_idx.data_ptr(); + params.reqIdxBias = static_cast(req_idx_bias); + params.blockTable = block_table.data_ptr(); + params.btStride0 = block_table.stride(0); + params.btStride1 = block_table.stride(1); + params.maxBlocksPerReq = static_cast(block_table.size(1)); + params.blockSize = static_cast(block_size); + params.strideFactor = static_cast(stride_factor); + params.layerIds = layerIdsPtr; + params.layerId = static_cast(layer_id); + params.groupSize = groupSize; + params.out = out.data_ptr(); + params.outStrideG = layer_ids.has_value() ? out.stride(0) : 0; + params.outStride0 = layer_ids.has_value() ? out.stride(1) : out.stride(0); + if (copy_dst.has_value() && copy_rows > 0) + { + auto const& dst = copy_dst.value(); + checkCuda(dst, "copy_dst"); + TORCH_CHECK(dst.scalar_type() == torch::kInt32 && dst.dim() == 2 && dst.stride(1) == 1 && dst.size(1) >= numTopk + && dst.size(0) >= copy_row_start + copy_rows, + "copy_dst must be int32 [>= start + rows, >= topk] with unit column stride"); + params.copyDst = dst.data_ptr(); + params.copyStride0 = dst.stride(0); + params.copyRows = static_cast(copy_rows); + params.copyRowStart = static_cast(copy_row_start); + params.copyMode = static_cast(copy_mode); + params.nextN = static_cast(next_n); + if (copy_mode == 1 && num_accepted.has_value()) + { + auto const& acc = num_accepted.value(); + checkCuda(acc, "num_accepted"); + TORCH_CHECK((acc.scalar_type() == torch::kInt32 || acc.scalar_type() == torch::kInt64) && acc.dim() == 1 + && acc.is_contiguous() && acc.size(0) >= copy_rows, + "num_accepted must be packed int32/int64 with at least copy_rows entries"); + params.numAccepted = acc.data_ptr(); + params.numAcceptedIs64 = acc.scalar_type() == torch::kInt64; + } + } + + auto stream = at::cuda::getCurrentCUDAStream(topk.get_device()); + rope_kv_append::invokeTopkBridge(params, tc::getEnvEnablePDL(), stream); + return out; +} + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_rope_kv_append(Tensor fused_q, Tensor q_pe, Tensor latent_cache, Tensor rotary_cos_sin, " + "Tensor(a!) cu_q_seqlens, Tensor(b!) cu_kv_seqlens, Tensor(c!) fmha_scheduler_counter, " + "Tensor(d!) mla_bmm1_scale, Tensor(e!) mla_bmm2_scale, Tensor(f!) quant_q_buffer, Tensor sequence_length, " + "Tensor host_context_lengths, int num_contexts, Tensor kv_cache_block_offsets, " + "Tensor host_kv_cache_pool_pointers, Tensor host_kv_cache_pool_mapping, Tensor? kv_cache_scale_orig_quant, " + "int layer_idx, int num_heads, int tokens_per_block, int attention_window_size, int quant_mode, " + "float q_scaling, int qk_nope_head_dim, int qk_rope_head_dim, int kv_lora_rank, Tensor? idx_k_fp8, " + "Tensor? idx_k_scale, Tensor(g!)? idx_k_cache, Tensor? idx_slot_mapping_fp8, Tensor? idx_slot_mapping_scale, " + "int idx_num_tokens) -> ()"); + m.def( + "glm_moe_dsa_topk_index_bridge(Tensor topk, int num_rows, Tensor req_idx, int req_idx_bias, Tensor " + "block_table, " + "int block_size, int stride_factor, Tensor? layer_ids, int layer_id, Tensor(a!)? copy_dst, " + "int copy_row_start, int copy_rows, int copy_mode, int next_n, Tensor? num_accepted) -> Tensor"); +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_rope_kv_append", &tensorrt_llm::torch_ext::glm_moe_dsa::ropeKvAppend); + m.impl("glm_moe_dsa_topk_index_bridge", &tensorrt_llm::torch_ext::glm_moe_dsa::topkIndexBridge); +} diff --git a/cpp/tensorrt_llm/thop/glmMoeDsa/sparseMlaDecodeOp.cpp b/cpp/tensorrt_llm/thop/glmMoeDsa/sparseMlaDecodeOp.cpp new file mode 100644 index 000000000000..a7dba3610644 --- /dev/null +++ b/cpp/tensorrt_llm/thop/glmMoeDsa/sparseMlaDecodeOp.cpp @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "tensorrt_llm/kernels/glmMoeDsa/sparseMlaDecode.h" + +#include +#include +#include +#include + +#include + +namespace tk = tensorrt_llm::kernels::glm_moe_dsa::sparse_mla_decode; + +TRTLLM_NAMESPACE_BEGIN + +namespace torch_ext::glm_moe_dsa +{ + +namespace +{ + +bool isFp8ByteTensor(torch::Tensor const& t) +{ + return t.scalar_type() == torch::kUInt8 || t.scalar_type() == torch::kFloat8_e4m3fn; +} + +} // namespace + +// Sparse MLA generation attention; see sparseMlaDecode.h. +void sparseMlaDecode(torch::Tensor const& q, int64_t kvPoolPtr, torch::Tensor const& indices, + torch::Tensor const& bmm1Scale, torch::Tensor const& bmm2Scale, torch::Tensor const& seqLensKv, int64_t seqLenQ, + torch::Tensor& out) +{ + TORCH_CHECK(q.is_cuda() && indices.is_cuda() && bmm1Scale.is_cuda() && bmm2Scale.is_cuda() && out.is_cuda(), + "glm_moe_dsa_sparse_mla_decode expects CUDA tensors"); + TORCH_CHECK(isFp8ByteTensor(q), "q must hold e4m3 bytes (uint8 or float8_e4m3fn)"); + TORCH_CHECK(q.is_contiguous(), "q must be contiguous"); + int64_t const numRows = q.size(0); + TORCH_CHECK(q.numel() == numRows * tk::kNumHeads * tk::kHeadDimQk, "q must be [rows, ", tk::kNumHeads, ", ", + tk::kHeadDimQk, "]"); + TORCH_CHECK(indices.scalar_type() == torch::kInt32 && indices.dim() == 2, "indices must be a 2-D int32 tensor"); + TORCH_CHECK(indices.size(0) == numRows, "indices must have one row per query row"); + TORCH_CHECK(indices.size(1) == tk::kTopK, "indices must hold ", tk::kTopK, " entries per row"); + TORCH_CHECK(indices.stride(1) == 1, "indices rows must be contiguous"); + TORCH_CHECK(bmm1Scale.scalar_type() == torch::kFloat32 && bmm1Scale.numel() >= 2, "bmm1_scale must be float32[2]"); + TORCH_CHECK(bmm2Scale.scalar_type() == torch::kFloat32 && bmm2Scale.numel() >= 1, "bmm2_scale must be float32[1]"); + TORCH_CHECK(out.scalar_type() == torch::kBFloat16 && out.is_contiguous(), "out must be contiguous bfloat16"); + TORCH_CHECK(out.numel() == numRows * tk::kNumHeads * tk::kHeadDimV, "out must be [rows, ", + tk::kNumHeads * tk::kHeadDimV, "]"); + TORCH_CHECK(kvPoolPtr != 0, "kv_pool_ptr must be a valid device address"); + + tk::SparseMlaDecodeParams params{}; + params.q = q.data_ptr(); + params.kvPool = reinterpret_cast(kvPoolPtr); + params.indices = indices.data_ptr(); + params.indicesRowStride = indices.stride(0); + params.bmm1Scale = bmm1Scale.data_ptr(); + params.bmm2Scale = bmm2Scale.data_ptr(); + TORCH_CHECK(seqLensKv.is_cuda() && seqLensKv.scalar_type() == torch::kInt32 && seqLensKv.is_contiguous(), + "seq_lens_kv must be a contiguous int32 CUDA tensor"); + TORCH_CHECK(seqLenQ > 0 && numRows % seqLenQ == 0, "rows must be a multiple of seq_len_q"); + TORCH_CHECK(seqLensKv.numel() >= numRows / seqLenQ, "seq_lens_kv must cover every sequence"); + params.seqLensKv = seqLensKv.data_ptr(); + params.seqLenQ = static_cast(seqLenQ); + params.out = reinterpret_cast<__nv_bfloat16*>(out.data_ptr()); + params.numRows = static_cast(numRows); + + auto const stream = at::cuda::getCurrentCUDAStream(q.get_device()); + tk::invokeSparseMlaDecode(params, stream); +} + +#ifdef GLM_MOE_DSA_SPARSE_MLA_DECODE_TIMING +torch::Tensor sparseMlaDecodeDebugTimeline() +{ + auto out = torch::empty({tk::kTimelineMaxCtas, tk::kTimelineSlots}, torch::dtype(torch::kInt64)); + tk::copySparseMlaDecodeTimeline(reinterpret_cast(out.data_ptr())); + return out; +} +#endif + +bool sparseMlaDecodeSupported(int64_t device) +{ + return tk::isSparseMlaDecodeDeviceSupported(static_cast(device)); +} + +} // namespace torch_ext::glm_moe_dsa + +TRTLLM_NAMESPACE_END + +TORCH_LIBRARY_FRAGMENT(trtllm, m) +{ + m.def( + "glm_moe_dsa_sparse_mla_decode(Tensor q, int kv_pool_ptr, Tensor indices, Tensor bmm1_scale, " + "Tensor bmm2_scale, Tensor seq_lens_kv, int seq_len_q, Tensor(a!) out) -> ()"); + m.def("glm_moe_dsa_sparse_mla_decode_supported(int device) -> bool", + &tensorrt_llm::torch_ext::glm_moe_dsa::sparseMlaDecodeSupported); +#ifdef GLM_MOE_DSA_SPARSE_MLA_DECODE_TIMING + m.def("glm_moe_dsa_sparse_mla_decode_debug_timeline() -> Tensor", + &tensorrt_llm::torch_ext::glm_moe_dsa::sparseMlaDecodeDebugTimeline); +#endif +} + +TORCH_LIBRARY_IMPL(trtllm, CUDA, m) +{ + m.impl("glm_moe_dsa_sparse_mla_decode", &tensorrt_llm::torch_ext::glm_moe_dsa::sparseMlaDecode); +} From e10663b9b4b76ae002a42f6c944bcbc8ebde6849 Mon Sep 17 00:00:00 2001 From: Xiao Wang <24860335+xwang233@users.noreply.github.com> Date: Sat, 17 Oct 2026 09:10:38 -0700 Subject: [PATCH 03/19] [None][feat] modeling_v2: GLM-5.2 NVFP4 targets for SM103 Add the glm_moe_dsa family to modeling_v2 with three targets, each claiming its own parallel topology through the family's route(): - glm_5_2_nvfp4__sm_103__tp8: tensor-parallel attention and MoE on 8 GPUs; - glm_5_2_nvfp4__sm_103__tep: tensor-parallel attention with expert-parallel experts on 4 or 8 GPUs; - glm_5_2_nvfp4__sm_103__dep: attention data parallelism with expert-parallel experts on 8 or 16 GPUs. Code the targets share (kernel packages, dispatch, attention, the layer composition, the speculative worker and the runtime-hook wiring) lives in models/glm_moe_dsa/shared/. Each target selects the fast paths it serves through its own plan; the paths default on inside the target and can be turned off with TRTLLM_STAIRCASE_GLM_LL_DISABLE or per-path TRTLLM_STAIRCASE_GLM_LL_* variables. Any configuration that no target claims runs the built-in model. Signed-off-by: Xiao Wang <24860335+xwang233@users.noreply.github.com> --- .../_experimental/modeling_v2/__init__.py | 2 + .../modeling_v2/_router_index.py | 22 +- .../models/glm_moe_dsa/__init__.py | 3 + .../glm_5_2_nvfp4__sm_103__dep/__init__.py | 3 + .../glm_5_2_nvfp4__sm_103__dep/modeling.py | 443 ++++ .../glm_5_2_nvfp4__sm_103__dep/weights.py | 21 + .../glm_5_2_nvfp4__sm_103__tep/__init__.py | 3 + .../glm_5_2_nvfp4__sm_103__tep/modeling.py | 357 +++ .../glm_5_2_nvfp4__sm_103__tep/weights.py | 21 + .../glm_5_2_nvfp4__sm_103__tp8/__init__.py | 3 + .../kernels/__init__.py | 3 + .../cross_layer_expert_prefetch/__init__.py | 29 + .../cross_layer_expert_prefetch/kernels.py | 405 ++++ .../spec_experts.py | 508 +++++ .../kernels/draft_routed_moe/__init__.py | 30 + .../kernels/draft_routed_moe/routed_moe.py | 408 ++++ .../kernels/hop_routing/__init__.py | 46 + .../kernels/hop_routing/hop.py | 251 +++ .../kernels/hop_routing/routed_moe.py | 316 +++ .../kernels/routed_moe/__init__.py | 39 + .../kernels/routed_moe/kernel.py | 1987 +++++++++++++++++ .../kernels/routed_moe/kernel_xc.py | 1775 +++++++++++++++ .../kernels/routed_moe/ptx.py | 844 +++++++ .../kernels/routed_moe/routed_moe.py | 880 ++++++++ .../shared_expert_prefetch/__init__.py | 36 + .../shared_expert_prefetch/kernel_xc.py | 745 ++++++ .../shared_expert_prefetch/routed_moe.py | 308 +++ .../glm_5_2_nvfp4__sm_103__tp8/modeling.py | 318 +++ .../glm_5_2_nvfp4__sm_103__tp8/weights.py | 21 + .../modeling_v2/models/glm_moe_dsa/routing.py | 290 +++ .../models/glm_moe_dsa/shared/__init__.py | 4 + .../models/glm_moe_dsa/shared/attention.py | 569 +++++ .../glm_moe_dsa/shared/attention_forward.py | 744 ++++++ .../models/glm_moe_dsa/shared/dispatch.py | 1209 ++++++++++ .../glm_moe_dsa/shared/kernels/__init__.py | 3 + .../shared/kernels/absorbed_query/__init__.py | 48 + .../shared/kernels/absorbed_query/kernels.py | 1764 +++++++++++++++ .../kernels/absorbed_query/query_proj.py | 886 ++++++++ .../kernels/absorbed_query/shared_indexer.py | 1348 +++++++++++ .../absorbed_query/shared_indexer_cute.py | 945 ++++++++ .../kernels/attention_output_tail/__init__.py | 46 + .../attention_output_tail/cute_fused.py | 585 +++++ .../attention_output_tail/cute_oproj.py | 390 ++++ .../attention_output_tail/cute_oproj_ring.py | 461 ++++ .../kernels/attention_output_tail/into_hop.py | 566 +++++ .../kernels/attention_output_tail/kernels.py | 612 +++++ .../kernels/attention_output_tail/prefetch.py | 217 ++ .../kernels/attention_output_tail/tail.py | 628 ++++++ .../glm_moe_dsa/shared/kernels/contracts.py | 244 ++ .../cross_rank_topk_cluster_merge/__init__.py | 39 + .../cp_indexer_decode.py | 264 +++ .../cp_workspace.py | 247 ++ .../cross_rank_topk_global_emit/__init__.py | 60 + .../cross_rank_topk_global_emit/bridge.py | 286 +++ .../cp_indexer_decode.py | 332 +++ .../cp_workspace.py | 246 ++ .../kernels/decode_prologue/__init__.py | 35 + .../kernels/decode_prologue/prologue.py | 596 +++++ .../kernels/deferred_input_prep/__init__.py | 30 + .../kernels/deferred_input_prep/compat.py | 84 + .../kernels/deferred_input_prep/guard.py | 233 ++ .../kernels/deferred_input_prep/packet.py | 398 ++++ .../deferred_input_prep/paged_mqa_schedule.py | 70 + .../deferred_input_prep/sampler_store.py | 248 ++ .../kernels/deferred_input_prep/stager.py | 703 ++++++ .../shared/kernels/deferred_norm/__init__.py | 72 + .../shared/kernels/deferred_norm/hop.py | 666 ++++++ .../kernels/deferred_norm/hop_kernel.py | 758 +++++++ .../shared/kernels/deferred_norm/packet.py | 105 + .../kernels/deferred_norm/proj_kernel.py | 889 ++++++++ .../kernels/deferred_norm/projection.py | 305 +++ .../shared/kernels/deferred_norm/ptx.py | 619 +++++ .../kernels/deferred_norm/query_entry.py | 224 ++ .../shared/kernels/deferred_norm/scope.py | 134 ++ .../deferred_norm_evict_hints/__init__.py | 62 + .../deferred_norm_evict_hints/entry.py | 122 + .../kernels/deferred_norm_evict_hints/hop.py | 239 ++ .../deferred_norm_evict_hints/hop_kernel.py | 814 +++++++ .../deferred_norm_evict_hints/proj_kernel.py | 703 ++++++ .../deferred_norm_evict_hints/projection.py | 227 ++ .../kernels/deferred_norm_evict_hints/ptx.py | 166 ++ .../shared/kernels/dense_mlp/__init__.py | 26 + .../shared/kernels/dense_mlp/dense_mlp.py | 152 ++ .../shared/kernels/draft_front/__init__.py | 42 + .../shared/kernels/draft_front/draft_front.py | 223 ++ .../shared/kernels/draft_front/kernels.py | 445 ++++ .../shared/kernels/draft_front/native_norm.py | 137 ++ .../kernels/draft_live_rows/__init__.py | 50 + .../shared/kernels/draft_live_rows/hop.py | 277 +++ .../shared/kernels/draft_live_rows/step0.py | 103 + .../shared/kernels/draft_loop/__init__.py | 46 + .../shared/kernels/draft_loop/draft_loop.py | 450 ++++ .../shared/kernels/draft_loop/draft_step.py | 497 +++++ .../shared/kernels/draft_loop/embedding.py | 175 ++ .../shared/kernels/draft_loop/verify.py | 245 ++ .../shared/kernels/draft_tail/__init__.py | 31 + .../shared/kernels/draft_tail/draft_tail.py | 564 +++++ .../fused_kv_indexer_projection/__init__.py | 38 + .../fused_kv_indexer_projection/entry.py | 208 ++ .../proj_kernel.py | 906 ++++++++ .../fused_kv_indexer_projection/projection.py | 316 +++ .../fused_kv_indexer_projection/ptx.py | 475 ++++ .../kernels/host_page_retention/__init__.py | 31 + .../host_page_retention/host_policy.py | 288 +++ .../kernels/indexer_layer_front/__init__.py | 55 + .../indexer_layer_front/cp_indexer_decode.py | 248 ++ .../indexer_layer_front/cp_logits_runner.py | 217 ++ .../fp8_paged_mqa_logits_cp.py | 1844 +++++++++++++++ .../kernels/indexer_layer_front/kernels.py | 732 ++++++ .../kernels/indexer_layer_front/query_proj.py | 403 ++++ .../kernels/indexer_layer_front/rope.py | 110 + .../shared/kernels/indexer_topk/__init__.py | 45 + .../indexer_topk/cross_rank_indexer_decode.py | 266 +++ .../indexer_topk/cross_rank_logits_runner.py | 219 ++ .../kernels/indexer_topk/cross_rank_policy.py | 143 ++ .../indexer_topk/cross_rank_workspace.py | 212 ++ .../fp8_paged_mqa_logits_cross_rank.py | 1806 +++++++++++++++ .../indexer_topk/fp8_paged_mqa_logits_rows.py | 1747 +++++++++++++++ .../fp8_paged_mqa_logits_rowsplit.py | 773 +++++++ .../kernels/indexer_topk/indexer_decode.py | 296 +++ .../kernels/indexer_topk/logits_runner.py | 237 ++ .../shared/kernels/indexer_topk/policy.py | 240 ++ .../shared/kernels/indexer_topk/region.py | 99 + .../kernels/indexer_topk/topk_select.py | 475 ++++ .../kernels/oproj_weight_staging/__init__.py | 48 + .../oproj_weight_staging/cute_oproj_stage.py | 841 +++++++ .../kernels/oproj_weight_staging/kernels.py | 389 ++++ .../kernels/oproj_weight_staging/tail.py | 208 ++ .../kernels/post_attention_hop/__init__.py | 74 + .../post_attention_hop/from_partials.py | 207 ++ .../shared/kernels/post_attention_hop/hop.py | 430 ++++ .../shared/kernels/post_moe_hop/__init__.py | 45 + .../kernels/post_moe_hop/cute_kernel.py | 1078 +++++++++ .../shared/kernels/post_moe_hop/health.py | 119 + .../shared/kernels/post_moe_hop/hop.py | 785 +++++++ .../shared/kernels/post_moe_hop/kernels.py | 446 ++++ .../shared/kernels/post_moe_hop/workspace.py | 200 ++ .../shared/kernels/rope_kv_append/__init__.py | 70 + .../shared/kernels/rope_kv_append/ops.py | 517 +++++ .../shared/kernels/router_gemm/__init__.py | 40 + .../shared/kernels/router_gemm/hop.py | 146 ++ .../kernels/router_gemm/router_kernel.py | 571 +++++ .../kernels/sparse_mla_decode/__init__.py | 31 + .../kernels/sparse_mla_decode/sparse_mla.py | 335 +++ .../models/glm_moe_dsa/shared/layers.py | 1189 ++++++++++ .../models/glm_moe_dsa/shared/runtime.py | 103 + .../models/glm_moe_dsa/shared/spec_worker.py | 621 ++++++ 147 files changed, 55745 insertions(+), 2 deletions(-) create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/modeling.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/weights.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/modeling.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/weights.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/kernels.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/spec_experts.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/draft_routed_moe/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/draft_routed_moe/routed_moe.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/hop.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/routed_moe.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/kernel.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/kernel_xc.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/ptx.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/routed_moe.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/kernel_xc.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/routed_moe.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/modeling.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/weights.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/routing.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/attention.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/attention_forward.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/dispatch.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/kernels.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/query_proj.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/shared_indexer.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/shared_indexer_cute.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/attention_output_tail/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/attention_output_tail/cute_fused.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/attention_output_tail/cute_oproj.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/attention_output_tail/cute_oproj_ring.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/attention_output_tail/into_hop.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/attention_output_tail/kernels.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/attention_output_tail/prefetch.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/attention_output_tail/tail.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/contracts.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/cross_rank_topk_cluster_merge/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/cross_rank_topk_cluster_merge/cp_indexer_decode.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/cross_rank_topk_cluster_merge/cp_workspace.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/cross_rank_topk_global_emit/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/cross_rank_topk_global_emit/bridge.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/cross_rank_topk_global_emit/cp_indexer_decode.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/cross_rank_topk_global_emit/cp_workspace.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/decode_prologue/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/decode_prologue/prologue.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_input_prep/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_input_prep/compat.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_input_prep/guard.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_input_prep/packet.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_input_prep/paged_mqa_schedule.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_input_prep/sampler_store.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_input_prep/stager.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm/hop.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm/hop_kernel.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm/packet.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm/proj_kernel.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm/projection.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm/ptx.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm/query_entry.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm/scope.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm_evict_hints/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm_evict_hints/entry.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm_evict_hints/hop.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm_evict_hints/hop_kernel.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm_evict_hints/proj_kernel.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm_evict_hints/projection.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/deferred_norm_evict_hints/ptx.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/dense_mlp/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/dense_mlp/dense_mlp.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_front/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_front/draft_front.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_front/kernels.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_front/native_norm.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_live_rows/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_live_rows/hop.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_live_rows/step0.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_loop/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_loop/draft_loop.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_loop/draft_step.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_loop/embedding.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_loop/verify.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_tail/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/draft_tail/draft_tail.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/fused_kv_indexer_projection/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/fused_kv_indexer_projection/entry.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/fused_kv_indexer_projection/proj_kernel.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/fused_kv_indexer_projection/projection.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/fused_kv_indexer_projection/ptx.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/host_page_retention/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/host_page_retention/host_policy.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_layer_front/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_layer_front/cp_indexer_decode.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_layer_front/cp_logits_runner.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_layer_front/fp8_paged_mqa_logits_cp.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_layer_front/kernels.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_layer_front/query_proj.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_layer_front/rope.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/cross_rank_indexer_decode.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/cross_rank_logits_runner.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/cross_rank_policy.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/cross_rank_workspace.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/fp8_paged_mqa_logits_cross_rank.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/fp8_paged_mqa_logits_rows.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/fp8_paged_mqa_logits_rowsplit.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/indexer_decode.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/logits_runner.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/policy.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/region.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/indexer_topk/topk_select.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/oproj_weight_staging/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/oproj_weight_staging/cute_oproj_stage.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/oproj_weight_staging/kernels.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/oproj_weight_staging/tail.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/post_attention_hop/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/post_attention_hop/from_partials.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/post_attention_hop/hop.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/post_moe_hop/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/post_moe_hop/cute_kernel.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/post_moe_hop/health.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/post_moe_hop/hop.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/post_moe_hop/kernels.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/post_moe_hop/workspace.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/rope_kv_append/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/rope_kv_append/ops.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/router_gemm/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/router_gemm/hop.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/router_gemm/router_kernel.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/sparse_mla_decode/__init__.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/sparse_mla_decode/sparse_mla.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/layers.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/runtime.py create mode 100644 tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/spec_worker.py diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/__init__.py index c5140e7c912b..7dc4b711fd91 100644 --- a/tensorrt_llm/_torch/_experimental/modeling_v2/__init__.py +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/__init__.py @@ -32,6 +32,7 @@ ModelingV2Mode, NoTarget, modeling_v2_resolve, + resolved_mode, step_contract_enabled, ) @@ -41,5 +42,6 @@ "ModelingV2Mode", "NoTarget", "modeling_v2_resolve", + "resolved_mode", "step_contract_enabled", ] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/_router_index.py b/tensorrt_llm/_torch/_experimental/modeling_v2/_router_index.py index b0b6e78ef8d8..4d6bb76f1ef4 100644 --- a/tensorrt_llm/_torch/_experimental/modeling_v2/_router_index.py +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/_router_index.py @@ -37,7 +37,7 @@ import enum import os from importlib import import_module -from typing import TYPE_CHECKING, Optional, Tuple +from typing import TYPE_CHECKING, Dict, Optional, Tuple from tensorrt_llm.logger import logger @@ -51,6 +51,7 @@ MODELING_V2_ROUTERS = { "GptOssForCausalLM": "models.gpt_oss.routing", "DeepseekV3ForCausalLM": "models.deepseek_v3.routing", + "GlmMoeDsaForCausalLM": "models.glm_moe_dsa.routing", # Qwen3.8-Flash-Next publishes ``Qwen4ExpForConditionalGeneration``, and the # config loader's composite branch writes the same name back. "Qwen4ExpForConditionalGeneration": "models.qwen4_exp.routing", @@ -104,6 +105,20 @@ class NoTarget(Exception): """ +# The mode each target class was last resolved under (the loader builds the class right after resolving it). +_RESOLVED_MODES: Dict[type, ModelingV2Mode] = {} + + +def resolved_mode(target: type) -> ModelingV2Mode: + """The mode ``modeling_v2_resolve`` returned ``target`` under; ``auto`` for a class built without it. + + A target reads it at construction: under ``require`` its in-target dispatch + raises where it would otherwise fall back to a built-in path inside the + shapes it claims. + """ + return _RESOLVED_MODES.get(target, ModelingV2Mode.AUTO) + + def routing_module(arch: str): """Import and return the routing module for ``arch``, or None.""" name = MODELING_V2_ROUTERS.get(arch) @@ -146,7 +161,7 @@ def modeling_v2_resolve(config: "ModelConfig", llm_args: "TorchLlmArgs") -> Opti raise NoTarget( f"no routing module for this architecture; routed architectures: {known}" ) - return routing.route( + target = routing.route( pretrained_config, config.mapping, _device_compute_capability(), config.spec_config ) except NoTarget as why: @@ -156,3 +171,6 @@ def modeling_v2_resolve(config: "ModelConfig", llm_args: "TorchLlmArgs") -> Opti f"modeling_v2: no target fits {arch!r} ({why}); using the built-in implementation" ) return None + if target is not None: + _RESOLVED_MODES[target] = mode + return target diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/__init__.py new file mode 100644 index 000000000000..638d9884388f --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""GlmMoeDsaForCausalLM targets.""" diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/__init__.py new file mode 100644 index 000000000000..dd69779ecd5e --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""GLM-5.2-NVFP4 / sm_103 / dep.""" diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/modeling.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/modeling.py new file mode 100644 index 000000000000..5b2625da55cb --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/modeling.py @@ -0,0 +1,443 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""ModelingV2 target: glm-5.2-nvfp4 / sm_103 / dep. + +GLM-5.2, modelopt NVFP4 export: 78 layers, hidden 6144, 64 query heads, MLA with +DSA sparse attention (index top-k 2048), layers 0-2 dense and 3-77 MoE (256 +routed experts at top-8 plus a shared expert). Layer 78 is the checkpoint's bf16 +MTP module, driven by the one-model MTP-Eagle loop. `dep` is attention data +parallelism over eight or sixteen ranks (each rank runs the whole attention on its +own requests) and the experts expert-parallel over the same ranks. + +``route`` claims three deployments by the facts it reads: the rank count, whether +the LM head is tensor-parallel in attention DP, and the draft tokens per request. +Two more facts fix what the fast paths can serve there, the KV-cache dtype and the +MoE backend, and only the model config carries them, so this target checks them +first, at construction (``CELLS``, ``target_paths``). Under ``require`` a +deployment outside the cells raises; under ``auto`` it serves no fast path and +runs the built-in forward. + +Under attention DP the fast paths that take this target's shapes are rank-local: +the attention's rope, KV append and top-k bridge with an FP8 KV cache +(``rope_kv_append``), the rope of the full-indexer layers' query front +(``indexer_layer_front``), the draft loop (``draft_loop``), the decode step's +prologue (``decode_prologue``) and the host page retention policy +(``host_page_retention``). The plan of a cell serves those and the bases they +extend; the bases decline every call under attention DP. The other fast paths of +the family take tensor-parallel attention, so this target serves none of them: its +decoder layers, MoE modules and draft layer stay the built-in classes, which run +the built-in model's attention-DP communication on every step. + +The shell is the built-in ``GlmMoeDsaForCausalLM``: the same module tree, the +same weights and the same engine preferences (KV cache manager, KV transceiver, +sparse-attention defaults), so a built-in context server and this target's +generation server exchange the same KV and indexer-K layout. Construction runs, +in order: + +1. the cell check (``target_paths``), from the model config alone, so every rank + decides alike before any collective; +2. the rank-uniform check of the knobs (``dispatch.check_rank_uniform``), before + every collective the construction below makes; +3. the dispatch plan (``dispatch.resolve``) of the paths the cell serves, before + the built-in model is built, so a knob value no rank can parse fails early; +4. the plan is installed (``dispatch.install``): from here on the fast paths read + it; +5. the built-in model, then the core: its text model adopted as + ``GlmMoeDsaDepModelingV2``, whose forward hands each step to + ``PrefillTarget`` or ``DecodeTarget``; +6. ``adopt_model``, which re-classes the attention modules of every layer + (``shared.attention``) when the plan runs an attention path, and the + speculative worker (``shared.spec_worker``) when it runs the draft loop; +7. the rank-uniform check of the attached fast-path state + (``dispatch.check_rank_uniform_adopted``, ``shared.layers.adopted_state``). + +The step-loop hooks the target declares to the engine (``runtime_hooks``, +``shared.runtime.make_runtime_hooks``) are built later, on the engine's first +read, after the model is loaded. + +The rank-uniform check of step 2 also covers the shared switches the kernels +read and the modeling_v2 mode (``dispatch.rank_uniform_inputs``). + +Every step runs under the phase of the target that takes it +(``dispatch.step``): the shell's forward enters it around the built-in forward, +so the model, the speculative worker and its draft loop all run in it. A +prefill step runs the built-in decoder forward and the built-in draft loop; a +decode step runs ``DecodeTarget``'s loop over the built-in layers, the attention +modules' decode composition and the worker's draft loop. Each rank picks its +phase from its own requests; the layers' communication is the built-in one in +both phases. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Mapping, Optional, Tuple + +import torch + +from tensorrt_llm._torch._experimental.modeling_v2._router_index import ( + ModelingV2Mode, + resolved_mode, +) +from tensorrt_llm._torch.attention.attention import maybe_allgather_for_helix_cp +from tensorrt_llm._torch.attention.backends import AttentionMetadata +from tensorrt_llm._torch.model_config import ModelConfig +from tensorrt_llm._torch.models.modeling_deepseekv3 import DeepseekV3Model, GlmMoeDsaForCausalLM +from tensorrt_llm._torch.pyexecutor.runtime_hooks import RuntimeHooks +from tensorrt_llm._torch.speculative import SpecMetadata +from tensorrt_llm.logger import logger +from tensorrt_llm.quantization.mode import QuantAlgo + +from ..shared import dispatch, runtime +from ..shared.attention import GlmMLA +from ..shared.layers import adopted_state +from ..shared.spec_worker import GlmMtpWorker +from . import weights as _weights + +if TYPE_CHECKING: + from ..shared.kernels.decode_prologue import DecodePrologueHook + + +# The paths that engage under attention DP with an FP8 KV cache: the attention's rope, KV append +# and top-k bridge, the full-indexer layers' query-front rope, the draft loop, the decode +# prologue and the host page retention policy. +_FP8_KV_ENGAGED = frozenset( + { + "rope_kv_append", + "indexer_layer_front", + "draft_loop", + "decode_prologue", + "host_page_retention", + } +) +# The bases these paths extend. Under attention DP their kernels decline every call: the query +# fronts on every layer, the top-k selection on every step, and the post-attention hop is never +# built (the built-in layers run no post-attention all-reduce under attention DP). +_FP8_KV_BASES = frozenset( + { + "absorbed_query", + "draft_absorbed_query", + "indexer_topk", + "cross_rank_topk", + "post_attention_hop", + } +) +# With an NVFP4 KV cache the attention paths are outside the shapes their kernels take; the +# draft loop, the decode prologue and the host page retention policy engage. +_NVFP4_KV_ENGAGED = frozenset({"draft_loop", "decode_prologue", "host_page_retention"}) +_NVFP4_KV_BASES = frozenset({"post_attention_hop"}) +_NO_LAYERS: frozenset = frozenset() + +#: What this target serves with an FP8 KV cache. The query fronts decline attention DP on every +#: layer, which is outside their shapes here. +FP8_KV_PATHS = dispatch.TargetPaths( + served=_FP8_KV_ENGAGED | _FP8_KV_BASES, + layer_cells={ + **dispatch.LAYER_CELLS, + "absorbed_query": {"layer": _NO_LAYERS}, + "draft_absorbed_query": {"layer": _NO_LAYERS}, + "indexer_layer_front": {"target_layer": _NO_LAYERS, "draft_layer": _NO_LAYERS}, + }, +) +#: What this target serves with an NVFP4 KV cache. +NVFP4_KV_PATHS = dispatch.TargetPaths(served=_NVFP4_KV_ENGAGED | _NVFP4_KV_BASES) +#: What a deployment outside the cells serves under ``auto``: no fast path. +NO_PATHS = dispatch.TargetPaths(served=frozenset()) + + +@dataclass(frozen=True) +class Cell: + """One deployment this target serves: the facts ``route`` does not read, and the plan.""" + + kv_cache: QuantAlgo + moe_backend: str + paths: dispatch.TargetPaths + + +#: The deployments this target serves, per (ranks, LM head tensor-parallel in attention DP, +#: draft tokens per request), the facts ``route`` claims them by. +CELLS: Mapping[Tuple[int, bool, int], Cell] = { + (16, False, 3): Cell(QuantAlgo.FP8, "TRTLLM", FP8_KV_PATHS), + (8, False, 3): Cell(QuantAlgo.FP8, "CUTEDSL", FP8_KV_PATHS), + (8, True, 5): Cell(QuantAlgo.NVFP4, "CUTEDSL", NVFP4_KV_PATHS), +} + + +def target_paths(model_config: ModelConfig, mode: ModelingV2Mode) -> dispatch.TargetPaths: + """What this target serves on ``model_config``'s deployment: its cell's paths, or none. + + Reads only the model config, so every rank decides alike. + + Raises: + ValueError: under ``require``, if the deployment is not one of ``CELLS`` (its KV-cache + dtype or MoE backend is not its cell's). + """ + mapping = model_config.mapping + spec_config = model_config.spec_config + key = ( + mapping.world_size, + bool(mapping.enable_lm_head_tp_in_adp), + getattr(spec_config, "max_draft_len", None), + ) + kv_cache = model_config.quant_config.kv_cache_quant_algo + cell = CELLS.get(key) + if cell is not None and (kv_cache, model_config.moe_backend) == ( + cell.kv_cache, + cell.moe_backend, + ): + return cell.paths + served = "; ".join( + f"{ranks} ranks, LM head tensor-parallel in attention DP {lm_head_tp}, {draft} draft " + f"tokens: KV cache {cell.kv_cache.value}, MoE backend {cell.moe_backend}" + for (ranks, lm_head_tp, draft), cell in CELLS.items() + ) + words = ( + f"{key[0]} ranks, LM head tensor-parallel in attention DP {key[1]}, {key[2]} draft " + f"tokens, KV cache {kv_cache}, MoE backend {model_config.moe_backend} is not a " + f"deployment this target serves ({served})" + ) + if mode is ModelingV2Mode.REQUIRE: + raise ValueError(f"modeling_v2='require': {words}") + logger.warning(f"[glm_moe_dsa] dispatch: {words}; no fast path runs") + return NO_PATHS + + +def adopt_model(model: "ModelingV2Glm52Nvfp4Sm103Dep", plan: dispatch.Plan) -> None: + """Re-class the attention modules of every layer and the speculative worker, as far as + ``plan`` runs their fast paths. + + The attention modules of the decoder layers and of the draft layer, in index order, when + the plan runs the rope and KV append (the attention paths that engage under attention DP + extend it); then the speculative worker, which the built-in model builds after the draft + layer, when the plan runs the draft loop. The layers, their MoE modules and the draft + layer stay the built-in classes. The fast paths read the installed plan, which is ``plan``. + + Raises: + TypeError: if an attention module or the speculative worker is not exactly the + built-in class. + """ + if dispatch.is_enabled("rope_kv_append"): + for layer in model.model.layers: + GlmMLA.adopt(layer.self_attn) + if model.spec_worker is not None and dispatch.is_enabled("draft_loop"): + GlmMtpWorker.adopt(model.spec_worker) + + +class GlmMoeDsaDepModelingV2(DeepseekV3Model): + """The text model: the built-in ``DeepseekV3Model``, whose forward picks a target per step. + + Never constructed directly: ``adopt`` re-classes the built-in model the shell + has already built, so the module tree, the parameters and their names stay + the built-in model's. The class adds methods and plain attributes only. + """ + + _prefill: "PrefillTarget" + _decode: "DecodeTarget" + # The decode prologue the step loop runs, set when the target's runtime hooks are built; + # the decode target takes the embedding rows of its launch. + decode_prologue: Optional["DecodePrologueHook"] = None + + @classmethod + def adopt(cls, model: DeepseekV3Model) -> "GlmMoeDsaDepModelingV2": + """Re-class the built-in text model as this core and build its two targets. + + Raises: + TypeError: if ``model`` is not exactly the built-in ``DeepseekV3Model``. + """ + if type(model) is not DeepseekV3Model: + raise TypeError( + f"{cls.__name__} adopts the built-in DeepseekV3Model, got {type(model).__name__}" + ) + model.__class__ = cls + model._prefill = PrefillTarget(model) + model._decode = DecodeTarget(model) + return model + + def _select_target(self, attn_metadata) -> "PrefillTarget | DecodeTarget": + """Which target runs this step. + + One host-side integer read and nothing else: a decode-only CUDA graph is a + per-capture constant environment. A mixed batch -- context and generation + rows in one step -- goes to prefill, so prefill is the general case and + decode the specialization that may assume it has no context rows. + """ + return self._decode if attn_metadata.num_contexts == 0 else self._prefill + + def forward( + self, + attn_metadata, + input_ids: torch.IntTensor | None = None, + position_ids: torch.IntTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + spec_metadata=None, + **kwargs, + ) -> torch.Tensor: + return self._select_target(attn_metadata).forward( + attn_metadata, + input_ids=input_ids, + position_ids=position_ids, + inputs_embeds=inputs_embeds, + spec_metadata=spec_metadata, + **kwargs, + ) + + +class PrefillTarget: + """The general case: context rows, and possibly generation rows beside them. + + Runs the built-in decoder forward. + """ + + phase = dispatch.Phase.PREFILL + + def __init__(self, core: GlmMoeDsaDepModelingV2) -> None: + self.core = core + + def forward( + self, + attn_metadata, + input_ids: torch.IntTensor | None = None, + position_ids: torch.IntTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + spec_metadata=None, + **kwargs, + ) -> torch.Tensor: + return DeepseekV3Model.forward( + self.core, + attn_metadata, + input_ids=input_ids, + position_ids=position_ids, + inputs_embeds=inputs_embeds, + spec_metadata=spec_metadata, + **kwargs, + ) + + +class DecodeTarget: + """The specialization: routed to only when there are no context rows. + + Runs this target's decoder loop. + """ + + phase = dispatch.Phase.DECODE + + def __init__(self, core: GlmMoeDsaDepModelingV2) -> None: + self.core = core + + def forward( + self, + attn_metadata: AttentionMetadata, + input_ids: Optional[torch.IntTensor] = None, + position_ids: Optional[torch.IntTensor] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + spec_metadata: Optional[SpecMetadata] = None, + **kwargs, + ) -> torch.Tensor: + """The decoder layers of the text model on a decode step. + + The built-in loop with the decode prologue's embedding. Each layer is the built-in + decoder layer, called as a module, so its forward hooks run; its attention runs the + attention modules' decode composition under the step's phase. The draft layer is not + run here; the last decoder layer applies the final norm. + """ + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError( + "You cannot specify both input_ids and inputs_embeds at the same time, and must specify either one" + ) + + prologue = self.core.decode_prologue + if inputs_embeds is None and prologue is not None: + # The rows the decode prologue launch embedded (None unless that launch ran for + # exactly these ids and this table). + inputs_embeds = prologue.take_embedding( + attn_metadata, input_ids, self.core.embed_tokens + ) + if inputs_embeds is None: + inputs_embeds = self.core.embed_tokens(input_ids) + + hidden_states = inputs_embeds + residual = None + + for idx, decoder_layer in enumerate(self.core.layers[: self.core.num_hidden_layers]): + hidden_states, residual = decoder_layer( + position_ids=position_ids, + hidden_states=hidden_states, + attn_metadata=attn_metadata, + residual=residual, + spec_metadata=spec_metadata, + ) + + hidden_states = maybe_allgather_for_helix_cp( + hidden_states, attn_metadata, self.core.mapping_with_cp + ) + return hidden_states + + +class ModelingV2Glm52Nvfp4Sm103Dep(GlmMoeDsaForCausalLM): + """GLM-5.2-NVFP4 on sm_103 at dep: the built-in causal LM with this target's core and dispatch.""" + + def __init__(self, model_config: ModelConfig): + world_size = model_config.mapping.world_size + mode = resolved_mode(type(self)) + paths = target_paths(model_config, mode) + knobs = dispatch.read_knobs() + dispatch.check_rank_uniform(dispatch.rank_uniform_inputs(knobs, mode), world_size) + plan = dispatch.resolve(mode, dict(knobs), paths) + dispatch.install(plan) + super().__init__(model_config) + GlmMoeDsaDepModelingV2.adopt(self.model) + self.plan = plan + adopt_model(self, self.plan) + dispatch.check_rank_uniform_adopted(adopted_state(self), world_size) + + @property + def runtime_hooks(self) -> RuntimeHooks: + """The step-loop hooks this target declares, built on the engine's first read. + + Built after the model is loaded: a hook may hold pinned host memory, which model + construction (under meta-device initialization) must not allocate. + + Raises: + RuntimeError: if building the hooks raises ``AttributeError``, which + ``runtime_hooks_of`` would otherwise read as "no hooks declared". + """ + hooks = self.__dict__.get("_runtime_hooks") + if hooks is None: + try: + hooks = runtime.make_runtime_hooks(self) + except AttributeError as exc: + raise RuntimeError( + f"building the step-loop hooks of {type(self).__name__} failed: {exc}" + ) from exc + self._runtime_hooks = hooks + return hooks + + def forward( + self, + attn_metadata, + input_ids: torch.IntTensor | None = None, + position_ids: torch.IntTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + return_context_logits: bool = False, + spec_metadata=None, + resource_manager=None, + **kwargs, + ): + """The built-in forward under the phase of the target that takes this step. + + The scope covers the text model, the speculative worker and its draft loop. + """ + with dispatch.step(self.model._select_target(attn_metadata).phase): + return super().forward( + attn_metadata, + input_ids=input_ids, + position_ids=position_ids, + inputs_embeds=inputs_embeds, + return_context_logits=return_context_logits, + spec_metadata=spec_metadata, + resource_manager=resource_manager, + **kwargs, + ) + + def load_weights(self, weights): + _weights.load(self, weights) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/weights.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/weights.py new file mode 100644 index 000000000000..b9046fba42dd --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__dep/weights.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""The weights this target expects: the GLM-5.2 NVFP4 checkpoint as the built-in model loads it. + +The target's module tree is the built-in model's, so the built-in loader maps +every checkpoint tensor, including the bf16 MTP layer and the NVFP4 scales. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from tensorrt_llm._torch.models.modeling_deepseekv3 import GlmMoeDsaForCausalLM + +if TYPE_CHECKING: + from .modeling import ModelingV2Glm52Nvfp4Sm103Dep + + +def load(model: "ModelingV2Glm52Nvfp4Sm103Dep", weights: Any) -> None: + """Load the checkpoint ``weights`` into ``model`` with the built-in loader.""" + GlmMoeDsaForCausalLM.load_weights(model, weights) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/__init__.py new file mode 100644 index 000000000000..bfe54ffe7404 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""GLM-5.2-NVFP4 / sm_103 / tep.""" diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/modeling.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/modeling.py new file mode 100644 index 000000000000..d4c83cbaec3c --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/modeling.py @@ -0,0 +1,357 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""ModelingV2 target: glm-5.2-nvfp4 / sm_103 / tep. + +GLM-5.2, modelopt NVFP4 export: 78 layers, hidden 6144, 64 query heads, MLA with +DSA sparse attention (index top-k 2048), layers 0-2 dense and 3-77 MoE (256 +routed experts at top-8 plus a shared expert). Layer 78 is the checkpoint's bf16 +MTP module, driven by the one-model MTP-Eagle loop at five draft tokens. `tep` +is the attention heads tensor-parallel over four or eight ranks and the experts +expert-parallel over the same ranks: each rank holds whole experts and runs the +built-in expert-parallel MoE on them. + +The fast paths this target serves depend on its tensor-parallel size +(``TARGET_PATHS``): at eight ranks the attention runs at the same shapes as on +the tp8 target, so it serves every fast path of the family except the routed-MoE +ones, which take only tensor-parallel experts; at four ranks it serves the paths +whose kernels take four ranks (the indexer, the top-k selection, the router GEMM, +the dense MLP, the draft loop and the step-loop paths) and the bases they extend. +The layers run the family's composition (``shared.layers``), in which the block of +a path this target does not serve never runs. + +The shell is the built-in ``GlmMoeDsaForCausalLM``: the same module tree, the +same weights and the same engine preferences (KV cache manager, KV transceiver, +sparse-attention defaults), so a built-in context server and this target's +generation server exchange the same KV and indexer-K layout. Construction runs, +in order: + +1. the rank-uniform check of the knobs (``dispatch.check_rank_uniform``), first, + so it runs before every collective the construction below makes; +2. the dispatch plan (``dispatch.resolve``) of the paths this target serves at its + tensor-parallel size, before the built-in model is built, so a knob value no + rank can parse fails early; +3. the plan is installed (``dispatch.install``): from here on the fast paths read + it; +4. the built-in model, then the core: its text model adopted as + ``GlmMoeDsaTepModelingV2``, whose forward hands each step to + ``PrefillTarget`` or ``DecodeTarget``; +5. ``adopt_model`` (``shared.layers``), which re-classes the decoder layers, + the draft layer, their attention modules (``shared.attention``) and their MoE + modules as the family's layer classes and attaches the fast-path objects the + plan enables to them, then re-classes the speculative worker + (``shared.spec_worker``). The attention backends build their FMHA libraries + after construction, in ``create_weights``, each with the libraries adoption + declared on it first; +6. the rank-uniform check of that state (``dispatch.check_rank_uniform_adopted``). + +The step-loop hooks the target declares to the engine (``runtime_hooks``, +``shared.runtime.make_runtime_hooks``) are built later, on the engine's first +read, after the model is loaded. + +The rank-uniform check of step 1 also covers the shared switches the kernels +read and the modeling_v2 mode (``dispatch.rank_uniform_inputs``). + +Every step runs under the phase of the target that takes it +(``dispatch.step``): the shell's forward enters it around the built-in forward, +so the model, the speculative worker and its draft loop all run in it. A +prefill step runs the built-in decoder forward, the built-in layers and the +built-in draft loop; a decode step runs ``DecodeTarget``'s loop, the layers' +decode composition and the worker's draft loop. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +import torch + +from tensorrt_llm._torch._experimental.modeling_v2._router_index import resolved_mode +from tensorrt_llm._torch.attention.attention import maybe_allgather_for_helix_cp +from tensorrt_llm._torch.attention.backends import AttentionMetadata +from tensorrt_llm._torch.model_config import ModelConfig +from tensorrt_llm._torch.models.modeling_deepseekv3 import DeepseekV3Model, GlmMoeDsaForCausalLM +from tensorrt_llm._torch.pyexecutor.runtime_hooks import RuntimeHooks +from tensorrt_llm._torch.speculative import SpecMetadata + +from ..shared import dispatch, runtime +from ..shared.layers import adopt_model, adopted_state +from . import weights as _weights + +if TYPE_CHECKING: + from ..shared.kernels.decode_prologue import DecodePrologueHook + + +# The routed-MoE paths take tensor-parallel experts (every expert on every rank); with the experts +# expert-parallel their kernels decline every call, so this target serves none of them. +_ROUTED_MOE_PATHS = frozenset( + { + "routed_moe", + "routed_moe_into_hop", + "hop_routing", + "shared_expert_prefetch", + "draft_routed_moe", + "cross_layer_expert_prefetch", + } +) +# At four ranks the attention operands, the attention output and the post-MoE hop are outside +# the kernels' shapes (sixteen local heads, a four-rank output projection, a single-node group); +# these paths take four ranks. +_TEP4_PATHS = frozenset( + { + "indexer_layer_front", + "rope_kv_append", + "indexer_topk", + "cross_rank_topk", + "cross_rank_topk_cluster_merge", + "cross_rank_topk_global_emit", + "router_gemm", + "dense_mlp", + "draft_front", + "draft_tail", + "draft_loop", + "decode_prologue", + "deferred_input_prep", + "sampler_store", + "execution_stream_loop", + "host_page_retention", + } +) +# The bases these paths extend at four ranks. Their kernels decline every layer there: the query +# fronts on their layer cells, the post-attention hop on its native all-reduce, which a +# single-node group runs without multi-node NVLink. +_TEP4_BASES = frozenset({"absorbed_query", "draft_absorbed_query", "post_attention_hop"}) +_NO_LAYERS: frozenset = frozenset() + +#: What this target serves at each tensor-parallel size. +TARGET_PATHS = { + 4: dispatch.TargetPaths( + served=_TEP4_PATHS | _TEP4_BASES, + layer_cells={ + **dispatch.LAYER_CELLS, + "absorbed_query": {"layer": _NO_LAYERS}, + "draft_absorbed_query": {"layer": _NO_LAYERS}, + "indexer_layer_front": {"target_layer": _NO_LAYERS, "draft_layer": _NO_LAYERS}, + }, + uncovered={"post_attention_hop": ("does not use MNNVL",)}, + ), + 8: dispatch.TargetPaths(served=frozenset(dispatch.FAST_PATH_NAMES) - _ROUTED_MOE_PATHS), +} + + +class GlmMoeDsaTepModelingV2(DeepseekV3Model): + """The text model: the built-in ``DeepseekV3Model``, whose forward picks a target per step. + + Never constructed directly: ``adopt`` re-classes the built-in model the shell + has already built, so the module tree, the parameters and their names stay + the built-in model's. The class adds methods and plain attributes only. + """ + + _prefill: "PrefillTarget" + _decode: "DecodeTarget" + # The decode prologue the step loop runs, set when the target's runtime hooks are built; + # the decode target takes the embedding rows of its launch. + decode_prologue: Optional["DecodePrologueHook"] = None + + @classmethod + def adopt(cls, model: DeepseekV3Model) -> "GlmMoeDsaTepModelingV2": + """Re-class the built-in text model as this core and build its two targets. + + Raises: + TypeError: if ``model`` is not exactly the built-in ``DeepseekV3Model``. + """ + if type(model) is not DeepseekV3Model: + raise TypeError( + f"{cls.__name__} adopts the built-in DeepseekV3Model, got {type(model).__name__}" + ) + model.__class__ = cls + model._prefill = PrefillTarget(model) + model._decode = DecodeTarget(model) + return model + + def _select_target(self, attn_metadata) -> "PrefillTarget | DecodeTarget": + """Which target runs this step. + + One host-side integer read and nothing else: a decode-only CUDA graph is a + per-capture constant environment. A mixed batch -- context and generation + rows in one step -- goes to prefill, so prefill is the general case and + decode the specialization that may assume it has no context rows. + """ + return self._decode if attn_metadata.num_contexts == 0 else self._prefill + + def forward( + self, + attn_metadata, + input_ids: torch.IntTensor | None = None, + position_ids: torch.IntTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + spec_metadata=None, + **kwargs, + ) -> torch.Tensor: + return self._select_target(attn_metadata).forward( + attn_metadata, + input_ids=input_ids, + position_ids=position_ids, + inputs_embeds=inputs_embeds, + spec_metadata=spec_metadata, + **kwargs, + ) + + +class PrefillTarget: + """The general case: context rows, and possibly generation rows beside them. + + Runs the built-in decoder forward. + """ + + phase = dispatch.Phase.PREFILL + + def __init__(self, core: GlmMoeDsaTepModelingV2) -> None: + self.core = core + + def forward( + self, + attn_metadata, + input_ids: torch.IntTensor | None = None, + position_ids: torch.IntTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + spec_metadata=None, + **kwargs, + ) -> torch.Tensor: + return DeepseekV3Model.forward( + self.core, + attn_metadata, + input_ids=input_ids, + position_ids=position_ids, + inputs_embeds=inputs_embeds, + spec_metadata=spec_metadata, + **kwargs, + ) + + +class DecodeTarget: + """The specialization: routed to only when there are no context rows. + + Runs this target's decoder loop. + """ + + phase = dispatch.Phase.DECODE + + def __init__(self, core: GlmMoeDsaTepModelingV2) -> None: + self.core = core + + def forward( + self, + attn_metadata: AttentionMetadata, + input_ids: Optional[torch.IntTensor] = None, + position_ids: Optional[torch.IntTensor] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + spec_metadata: Optional[SpecMetadata] = None, + **kwargs, + ) -> torch.Tensor: + """The decoder layers of the text model on a decode step. + + The built-in loop with the decode prologue's embedding. + Each layer is called as a module, so its forward hooks run, and runs its decode + composition under the step's phase. The draft layer is not run here; the last decoder + layer applies the final norm. + """ + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError( + "You cannot specify both input_ids and inputs_embeds at the same time, and must specify either one" + ) + + prologue = self.core.decode_prologue + if inputs_embeds is None and prologue is not None: + # The rows the decode prologue launch embedded (None unless that launch ran for + # exactly these ids and this table). + inputs_embeds = prologue.take_embedding( + attn_metadata, input_ids, self.core.embed_tokens + ) + if inputs_embeds is None: + inputs_embeds = self.core.embed_tokens(input_ids) + + hidden_states = inputs_embeds + residual = None + + for idx, decoder_layer in enumerate(self.core.layers[: self.core.num_hidden_layers]): + hidden_states, residual = decoder_layer( + position_ids=position_ids, + hidden_states=hidden_states, + attn_metadata=attn_metadata, + residual=residual, + spec_metadata=spec_metadata, + ) + + hidden_states = maybe_allgather_for_helix_cp( + hidden_states, attn_metadata, self.core.mapping_with_cp + ) + return hidden_states + + +class ModelingV2Glm52Nvfp4Sm103Tep(GlmMoeDsaForCausalLM): + """GLM-5.2-NVFP4 on sm_103 at tep: the built-in causal LM with this target's core and dispatch.""" + + def __init__(self, model_config: ModelConfig): + world_size = model_config.mapping.world_size + knobs = dispatch.read_knobs() + mode = resolved_mode(type(self)) + dispatch.check_rank_uniform(dispatch.rank_uniform_inputs(knobs, mode), world_size) + plan = dispatch.resolve(mode, dict(knobs), TARGET_PATHS[model_config.mapping.tp_size]) + dispatch.install(plan) + super().__init__(model_config) + GlmMoeDsaTepModelingV2.adopt(self.model) + self.plan = plan + adopt_model(self, self.plan) + dispatch.check_rank_uniform_adopted(adopted_state(self), world_size) + + @property + def runtime_hooks(self) -> RuntimeHooks: + """The step-loop hooks this target declares, built on the engine's first read. + + Built after the model is loaded: a hook may hold pinned host memory, which model + construction (under meta-device initialization) must not allocate. + + Raises: + RuntimeError: if building the hooks raises ``AttributeError``, which + ``runtime_hooks_of`` would otherwise read as "no hooks declared". + """ + hooks = self.__dict__.get("_runtime_hooks") + if hooks is None: + try: + hooks = runtime.make_runtime_hooks(self) + except AttributeError as exc: + raise RuntimeError( + f"building the step-loop hooks of {type(self).__name__} failed: {exc}" + ) from exc + self._runtime_hooks = hooks + return hooks + + def forward( + self, + attn_metadata, + input_ids: torch.IntTensor | None = None, + position_ids: torch.IntTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + return_context_logits: bool = False, + spec_metadata=None, + resource_manager=None, + **kwargs, + ): + """The built-in forward under the phase of the target that takes this step. + + The scope covers the text model, the speculative worker and its draft loop. + """ + with dispatch.step(self.model._select_target(attn_metadata).phase): + return super().forward( + attn_metadata, + input_ids=input_ids, + position_ids=position_ids, + inputs_embeds=inputs_embeds, + return_context_logits=return_context_logits, + spec_metadata=spec_metadata, + resource_manager=resource_manager, + **kwargs, + ) + + def load_weights(self, weights): + _weights.load(self, weights) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/weights.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/weights.py new file mode 100644 index 000000000000..09b1849fe5f4 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tep/weights.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""The weights this target expects: the GLM-5.2 NVFP4 checkpoint as the built-in model loads it. + +The target's module tree is the built-in model's, so the built-in loader maps +every checkpoint tensor, including the bf16 MTP layer and the NVFP4 scales. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from tensorrt_llm._torch.models.modeling_deepseekv3 import GlmMoeDsaForCausalLM + +if TYPE_CHECKING: + from .modeling import ModelingV2Glm52Nvfp4Sm103Tep + + +def load(model: "ModelingV2Glm52Nvfp4Sm103Tep", weights: Any) -> None: + """Load the checkpoint ``weights`` into ``model`` with the built-in loader.""" + GlmMoeDsaForCausalLM.load_weights(model, weights) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/__init__.py new file mode 100644 index 000000000000..87156adf61ff --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""GLM-5.2-NVFP4 / sm_103 / tp8.""" diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/__init__.py new file mode 100644 index 000000000000..94b44234a92b --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Kernels of the GLM-5.2-NVFP4 sm_103 tp8 target, one package per fast path.""" diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/__init__.py new file mode 100644 index 000000000000..555ee2664d03 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/__init__.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""cross_layer_expert_prefetch: cross-layer speculative routed-expert L2 prefetch. + +Registered entry points: ``link_layers(layers, num_hidden_layers)`` (from the +model's ``setup_aliases``) and ``join()`` (end of the target model's forward). +Per-layer hooks are methods of the ``ExpertPrefetchLink`` bound to each MoE layer +as ``_expert_prefetch_link``. +""" + +from ....shared import dispatch +from .spec_experts import ExpertPrefetchLink, config, join, link_layers + +dispatch.register("cross_layer_expert_prefetch", "link_layers", link_layers) +dispatch.register("cross_layer_expert_prefetch", "join", join) + +__all__ = ["ExpertPrefetchLink", "config", "join", "link_layers"] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/kernels.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/kernels.py new file mode 100644 index 000000000000..ab6a84c51599 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/kernels.py @@ -0,0 +1,405 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Triton kernels of cross_layer_expert_prefetch. + +``rank_and_stats``: one launch over the 256 experts of one MoE layer, with no +sequential loop over experts. + +* Prediction (``NE / BE`` programs, one block of ``BE`` experts each): from + approximate router logits of the next layer (``h_L`` times the next layer's + gate), the score ``sigmoid(logit) + bias`` selects each live row's top-8 + experts; experts are ranked by how many rows select them, then by their + maximum score over the rows, then by ID. Each program computes the rank of + its own experts by pairwise comparison against all 256 and writes the IDs + ranked below ``KMAX`` to their rank slot. +* Statistics (one more program): from this layer's router logits (the ones + its routed MoE uses) and the ranking predicted for this layer by the + previous layer, the program adds to this layer's counters: calls, distinct + experts D, and the number of the D experts among the first K predicted, for + each K of ``STAT_KS``. + +``gemm_rank``: the prediction in one launch, without the router GEMM: each of +``NE / 16`` programs computes the logits of 16 experts (``tl.dot`` over the +hidden size) and their scores, publishes them, and the last program to arrive +ranks all experts in the same order as above, with one sort of a unique key. + +``delay``: one warp that sleeps for a given time, to place the work queued after +it on the same stream later in the decode step. + +``prefetch_experts``: fire-and-forget ``cp.async.bulk.prefetch.L2`` of the +local FC1 / FC2 weights and block scales of the first ``K`` predicted experts, +expert by expert in rank order, FC1 bytes before FC2 bytes. +""" + +import triton +import triton.language as tl + +NUM_EXPERTS = 256 +TOP_K = 8 +MAX_ROWS = 8 +KMAX = 32 +# Experts ranked per prediction program. +RANK_BLOCK = 32 +# Experts per program of the fused GEMM + rank kernel. +GEMM_BLOCK = 16 +# Columns of the per-layer counter rows (int64). +STAT_KS = (8, 12, 18, 24, 32) +C_CALLS = 0 +C_DISTINCT = 1 +C_HITS = 2 # C_HITS + i for STAT_KS[i] +NUM_COUNTERS = C_HITS + len(STAT_KS) + + +@triton.jit +def _scores_topk_union(s, live, NE: tl.constexpr, TOPK: tl.constexpr): + """For scores ``s`` [BR, NE] (dead rows -inf), the experts in each live + row's top-``TOPK``; returns (count over rows [NE], max score [NE]).""" + e = tl.arange(0, NE) + # TOPK-th largest score of every row: TOPK passes, each removing one + # maximum (the lowest index among equal maxima). + work = s + for _ in tl.static_range(TOPK): + thr, idx = tl.max(work, axis=1, return_indices=True) + work = tl.where(e[None, :] == idx[:, None], float("-inf"), work) + sel = (s >= thr[:, None]) & live[:, None] + cnt = tl.sum(sel.to(tl.int32), axis=0) + mx = tl.max(s, axis=0) + return cnt, mx + + +@triton.jit +def _row_topk_union( + logits_ptr, bias_ptr, rows, NE: tl.constexpr, BR: tl.constexpr, TOPK: tl.constexpr +): + """Per live row, the experts in that row's top-``TOPK`` of + ``sigmoid(logit) + bias``; returns (count over rows [NE], max score [NE]).""" + r = tl.arange(0, BR) + e = tl.arange(0, NE) + live = r < rows + x = tl.load(logits_ptr + r[:, None] * NE + e[None, :], mask=live[:, None], other=0.0).to( + tl.float32 + ) + b = tl.load(bias_ptr + e).to(tl.float32) + s = 1.0 / (1.0 + tl.exp(-x)) + b[None, :] + s = tl.where(live[:, None], s, float("-inf")) + return _scores_topk_union(s, live, NE, TOPK) + + +@triton.jit +def _store_ranked(cnt, mx, eb, out_ids_ptr, NE: tl.constexpr, KM: tl.constexpr): + """Rank the experts ``eb`` against all NE by (rows selecting, max score, + ID) and store each one ranked below ``KM`` to its rank slot.""" + e = tl.arange(0, NE) + same = e[None, :] == eb[:, None] + cb = tl.sum(tl.where(same, cnt[None, :], 0), axis=1) + mb = tl.max(tl.where(same, mx[None, :], float("-inf")), axis=1) + cj = cnt[None, :] + mj = mx[None, :] + better = (cj > cb[:, None]) | ( + (cj == cb[:, None]) + & ((mj > mb[:, None]) | ((mj == mb[:, None]) & (e[None, :] < eb[:, None]))) + ) + rank = tl.sum(better.to(tl.int32), axis=1) + tl.store(out_ids_ptr + rank, eb.to(tl.int32), mask=rank < KM) + + +@triton.jit +def _rank_and_stats_kernel( + next_logits_ptr, + next_bias_ptr, + out_ids_ptr, + cur_logits_ptr, + cur_bias_ptr, + cur_ids_ptr, + counters_ptr, + rows, + NPRED: tl.constexpr, + HAS_STATS: tl.constexpr, + NE: tl.constexpr, + BR: tl.constexpr, + BE: tl.constexpr, + TOPK: tl.constexpr, + KM: tl.constexpr, + K0: tl.constexpr, + K1: tl.constexpr, + K2: tl.constexpr, + K3: tl.constexpr, + K4: tl.constexpr, +): + pid = tl.program_id(0) + e = tl.arange(0, NE) + if pid < NPRED: + cnt, mx = _row_topk_union(next_logits_ptr, next_bias_ptr, rows, NE, BR, TOPK) + # this program's experts, ranked with the values of the full vectors + _store_ranked(cnt, mx, pid * BE + tl.arange(0, BE), out_ids_ptr, NE, KM) + elif HAS_STATS: + cnt, _ = _row_topk_union(cur_logits_ptr, cur_bias_ptr, rows, NE, BR, TOPK) + actual = cnt > 0 + # rank position of every expert in the ranking predicted for this + # layer (KM = not among the first KM) + j = tl.arange(0, KM) + ids = tl.load(cur_ids_ptr + j) + pos = tl.min(tl.where(ids[:, None] == e[None, :], j[:, None], KM), axis=0) + tl.atomic_add(counters_ptr + 0, tl.full([], 1, tl.int64)) + tl.atomic_add(counters_ptr + 1, tl.sum(actual.to(tl.int64), axis=0)) + tl.atomic_add(counters_ptr + 2, tl.sum((actual & (pos < K0)).to(tl.int64), axis=0)) + tl.atomic_add(counters_ptr + 3, tl.sum((actual & (pos < K1)).to(tl.int64), axis=0)) + tl.atomic_add(counters_ptr + 4, tl.sum((actual & (pos < K2)).to(tl.int64), axis=0)) + tl.atomic_add(counters_ptr + 5, tl.sum((actual & (pos < K3)).to(tl.int64), axis=0)) + tl.atomic_add(counters_ptr + 6, tl.sum((actual & (pos < K4)).to(tl.int64), axis=0)) + + +def rank_and_stats( + next_logits, + next_bias, + out_ids, + cur_logits, + cur_bias, + cur_ids, + counters, + rows: int, + has_pred: bool, + has_stats: bool, +) -> None: + """Launch the prediction and / or statistics programs on the current + stream. Unused pointer arguments may be any tensor of the right device.""" + npred = NUM_EXPERTS // RANK_BLOCK if has_pred else 0 + grid = npred + (1 if has_stats else 0) + if grid == 0: + return + _rank_and_stats_kernel[(grid,)]( + next_logits, + next_bias, + out_ids, + cur_logits, + cur_bias, + cur_ids, + counters, + rows, + NPRED=npred, + HAS_STATS=has_stats, + NE=NUM_EXPERTS, + BR=MAX_ROWS, + BE=RANK_BLOCK, + TOPK=TOP_K, + KM=KMAX, + K0=STAT_KS[0], + K1=STAT_KS[1], + K2=STAT_KS[2], + K3=STAT_KS[3], + K4=STAT_KS[4], + num_warps=4, + ) + + +@triton.jit +def _gemm_rank_kernel( + h_ptr, + w_ptr, + bias_ptr, + scores_ptr, + ticket_ptr, + out_ids_ptr, + rows, + H: tl.constexpr, + NE: tl.constexpr, + BR: tl.constexpr, + BM: tl.constexpr, + BE: tl.constexpr, + BK: tl.constexpr, + TOPK: tl.constexpr, + KM: tl.constexpr, +): + # Logits and scores of this program's BE experts for the rows of h; the + # gate rows are read contiguously along the hidden dimension. + pid = tl.program_id(0) + NPROG: tl.constexpr = NE // BE + m = tl.arange(0, BM) + eb = pid * BE + tl.arange(0, BE) + kk = tl.arange(0, BK) + acc = tl.zeros([BM, BE], dtype=tl.float32) + for k0 in range(0, H, BK): + a = tl.load(h_ptr + m[:, None] * H + k0 + kk[None, :], mask=(m < rows)[:, None], other=0.0) + b = tl.load(w_ptr + eb[:, None] * H + k0 + kk[None, :]) + acc += tl.dot(a, tl.trans(b)) + bias = tl.load(bias_ptr + eb).to(tl.float32) + s = 1.0 / (1.0 + tl.exp(-acc)) + bias[None, :] + # Publish the scores at L2 and take a ticket; the last program to arrive + # ranks all NE experts and resets the ticket for the next launch. + tl.atomic_xchg( + scores_ptr + m[:, None] * NE + eb[None, :], s, mask=(m < BR)[:, None], sem="relaxed" + ) + tl.debug_barrier() + ticket = tl.atomic_add(ticket_ptr, 1, sem="acq_rel") + if ticket == NPROG - 1: + r = tl.arange(0, BR) + e = tl.arange(0, NE) + live = r < rows + sc = tl.load(scores_ptr + r[:, None] * NE + e[None, :], cache_modifier=".cg") + sc = tl.where(live[:, None], sc, float("-inf")) + cnt, mx = _scores_topk_union(sc, live, NE, TOPK) + # One sort of a unique key per expert: rows selecting, then the max + # score (order-preserving bits), then the lower ID first. + bits = mx.to(tl.int32, bitcast=True) + ordered = tl.where(bits < 0, bits ^ 0x7FFFFFFF, bits).to(tl.int64) + 2147483648 + key = (cnt.to(tl.int64) << 41) + (ordered << 8) + (NE - 1 - e).to(tl.int64) + key = tl.sort(key, dim=0, descending=True) + ids = (NE - 1 - (key & (NE - 1))).to(tl.int32) + tl.store(out_ids_ptr + e, ids, mask=e < KM) + tl.atomic_xchg(ticket_ptr, 0, sem="relaxed") + + +def gemm_rank(hidden, weight, bias, scores, ticket, out_ids, rows: int) -> None: + """Prediction in one launch on the current stream: ``hidden`` [rows, H] + BF16 times the next layer's gate ``weight`` [256, H] BF16, scores, per-row + top-8 union and ranking into ``out_ids`` (as ``rank_and_stats``). + ``scores`` is an FP32 [MAX_ROWS, 256] scratch and ``ticket`` an int32 [1] + zero; both are reused by every launch on the same stream.""" + _gemm_rank_kernel[(NUM_EXPERTS // GEMM_BLOCK,)]( + hidden, + weight, + bias, + scores, + ticket, + out_ids, + rows, + H=hidden.shape[1], + NE=NUM_EXPERTS, + BR=MAX_ROWS, + BM=16, + BE=GEMM_BLOCK, + BK=512, + TOPK=TOP_K, + KM=KMAX, + num_warps=4, + num_stages=4, + ) + + +@triton.jit +def _globaltimer(z): + return tl.inline_asm_elementwise( + "mov.u64 $0, %globaltimer; // $1", "=l,r", [z], dtype=tl.int64, is_pure=False, pack=1 + ) + + +@triton.jit +def _nanosleep(ns): + return tl.inline_asm_elementwise( + "nanosleep.u32 $1; mov.u32 $0, 0;", "=r,r", [ns], dtype=tl.int32, is_pure=False, pack=1 + ) + + +@triton.jit +def _delay_kernel(delay_ns): + # One warp sleeps until delay_ns of global time have passed. + z = tl.zeros([32], dtype=tl.int32) + t0 = tl.max(_globaltimer(z), axis=0) + t = t0 + while t - t0 < delay_ns: + _nanosleep(z + 256) + t = tl.max(_globaltimer(z), axis=0) + + +def delay(delay_ns: int) -> None: + """Hold the current stream for about ``delay_ns`` nanoseconds with one + warp (the work queued after it starts that much later).""" + _delay_kernel[(1,)](delay_ns, num_warps=1) + + +@triton.jit +def _bulk_prefetch_l2(addr, nbytes): + tl.inline_asm_elementwise( + "cp.async.bulk.prefetch.L2.global [$1], $2; // dummy $0", + "=r,l,r", + [addr, nbytes], + dtype=tl.int32, + is_pure=False, + pack=1, + ) + + +@triton.jit +def _prefetch_experts_kernel( + ids_ptr, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + K: tl.constexpr, + CHUNK: tl.constexpr, + C13: tl.constexpr, + C13SF: tl.constexpr, + C2: tl.constexpr, + C2SF: tl.constexpr, + NPROG: tl.constexpr, + NT: tl.constexpr, +): + # Chunks of one expert, in the order the routed kernel reads them: + # FC1 weight, FC1 scales, FC2 weight, FC2 scales. Addresses are int64 + # byte addresses (the four tensors have different element types). + CPE: tl.constexpr = C13 + C13SF + C2 + C2SF + TOTAL: tl.constexpr = K * CPE + ITERS: tl.constexpr = (TOTAL + NPROG * NT - 1) // (NPROG * NT) + pid = tl.program_id(0).to(tl.int64) + lanes = tl.arange(0, NT).to(tl.int64) + for it in range(ITERS): + c = (it * NPROG + pid) * NT + lanes + live = c < TOTAL + c = tl.where(live, c, 0) + slot = c // CPE + off = c - slot * CPE + ex = tl.load(ids_ptr + slot, mask=live, other=0).to(tl.int64) + a13 = w13_addr + ex * (C13 * CHUNK) + off * CHUNK + o1 = off - C13 + a13sf = w13_sf_addr + ex * (C13SF * CHUNK) + o1 * CHUNK + o2 = o1 - C13SF + a2 = w2_addr + ex * (C2 * CHUNK) + o2 * CHUNK + o3 = o2 - C2 + a2sf = w2_sf_addr + ex * (C2SF * CHUNK) + o3 * CHUNK + addr = tl.where(off < C13, a13, tl.where(o1 < C13SF, a13sf, tl.where(o2 < C2, a2, a2sf))) + # Lanes past the end re-issue the first chunk, which is harmless. + _bulk_prefetch_l2(addr, tl.full([NT], CHUNK, tl.int32)) + + +def prefetch_experts( + ids, w13, w13_sf, w2, w2_sf, k: int, chunk: int, ctas: int, threads: int = 32 +) -> None: + """Prefetch the first ``k`` experts of ``ids`` into L2 on the current + stream. Every expert slice of the four tensors must be a whole number of + ``chunk`` bytes.""" + + def per_expert_chunks(t): + nbytes = t[0].numel() * t.element_size() + assert nbytes % chunk == 0, (tuple(t.shape), chunk) + return nbytes // chunk + + _prefetch_experts_kernel[(ctas,)]( + ids, + w13.data_ptr(), + w13_sf.data_ptr(), + w2.data_ptr(), + w2_sf.data_ptr(), + K=k, + CHUNK=chunk, + C13=per_expert_chunks(w13), + C13SF=per_expert_chunks(w13_sf), + C2=per_expert_chunks(w2), + C2SF=per_expert_chunks(w2_sf), + NPROG=ctas, + NT=threads, + num_warps=max(1, threads // 32), + num_stages=1, + ) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/spec_experts.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/spec_experts.py new file mode 100644 index 000000000000..82dec07518b5 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/cross_layer_expert_prefetch/spec_experts.py @@ -0,0 +1,508 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Cross-layer speculative routed-expert L2 prefetch (``cross_layer_expert_prefetch``). + +At a decode call of MoE layer ``L`` (1 to 8 rows), the post-attention hop has +produced this layer's normalized hidden ``h_L`` and router logits. On a +private auxiliary stream, forked from the caller's stream at that point: + +1. ``h_L`` times the next MoE layer's gate gives approximate router logits of + layer ``L + 1``; every live row's top-8 of ``sigmoid(logit) + bias`` is + taken, and the experts are ranked by how many rows select them (then by + their best score). The first ``KMAX`` expert IDs are kept for layer + ``L + 1``. +2. Layer ``L``'s own routing (from its router logits) is compared with the + ranking predicted for it at layer ``L - 1``: per-layer device counters of + calls, distinct experts and hits among the first K predicted experts. +3. After layer ``L``'s routed MoE (``start="moe"``), + or right after the prediction (``hop``), or after layer ``L``'s post-MoE + hop (``post``), a kernel issues ``cp.async.bulk.prefetch.L2`` over the + local FC1 / FC2 weights and block scales of the first ``K`` predicted + experts of layer ``L + 1`` (``k``, 0 = predict and count only). + +With ``issue="xc"`` no prefetch kernel is launched: the routed_moe +expert-cluster kernel of layer ``L + 1`` issues the prefetch itself, before +its dependency wait, i.e. while the pre-MoE hop of layer ``L + 1`` still runs +(``routed_moe/kernel_xc.py``, ``spec_k``). With ``pred="self"`` there +is no auxiliary work at all: the expert-cluster kernel of layer ``L`` writes +its own experts, ranked by row count, and its next call (the next decode +step) prefetches them. + +A prefetch has no architectural effect, and the auxiliary work writes only +this package's buffers, so model outputs are unchanged. The caller's stream +never waits for the auxiliary stream inside a layer: the hidden state and +router logits the auxiliary work reads are recorded on the auxiliary stream +(``Tensor.record_stream``), so the caching allocator does not reuse them +before that work is done (under CUDA-graph capture, not before the capture +ends). The caller's stream waits for all auxiliary work once, at the end of +the target model's forward (``join``), so a CUDA-graph capture always sees the +auxiliary stream rejoin. + +Configuration (``ExpertPrefetchConfig``; this target runs ``SERVED_CONFIG``): + +* ``k``: experts prefetched per layer, 0..32 (8). +* ``start``: ``moe``, ``hop`` or ``post`` (``moe``). +* ``pred``: where the prediction (and counters) is forked: + ``hop`` (right after the pre-MoE hop, concurrent with layer L's + routed MoE) or ``moe`` (right after layer L's routed MoE, together with a + ``start=moe`` prefetch); ``moe`` excludes ``start=hop``. ``self``: no + cross-layer prediction (see above; needs ``issue=xc``, no counters) (``self``). +* ``issue``: ``aux`` (a prefetch kernel on the auxiliary stream, placed by + ``start`` and ``delay_ns``) or ``xc`` (the routed MoE kernel of the + predicted layer) (``xc``). +* ``xc_ctas``: with ``issue=xc``, the routed-kernel CTAs that issue the + prefetch (0 = all) (32). +* ``fused``: predict with one fused Triton launch (``kernels.gemm_rank``: gate + GEMM, scores and ranking) when the next gate is a BF16 [256, 6144] weight; + otherwise the next gate module plus the ranking kernel (True). +* ``delay_ns``: nanoseconds the auxiliary stream waits (one sleeping warp) + right before each prefetch, to move the prefetch traffic later into layer + L+1 (0). +* ``chunk``: bytes per prefetch instruction (16384). +* ``ctas``: CTAs of the prefetch kernel (16). + +``start``, ``fused``, ``delay_ns``, ``chunk`` and ``ctas`` act only with the +``hop`` / ``moe`` predictors and the ``aux`` prefetch kernel; under ``self`` +they appear only in the capability key. +""" + +from dataclasses import dataclass +from typing import Dict, List, Optional + +import torch + +from ....shared import dispatch +from . import kernels + +HIDDEN = 6144 +STARTS = ("moe", "hop", "post") +PREDS = ("hop", "moe", "self") +ISSUES = ("aux", "xc") + + +@dataclass(frozen=True) +class ExpertPrefetchConfig: + k: int + start: str + pred: str + fused: bool + delay_ns: int + chunk: int + ctas: int + issue: str = "xc" + xc_ctas: int = 32 + + +#: The expert-prefetch configuration of this target: the routed MoE kernel of each layer writes +#: its own experts (``self``) and its next call prefetches the first 8 on 32 of its CTAs. +SERVED_CONFIG = ExpertPrefetchConfig( + k=8, + start="moe", + pred="self", + fused=True, + delay_ns=0, + chunk=16384, + ctas=16, + issue="xc", + xc_ctas=32, +) + + +def config() -> ExpertPrefetchConfig: + """The expert-prefetch configuration of this target, ``SERVED_CONFIG``.""" + return SERVED_CONFIG + + +class _DeviceState: + """Per-device stream, events and buffers shared by every linked layer. + + Created on the first eager call of a device, never replaced (captured + graphs keep the buffer addresses). + """ + + def __init__(self, device: torch.device, num_layers: int): + self.device = device + self.aux = torch.cuda.Stream(device=device) + self.ev_fork = torch.cuda.Event() + self.ev_tail = torch.cuda.Event() + self.counters = torch.zeros( + num_layers, kernels.NUM_COUNTERS, dtype=torch.int64, device=device + ) + self.pred_ids = torch.zeros(num_layers + 1, kernels.KMAX, dtype=torch.int32, device=device) + self.dummy_f32 = torch.zeros( + kernels.MAX_ROWS, kernels.NUM_EXPERTS, dtype=torch.float32, device=device + ) + self.dummy_bias = torch.zeros(kernels.NUM_EXPERTS, dtype=torch.float32, device=device) + # scratch of the fused prediction (kernels.gemm_rank) + self.scores = torch.zeros( + kernels.MAX_ROWS, kernels.NUM_EXPERTS, dtype=torch.float32, device=device + ) + self.ticket = torch.zeros(1, dtype=torch.int32, device=device) + self.forked = False + # prefetch launches issued from Python (eager calls and captures, not + # graph replays) + self.prefetch_calls = 0 + + +_STATES: Dict[int, _DeviceState] = {} +_NUM_LAYERS = {"n": 0} + + +def _state(device: torch.device) -> _DeviceState: + idx = device.index if device.index is not None else torch.cuda.current_device() + st = _STATES.get(idx) + if st is None: + if torch.cuda.is_current_stream_capturing(): + raise RuntimeError( + "[glm_moe_dsa] cross_layer_expert_prefetch device state must be created by an " + "eager call before CUDA-graph capture" + ) + try: + from torch.utils._python_dispatch import _disable_current_modes + except ImportError: + import contextlib + + _disable_current_modes = contextlib.nullcontext + with _disable_current_modes(): + st = _DeviceState(torch.device("cuda", idx), _NUM_LAYERS["n"]) + _STATES[idx] = st + return st + + +def _expert_tensors(experts) -> Optional[List[torch.Tensor]]: + """The four per-expert weight tensors of a native NVFP4 MoE leaf, or None.""" + from ..routed_moe.routed_moe import resolve_expert_owner + + owner, _ = resolve_expert_owner(experts) + if owner is None: + return None + names = ("w3_w1_weight", "w3_w1_weight_scale", "w2_weight", "w2_weight_scale") + ts = [owner._parameters.get(n) for n in names] + if any(t is None for t in ts): + return None + return ts + + +def _prefetch_reason(ts: Optional[List[torch.Tensor]], chunk: int) -> str: + if ts is None: + return "next layer has no native NVFP4 expert leaf" + for t in ts: + if not t.is_cuda or not t.is_contiguous(): + return "expert tensor is not contiguous on CUDA" + if t.shape[0] != kernels.NUM_EXPERTS: + return f"expert tensor {tuple(t.shape)} is not [256, ...]" + if t.data_ptr() % 16: + return "expert tensor is not 16-byte aligned" + if (t[0].numel() * t.element_size()) % chunk: + return f"expert slice of {tuple(t.shape)} is not a multiple of {chunk} bytes" + return "" + + +class ExpertPrefetchLink: + """cross_layer_expert_prefetch state of one MoE decoder layer ``L`` (bound by ``link_layers``).""" + + def __init__(self, layer_idx: int, cur_gate, next_gate, next_experts, has_prev: bool): + self.layer_idx = layer_idx + self.cur_gate = cur_gate + self.next_gate = next_gate + self.next_experts = next_experts + self.has_prev = has_prev + self._armed = False + self._pending_prefetch = False + self._pending_predict: Optional[tuple] = None + self._next_tensors: Optional[List[torch.Tensor]] = None + self._prefetch_ok: Optional[bool] = None + self._fused_ok: Optional[bool] = None + + @staticmethod + def _eligible(hidden, router_logits) -> str: + if not isinstance(hidden, torch.Tensor) or not hidden.is_cuda: + return "hidden is not a CUDA tensor" + if hidden.dtype != torch.bfloat16 or hidden.dim() != 2 or hidden.shape[1] != HIDDEN: + return f"hidden {tuple(hidden.shape)} {hidden.dtype}" + rows = hidden.shape[0] + if not 1 <= rows <= kernels.MAX_ROWS: + return f"{rows} rows outside [1, {kernels.MAX_ROWS}]" + if not hidden.is_contiguous(): + return "hidden is not contiguous" + if router_logits is not None and ( + router_logits.dtype != torch.float32 + or tuple(router_logits.shape) != (rows, kernels.NUM_EXPERTS) + or not router_logits.is_contiguous() + ): + return f"router logits {tuple(router_logits.shape)} {router_logits.dtype}" + return "" + + def _prefetch_ready(self, cfg: ExpertPrefetchConfig) -> bool: + if self._prefetch_ok is None: + ts = _expert_tensors(self.next_experts) if self.next_experts is not None else None + reason = _prefetch_reason(ts, cfg.chunk) + self._prefetch_ok = not reason + self._next_tensors = ts if not reason else None + dispatch.log_capability( + "cross_layer_expert_prefetch", "expert_prefetch", self._prefetch_ok, reason + ) + return self._prefetch_ok + + def _launch_prefetch(self, st: _DeviceState, cfg: ExpertPrefetchConfig) -> None: + if cfg.delay_ns: + kernels.delay(cfg.delay_ns) + w13, w13_sf, w2, w2_sf = self._next_tensors + kernels.prefetch_experts( + st.pred_ids[self.layer_idx + 1], w13, w13_sf, w2, w2_sf, cfg.k, cfg.chunk, cfg.ctas + ) + st.prefetch_calls += 1 + + def _fused_ready(self, cfg: ExpertPrefetchConfig) -> bool: + if self._fused_ok is None: + reason = "" + if not cfg.fused: + reason = "fused=0" + else: + w = getattr(self.next_gate, "weight", None) + if ( + not isinstance(w, torch.Tensor) + or w.dtype != torch.bfloat16 + or tuple(w.shape) != (kernels.NUM_EXPERTS, HIDDEN) + or not w.is_contiguous() + ): + reason = "next gate weight is not a contiguous BF16 [256, 6144]" + self._fused_ok = not reason + dispatch.log_capability( + "cross_layer_expert_prefetch", "fused_predict", self._fused_ok, reason + ) + return self._fused_ok + + def _predict( + self, + st: _DeviceState, + cfg: ExpertPrefetchConfig, + hidden, + router_logits, + has_pred: bool, + has_stats: bool, + ) -> None: + """Prediction for layer L+1 and counters of layer L (current stream).""" + rows = hidden.shape[0] + out_ids = st.pred_ids[self.layer_idx + 1] + fused = has_pred and self._fused_ready(cfg) + if fused: + kernels.gemm_rank( + hidden, + self.next_gate.weight, + self.next_gate.e_score_correction_bias, + st.scores, + st.ticket, + out_ids, + rows, + ) + if fused and not has_stats: + return + has_pred = has_pred and not fused + next_logits = self.next_gate(hidden) if has_pred else st.dummy_f32 + next_bias = self.next_gate.e_score_correction_bias if has_pred else st.dummy_bias + kernels.rank_and_stats( + next_logits, + next_bias, + out_ids, + router_logits if has_stats else st.dummy_f32, + self.cur_gate.e_score_correction_bias if has_stats else st.dummy_bias, + st.pred_ids[self.layer_idx], + st.counters[self.layer_idx], + rows, + has_pred, + has_stats, + ) + + def _fork( + self, st: _DeviceState, cfg: ExpertPrefetchConfig, predict: Optional[tuple], prefetch: bool + ) -> None: + """Fork auxiliary work at this point of the caller's stream: the + prediction (``predict`` = arguments of ``_predict``) and / or the + prefetch of layer L+1, in that order.""" + caller = torch.cuda.current_stream() + st.ev_fork.record(caller) + st.aux.wait_event(st.ev_fork) + with torch.cuda.stream(st.aux): + if predict is not None: + self._predict(st, cfg, *predict) + if prefetch: + self._launch_prefetch(st, cfg) + st.ev_tail.record(st.aux) + st.forked = True + + def on_hop(self, hidden, router_logits) -> None: + """After layer L's pre-MoE hop: fork the prediction (``pred=hop``) or + keep its inputs for ``after_moe`` (``pred=moe``).""" + self._armed = False + self._pending_prefetch = False + self._pending_predict = None + reason = self._eligible(hidden, router_logits) + if reason: + # calls outside the decode shape (e.g. warmup with many rows) + dispatch.log_capability( + "cross_layer_expert_prefetch", "spec_experts_declined", False, reason + ) + return + dispatch.log_capability("cross_layer_expert_prefetch", "spec_experts", True) + cfg = config() + # created eagerly here: the routed kernel's prefetch hook reads it + st = _state(hidden.device) + if cfg.pred == "self": + return + has_pred = self.next_gate is not None + # The hit counters are not reported in this target. + has_stats = False + if not has_pred and not has_stats: + return + hidden.record_stream(st.aux) + if has_stats: + router_logits.record_stream(st.aux) + predict = (hidden, router_logits, has_pred, has_stats) + if cfg.pred == "hop": + prefetch = ( + has_pred + and cfg.k > 0 + and cfg.issue == "aux" + and cfg.start == "hop" + and self._prefetch_ready(cfg) + ) + self._fork(st, cfg, predict, prefetch) + else: + self._pending_predict = predict + self._armed = True + self._pending_prefetch = ( + has_pred and cfg.k > 0 and cfg.issue == "aux" and cfg.start != "hop" + ) + + def after_moe(self) -> None: + """After layer L's routed MoE: fork the pending prediction + (``pred=moe``) and / or the prefetch of layer L+1 (``start=moe``).""" + if not self._armed: + return + cfg = config() + prefetch = self._pending_prefetch and cfg.start == "moe" and self._prefetch_ready(cfg) + if prefetch: + self._pending_prefetch = False + predict, self._pending_predict = self._pending_predict, None + if predict is not None or prefetch: + st = _state(torch.device("cuda", torch.cuda.current_device())) + self._fork(st, cfg, predict, prefetch) + + def after_post_moe(self) -> None: + """After layer L's post-MoE hop (``start=post``).""" + if not self._armed: + return + self._armed = False + cfg = config() + if self._pending_prefetch and cfg.start == "post" and self._prefetch_ready(cfg): + self._pending_prefetch = False + st = _state(torch.device("cuda", torch.cuda.current_device())) + self._fork(st, cfg, None, True) + + +def _is_moe_with_gate(layer) -> bool: + mlp = getattr(layer, "mlp", None) + gate = getattr(mlp, "gate", None) + return ( + mlp is not None + and getattr(mlp, "experts", None) is not None + and gate is not None + and hasattr(gate, "e_score_correction_bias") + and hasattr(gate, "weight") + ) + + +def _xc_hook(row: int, cfg: ExpertPrefetchConfig): + """Prefetch hook of a routed-MoE leaf (read by ``routed_moe.routed_moe``): the + ``pred_ids`` row its expert-cluster kernel prefetches, or ``None`` before + the device state exists.""" + self_store = cfg.pred == "self" + + def hook(): + st = _STATES.get(torch.cuda.current_device()) + if st is None: + return None + return (st.pred_ids[row], cfg.k, cfg.xc_ctas, self_store) + + return hook + + +def _xc_issue_reason(cfg: ExpertPrefetchConfig) -> str: + """Why the routed kernel cannot issue the prefetch ("" when it can).""" + if not dispatch.is_enabled("routed_moe"): + return "issue=xc needs routed_moe" + return "" + + +def _bind_xc(experts, row: int, cfg: ExpertPrefetchConfig) -> bool: + from ..routed_moe.routed_moe import resolve_expert_owner + + owner, _ = resolve_expert_owner(experts) + if owner is None: + return False + owner._expert_prefetch_spec = _xc_hook(row, cfg) + return True + + +def link_layers(layers, num_hidden_layers: int) -> int: + """Bind a ``ExpertPrefetchLink`` to every target MoE layer (structural only).""" + cfg = config() + _NUM_LAYERS["n"] = max(_NUM_LAYERS["n"], num_hidden_layers) + linked = 0 + xc_bound = 0 + xc_on = cfg.issue == "xc" and cfg.k > 0 + if xc_on: + reason = _xc_issue_reason(cfg) + xc_on = dispatch.log_capability( + "cross_layer_expert_prefetch", "xc_prefetch", not reason, reason + ) + for idx in range(num_hidden_layers): + layer = layers[idx] + if not _is_moe_with_gate(layer): + continue + nxt = layers[idx + 1] if idx + 1 < num_hidden_layers else None + nxt_moe = nxt is not None and _is_moe_with_gate(nxt) + prev_moe = idx > 0 and _is_moe_with_gate(layers[idx - 1]) + layer._expert_prefetch_link = ExpertPrefetchLink( + idx, + layer.mlp.gate, + nxt.mlp.gate if nxt_moe else None, + nxt.mlp.experts if nxt_moe else None, + prev_moe, + ) + linked += 1 + if xc_on: + if cfg.pred == "self": + xc_bound += _bind_xc(layer.mlp.experts, idx, cfg) + elif nxt_moe: + xc_bound += _bind_xc(nxt.mlp.experts, idx + 1, cfg) + dispatch.log_capability( + "cross_layer_expert_prefetch", + f"link[k={cfg.k},start={cfg.start},pred={cfg.pred},fused={int(cfg.fused)}," + f"delay_ns={cfg.delay_ns},chunk={cfg.chunk}," + f"ctas={cfg.ctas},issue={cfg.issue},xc_ctas={cfg.xc_ctas}]", + linked > 0, + f"{linked} MoE layers, {xc_bound} routed-kernel prefetch hooks", + ) + return linked + + +def join() -> None: + """Make the current stream wait for all forked auxiliary work.""" + for st in _STATES.values(): + if st.forked and st.device.index == torch.cuda.current_device(): + torch.cuda.current_stream().wait_event(st.ev_tail) + st.forked = False diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/draft_routed_moe/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/draft_routed_moe/__init__.py new file mode 100644 index 000000000000..2156cea0dd74 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/draft_routed_moe/__init__.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Package draft_routed_moe: the MTP draft layer's routed experts (BF16) at the DRAM rate. + +Entry point registered with ``dispatch``: + +* ``routed_output(module, routing_bias, x, router_logits, do_finalize)``: + the finalized routed output ``[R, 6144]`` of the BF16 FlashInfer + TRT-LLM-Gen leaf, bit-identical to ``trtllm_bf16_moe``, or ``None`` (the + native path; decided before any device work). +""" + +from ....shared.dispatch import register +from .routed_moe import eligibility, launch_config, op_available, routed_output + +register("draft_routed_moe", "routed_output", routed_output) + +__all__ = ["eligibility", "launch_config", "op_available", "routed_output"] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/draft_routed_moe/routed_moe.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/draft_routed_moe/routed_moe.py new file mode 100644 index 000000000000..66fc869f73d3 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/draft_routed_moe/routed_moe.py @@ -0,0 +1,408 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Host side of package draft_routed_moe: the routed experts of the MTP draft layer. + +The MTP layer of the GLM-5.2 NVFP4 checkpoint is unquantized, so its routed +experts run FlashInfer's TRT-LLM-Gen BF16 MoE (``FlashinferTrtllmGenBf16Impl``, +``trtllm_bf16_moe``). ``routed_output`` replaces that call with +``torch.ops.trtllm.glm_moe_dsa_draft_routed_moe`` on the leaf's own weight buffers (no +copy, no re-layout) and returns the same finalized ``[R, 6144]`` BF16 tensor, +bit for bit: + +* routing: FlashInfer's ``routingCustom`` with ``SigmoidBias`` preprocess and + ``ScaledSumNormalize`` postprocess (DeepSeek-V3 with ``n_group <= 1``), + including its fast-math instructions, tie order and FP32 sum order; +* FC1/FC2: ``mma.sync`` over K in natural order with one FP32 accumulator per + output element (the batched-GEMM cubins accumulate K sequentially and have + no split-K), the cubin's SwiGLU epilogue and BF16 rounding; +* finalize: ``finalizeKernel``'s FP32 ``fma`` chain in top-k slot order. + +The eligibility decision uses host-side state only (module types, routing +configuration, shapes, dtypes, devices, the op's availability and whether the +scratch buffers exist before a CUDA graph capture) and is taken before any +device work; ``None`` leaves the native path with no side effect. + +Programmatic dependent launch (memory order and progress): + +* both kernels are PDL secondaries; neither reads anything before its + ``griddepcontrol.wait`` (the FC1 kernel's first reads, the router logits + and the activation, are the predecessor's outputs); +* the FC1 grid writes the routing record and the BF16 intermediate into the + per-device scratch after its wait; the FC2 grid reads them after its own + wait, which returns once the FC1 grid has completed and its writes are + visible; the FC1 grid triggers its dependents after its tiles (or, with + flag bit 4 ``FLAG_EARLY_TRIGGER``, after the routing), which only lets the + FC2 CTAs be scheduled earlier; +* the scratch of call ``i`` is rewritten by the FC1 grid of call ``i + 1`` + only after that grid's wait, i.e. after its predecessor has completed; every + kernel between the two calls on the stream either is launched without + programmatic overlap or waits for its own predecessor before completing, so + that predecessor's completion implies the completion of call ``i``'s FC2 + grid; +* the optional L2 prefetch of FC2 weights reads immutable model parameters; +* no CTA waits for another CTA of its grid (no spin, no grid barrier), so + progress does not depend on co-residency with the shared expert's kernels. +""" + +import os +from typing import Dict, Optional, Tuple + +import torch + +HIDDEN = 6144 +INTER = 256 +NUM_EXPERTS = 256 +TOP_K = 8 +MAX_ROWS = 8 +K_CHUNK = 64 +SCRATCH_H_ELEMS = 64 * MAX_ROWS * INTER +ROUTE_WORDS = 1 + 64 + 64 + 64 + +FLAG_PDL = 1 +FLAG_PREFETCH_FC2 = 2 +FLAG_EARLY_TRIGGER = 4 +FLAG_FC1_PAIRS = 8 +FLAG_FC2_PAIRS = 16 +FLAG_DEEP_RING = 32 +FLAG_TMA = 64 +FLAG_L2_PREFETCH = 128 +FLAG_PREFETCH_FC2_EARLY = 256 +FLAG_FC1_TRIPLES = 4096 +FLAG_EARLY_TRIGGER_TRIPLES = 8192 +FLAG_FC1_EVICT_FIRST = 32768 +FLAG_STREAM_X = 65536 +FLAG_OPTIONS = ( + FLAG_PREFETCH_FC2 + | FLAG_EARLY_TRIGGER + | FLAG_FC1_PAIRS + | FLAG_FC2_PAIRS + | FLAG_DEEP_RING + | FLAG_TMA + | FLAG_L2_PREFETCH + | FLAG_PREFETCH_FC2_EARLY + | FLAG_FC1_TRIPLES + | FLAG_EARLY_TRIGGER_TRIPLES + | FLAG_FC1_EVICT_FIRST + | FLAG_STREAM_X +) +# Shared memory of the FC1 kernel: the activation rows plus a weight ring per warp (2 KB chunks); the deep ring +# applies to a call only when it fits this budget (the static routing state takes about 3 KB of the 227 KB). +FC1_RING_CHUNKS_DEEP = 20 +FC1_DYNAMIC_SMEM_BUDGET = 220 * 1024 + +OP_NAME = "glm_moe_dsa_draft_routed_moe" + +# Launch configuration (``launch_config``): (ctas, warps) of each kernel and extra flag bits (2: FC2 weights prefetched +# into L2 by the FC1 warps, 4: early dependent trigger, 8: FC1 tile pairs, 16: FC2 expert pairs, 32: deeper weight +# rings, dropped for a call whose FC1 shared memory would not fit, 64: TMA-fed kernels, where "ctas" is the persistent +# grid size and "warps" the number of MMA warps (FC2 also 3), each owning a 16-row chain (two with bit 8), next to one +# producer warp; with them bit 2 is issued by the FC1 producer after its last weight copy, 16 gives every FC2 warp two +# experts per step, 128 adds L2 prefetches of the FC1 weights ahead of the ring, 256 moves the FC2 prefetch to right +# after routing, and bit 32 does not apply; 4096 (4 FC1 MMA warps): a call whose FC1 tiles exceed the grid runs in one +# round of three 64-row sub-tiles per CTA when they fit, chosen on the device from the routing; 8192: the early +# trigger for calls on that path only; 32768: FC1 weight loads with an L2 evict-first policy; 65536 (4 FC1 MMA warps +# with bit 8): the activation streamed through the FC1 ring with the weights instead of kept in shared memory, so the +# ring is deeper). +# Each value is a pair (calls with one row, calls with more rows). None of them changes a bit of the output. +# (one row, more rows): TMA-fed kernels. One row: 64 FC1 CTAs (64-row tiles, one per CTA), which leaves SMs to the +# shared expert on the auxiliary stream, and the FC2 weights prefetched into L2 behind the last FC1 copy. More rows: +# 128-row FC1 tiles (two chains per MMA warp) on up to 152 CTAs, one round for up to 38 distinct experts; above that +# the triples path (three 64-row sub-tiles per CTA, one round up to 57 distinct experts) with the early trigger, so +# the FC2 grid takes the SMs FC1 leaves and is resident when FC1 ends (calls on the regular path trigger after their +# tiles, which leaves those SMs to the shared expert); FC1 weights (read once per call) are loaded L2 evict-first. +DEFAULT_FC1 = ((64, 4), (152, 4)) +DEFAULT_FC2 = ((128, 3), (128, 3)) +DEFAULT_FLAGS = ( + FLAG_TMA | FLAG_PREFETCH_FC2, + FLAG_TMA + | FLAG_FC1_PAIRS + | FLAG_FC1_TRIPLES + | FLAG_EARLY_TRIGGER_TRIPLES + | FLAG_FC1_EVICT_FIRST, +) + + +def op_available() -> bool: + return hasattr(torch.ops.trtllm, OP_NAME) + + +def _pdl_enabled() -> bool: + return os.environ.get("TRTLLM_ENABLE_PDL", "1") != "0" + + +def launch_config(rows: int = 1) -> Tuple[Tuple[int, int], Tuple[int, int], int]: + """(FC1 ctas, warps), (FC2 ctas, warps) and the flag bits for calls with ``rows`` rows + (``DEFAULT_FC1``, ``DEFAULT_FC2``, ``DEFAULT_FLAGS``).""" + pick = 0 if rows == 1 else 1 + flags = DEFAULT_FLAGS[pick] & FLAG_OPTIONS + if _pdl_enabled(): + flags |= FLAG_PDL + fc1 = DEFAULT_FC1[pick] + # (The TMA-fed kernels choose their ring depth in the launcher.) + if ( + flags & FLAG_DEEP_RING + and not flags & FLAG_TMA + and rows * HIDDEN * 2 + fc1[1] * FC1_RING_CHUNKS_DEEP * 2048 > FC1_DYNAMIC_SMEM_BUDGET + ): + flags &= ~FLAG_DEEP_RING + fc2 = DEFAULT_FC2[pick] + if fc1[1] == 3 or (fc2[1] == 3 and not flags & FLAG_TMA): + raise ValueError("3 warps per CTA exist only for the TMA-fed FC2 kernel") + return fc1, fc2, flags + + +class _Scratch: + """Per-device scratch of the two kernels (routing record and intermediate).""" + + def __init__(self, device: torch.device): + self.h = torch.empty(SCRATCH_H_ELEMS, dtype=torch.bfloat16, device=device) + self.route = torch.empty(ROUTE_WORDS, dtype=torch.int32, device=device) + + +# One scratch per device, shared by every draft_routed_moe call on it: correct only while those +# calls are ordered on one stream (the MTP layer's draft steps run in sequence on the model's main +# stream, eagerly and in every captured graph, and graphs replay one at a time). Calls that may +# overlap on other streams would each need their own scratch. +_SCRATCH: Dict[int, _Scratch] = {} + + +def _scratch(device: torch.device, create: bool) -> Optional[_Scratch]: + key = device.index if device.index is not None else torch.cuda.current_device() + if key not in _SCRATCH and create: + _SCRATCH[key] = _Scratch(torch.device("cuda", key)) + return _SCRATCH.get(key) + + +def resolve_owner(module) -> Tuple[Optional[torch.nn.Module], str]: + """The leaf owning the BF16 expert parameters (``ConfigurableMoE.backend`` or the leaf).""" + backend = getattr(module, "backend", None) + owner = backend if isinstance(backend, torch.nn.Module) else module + if not isinstance(owner, torch.nn.Module): + return None, f"{type(module).__name__} is not a module" + if "w3_w1_weight" not in owner._parameters or "w2_weight" not in owner._parameters: + return None, f"{type(owner).__name__} owns no expert parameters" + return owner, "" + + +def _wrapper_reason(wrapper) -> str: + """Wrapper-level behaviour the draft_routed_moe call would skip; empty when none.""" + if getattr(wrapper, "comm", None) is not None: + return "MoE communication strategy" + if getattr(wrapper, "layer_load_balancer", None) is not None: + return "EPLB load balancer" + if getattr(wrapper, "enable_dwdp", False): + return "DWDP" + if getattr(wrapper, "unfinalized_combine_fn", None) is not None: + return "unfinalized_combine_fn" + if getattr(wrapper, "apply_router_weight_on_input", False): + return "router weight on input" + if getattr(wrapper, "_enable_perfect_router", False): + return "perfect router" + if getattr(wrapper, "reduce_results", False) and getattr(wrapper, "parallel_size", 1) > 1: + return "in-op all-reduce" + return "" + + +def _routing_reason(rm) -> str: + from tensorrt_llm._torch.moe.fused_moe.routing import DeepSeekV3MoeRoutingMethod + + if not isinstance(rm, DeepSeekV3MoeRoutingMethod): + return f"routing method {type(rm).__name__}" + impl = rm.routing_impl + groups = (impl.top_k, impl.n_group, impl.topk_group) + if groups != (TOP_K, 1, 1): + return f"routing {groups}" + scale = impl.routed_scaling_factor + if ( + scale is None + or not (float(scale) == float(scale)) + or float(scale) in (float("inf"), float("-inf")) + ): + return f"routed scaling {scale}" + return "" + + +def _owner_reason(owner, device: torch.device) -> str: + """Leaf-level eligibility: FlashInfer TRT-LLM-Gen BF16 leaf, TP shard with every expert local.""" + from tensorrt_llm._torch.moe.fused_moe.quantization import BF16TRTLLMGenFusedMoEMethod + from tensorrt_llm._torch.moe.fused_moe.trtllm_gen.flashinfer_bf16 import ( + FlashinferTrtllmGenBf16Impl, + ) + from tensorrt_llm._torch.utils import ActivationType + + if not isinstance(owner, FlashinferTrtllmGenBf16Impl): + return f"{type(owner).__name__} is not the FlashInfer TRT-LLM-Gen BF16 leaf" + if getattr(owner, "use_dp", False): + return "attention DP" + routes_outside = getattr(owner, "_routes_outside_the_kernel", None) + if routes_outside is None or routes_outside(): + return "separated routing" + if getattr(owner, "activation_type", None) != ActivationType.Swiglu: + return f"activation {getattr(owner, 'activation_type', None)}" + if getattr(owner, "bias", False): + return "expert bias" + for name in ("act_alpha", "act_beta", "act_clamp"): + if getattr(owner, name, None) is not None: + return f"{name} set" + if ( + getattr(owner, "num_slots", None) != NUM_EXPERTS + or getattr(owner, "slot_start", None) != 0 + or getattr(owner, "expert_size_per_partition", None) != NUM_EXPERTS + ): + return "not all 256 experts local (EP or EPLB slots)" + if getattr(owner, "hidden_size", None) != HIDDEN: + return f"hidden {getattr(owner, 'hidden_size', None)}" + if getattr(owner, "intermediate_size_per_partition", None) != INTER: + return f"intermediate {getattr(owner, 'intermediate_size_per_partition', None)}" + qm = getattr(owner, "quant_method", None) + if not isinstance(qm, BF16TRTLLMGenFusedMoEMethod): + return f"quant method {type(qm).__name__}" + if not getattr(qm, "use_shuffled_weight", False) or getattr(qm, "block_k", None) != K_CHUNK: + return "weights not shuffled into 64-element BlockMajorK blocks" + from tensorrt_llm._torch.moe.fused_moe.quantization import ( + TRTLLM_GEN_WEIGHT_LAYOUT_BLOCK_MAJOR_K, + ) + + if getattr(qm, "weight_layout", None) != TRTLLM_GEN_WEIGHT_LAYOUT_BLOCK_MAJOR_K: + return f"weight layout {getattr(qm, 'weight_layout', None)}" + if getattr(owner, "_trtllm_gen_layout_transform_pending", False): + return "weight layout transform pending" + w13 = owner._parameters["w3_w1_weight"] + w2 = owner._parameters["w2_weight"] + if ( + tuple(w13.shape) != (NUM_EXPERTS, HIDDEN // K_CHUNK, 2 * INTER, K_CHUNK) + or w13.dtype != torch.bfloat16 + ): + return f"w3_w1 {tuple(w13.shape)} {w13.dtype}" + if ( + tuple(w2.shape) != (NUM_EXPERTS, INTER // K_CHUNK, HIDDEN, K_CHUNK) + or w2.dtype != torch.bfloat16 + ): + return f"w2 {tuple(w2.shape)} {w2.dtype}" + for t in (w13, w2): + if not t.is_contiguous() or t.device != device: + return "expert buffers not contiguous on the activation's device" + return _routing_reason(getattr(owner, "routing_method", None)) + + +def eligibility( + module, routing_bias, x: torch.Tensor, router_logits: torch.Tensor, do_finalize: bool +) -> Tuple[bool, str]: + """Decide draft_routed_moe support for this call from host-side state only.""" + if not do_finalize: + return False, "unfinalized output requested" + if not torch.cuda.is_available() or x.device.type != "cuda": + return False, "not on CUDA" + major, _ = torch.cuda.get_device_capability(x.device) + if major != 10: + return False, f"SM{major}x is not the SM100 family" + if x.dtype != torch.bfloat16 or x.dim() != 2 or x.shape[1] != HIDDEN: + return False, f"activation {tuple(x.shape)} {x.dtype}" + rows = x.shape[0] + if rows < 1 or rows > MAX_ROWS: + return False, f"{rows} rows outside [1, {MAX_ROWS}]" + if not x.is_contiguous() or router_logits is None or not router_logits.is_contiguous(): + return False, "non-contiguous input" + if ( + router_logits.dtype != torch.float32 + or tuple(router_logits.shape) != (rows, NUM_EXPERTS) + or router_logits.device != x.device + ): + return False, f"logits {tuple(router_logits.shape)} {router_logits.dtype}" + if torch.compiler.is_compiling(): + return False, "torch.compile tracing" + if not op_available(): + return False, f"torch.ops.trtllm.{OP_NAME} is not built" + owner, reason = resolve_owner(module) + if owner is None: + return False, reason + if owner is not module: + reason = _wrapper_reason(module) + if reason: + return False, reason + reason = _owner_reason(owner, x.device) + if reason: + return False, reason + native_bias = getattr(getattr(owner, "routing_method", None), "e_score_correction_bias", None) + if ( + routing_bias is None + or routing_bias.dtype != torch.bfloat16 + or tuple(routing_bias.shape) != (NUM_EXPERTS,) + or not routing_bias.is_contiguous() + or routing_bias.device != x.device + ): + return False, "router bias is not a contiguous BF16 [256] on the device" + if native_bias is None or native_bias.data_ptr() != routing_bias.data_ptr(): + return False, "router bias is not the routing method's bias" + if torch.cuda.is_current_stream_capturing() and _scratch(x.device, create=False) is None: + # Allocating inside a capture would record the allocation into the graph; the model's eager warmup + # forwards create the scratch before capture. + return False, "not warmed up before CUDA graph capture" + return True, "" + + +def run(owner, routing_bias, x, router_logits, debug_fc2=None, timeline=None) -> torch.Tensor: + """Launch the two kernels (no eligibility check; ``owner`` is the BF16 leaf).""" + (c1, w1), (c2, w2), flags = launch_config(x.shape[0]) + scratch = _scratch(x.device, create=True) + # The kernels read the activation, the logits and the bias with 16-byte loads. + if x.data_ptr() % 16 != 0: + x = x.clone(memory_format=torch.contiguous_format) + if router_logits.data_ptr() % 16 != 0: + router_logits = router_logits.clone(memory_format=torch.contiguous_format) + if routing_bias.data_ptr() % 16 != 0: + routing_bias = routing_bias.clone(memory_format=torch.contiguous_format) + rm = owner.routing_method + return getattr(torch.ops.trtllm, OP_NAME)( + x, + router_logits, + routing_bias, + owner._parameters["w3_w1_weight"], + owner._parameters["w2_weight"], + scratch.h, + scratch.route, + float(rm.routing_impl.routed_scaling_factor), + c1, + w1, + c2, + w2, + flags, + debug_fc2, + timeline, + ) + + +def routed_output( + module, + routing_bias: torch.Tensor, + x: torch.Tensor, + router_logits: torch.Tensor, + do_finalize: bool, +) -> Optional[torch.Tensor]: + """Entry point of the MTP routed-MoE call site; ``None`` means "use the native path". + + ``module`` is the served ``ConfigurableMoE`` wrapper (or the bare BF16 leaf), ``routing_bias`` the gate's + ``e_score_correction_bias``. The decision is made before any device work. + """ + from ....shared import dispatch + + ok, reason = eligibility(module, routing_bias, x, router_logits, do_finalize) + dispatch.log_capability( + "draft_routed_moe", f"rows={x.shape[0]} {reason or 'engaged'}", ok, reason + ) + if not ok: + return None + owner, _ = resolve_owner(module) + return run(owner, routing_bias, x, router_logits) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/__init__.py new file mode 100644 index 000000000000..17274028d3bb --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/__init__.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Package hop_routing: native top-8 routing in the pre-MoE into-hop entry. + +Entry points registered with ``dispatch``: + +* ``make_transport(into_hop)`` returns the :class:`~.hop.HopRoutingTransport` of an + ``attention_output_into_hop`` transport (or None, the into-hop path). Call it at the same point + of model construction on every rank. +* ``engagement_counts()`` returns the host-side routing-hop call counters. +* ``routed_output_into_hop(module, routing_bias, x, router_logits, routing, + hop)`` and ``routed_output(module, routing_bias, x, router_logits, routing, + do_finalize)``: routed_moe's entries of the same names with the hop's routing + (``routing``, int32 ``[2, R, 8]``) in place of routed_moe's own; ``None`` means + "run routed_moe on ``router_logits``" (decided before any device work). +""" + +from ....shared.dispatch import register +from .hop import HopRoutingTransport, engagement_counts, op_available, split_routing +from .routed_moe import routed_output, routed_output_into_hop + +register("hop_routing", "make_transport", HopRoutingTransport.create) +register("hop_routing", "engagement_counts", engagement_counts) +register("hop_routing", "routed_output", routed_output) +register("hop_routing", "routed_output_into_hop", routed_output_into_hop) + +__all__ = [ + "HopRoutingTransport", + "engagement_counts", + "op_available", + "routed_output", + "routed_output_into_hop", + "split_routing", +] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/hop.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/hop.py new file mode 100644 index 000000000000..4f9e12391d26 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/hop.py @@ -0,0 +1,251 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Routing hop of hop_routing: the into-hop entry plus native top-8 routing. + +``HopRoutingTransport`` wraps the ``attention_output_into_hop`` :class:`IntoHopTransport` of a +``post_attention_hop``. The producer side is the into-hop entry's unchanged (the attention tail +publishes into the same transport); the hop runs ``trtllm::glm_moe_dsa_post_attention_hop_routing``, +which returns the into-hop entry's normalized hidden, residual and router logits bit for bit plus +the native DeepSeek-V3 routing of every row (expert ids int32 ``[R, 8]`` and +weights FP32 ``[R, 8]`` holding the bf16-rounded native weights, descending +native key order). See +``cpp/tensorrt_llm/kernels/glmMoeDsa/postAttentionHopRouting.h`` for the +arithmetic, the gather protocol and the memory-order contract. + +The gather buffers and the parity word are private to hop_routing and shared by every +layer that uses the same ``post_attention_hop`` (one TP group, one workspace); they are +allocated once, at model construction. +""" + +from typing import Dict, Optional, Tuple + +import torch + +from ....shared.dispatch import log_capability + +FEATURE = "hop_routing" +_OP_NAME = "glm_moe_dsa_post_attention_hop_routing" +HIDDEN_SIZE = 6144 +NUM_EXPERTS = 256 +TOP_K = 8 +MAX_ROWS = 8 +NUM_RANKS = 8 +ROUTED_SCALING = 2.5 +GATHER_WORDS = 2 * MAX_ROWS * NUM_EXPERTS +STATE_WORDS = 4 +NUM_STAMPS = 8 +MAX_GRID_CTAS = 96 +# The empty gather slot (0x80000000 as int32). +GATHER_SENTINEL = -(1 << 31) + +_counts: Dict[str, int] = {} + + +def _count(key: str) -> None: + _counts[key] = _counts.get(key, 0) + 1 + + +def engagement_counts() -> Dict[str, int]: + """Copy of the host-side call counters (eager calls and CUDA graph + captures, not replays): ``hop_routing_hop`` counts routing-hop calls.""" + return dict(_counts) + + +def reset_engagement_counts() -> None: + _counts.clear() + + +def op_available(op_name: str = _OP_NAME) -> bool: + """Whether ``trtllm::`` (default: the routing hop) is loaded.""" + return hasattr(torch.ops.trtllm, op_name) + + +def _agreed_op_reason(hop, op_name: str = _OP_NAME) -> str: + """Why no rank of ``hop``'s group may use the routing hop ("" when every + rank may). Agreed once per hop and op over the hop's TP communicator.""" + cache = hop.__dict__.setdefault("_hop_routing_op_reason", {}) + if op_name in cache: + return cache[op_name] + local = "" if op_available(op_name) else f"trtllm::{op_name} is not loaded" + comm = getattr(hop, "comm", None) + agreed = comm is None or comm.allreduce(0 if local else 1) == comm.Get_size() + reason = local or ("" if agreed else f"trtllm::{op_name} is not loaded on another rank") + cache[op_name] = reason + return reason + + +class _GatherState: + """Gather buffers and parity of one ``post_attention_hop`` workspace.""" + + def __init__(self, device: torch.device): + self.gather = torch.full((GATHER_WORDS,), GATHER_SENTINEL, dtype=torch.int32, device=device) + self.zstate = torch.zeros(STATE_WORDS, dtype=torch.int32, device=device) + + +def _gather_state(hop) -> _GatherState: + state = getattr(hop, "_hop_routing_gather_state", None) + if state is None: + state = _GatherState(hop.flags.device) + hop._hop_routing_gather_state = state + return state + + +class HopRoutingTransport: + """into-hop producer view plus the routing hop.""" + + def __init__(self, into_hop, op_name: str = _OP_NAME): + self.into_hop = into_hop + self.hop = into_hop.hop + self.rank = into_hop.rank + self.flags = into_hop.flags + self.mc_words = into_hop.mc_words + state = _gather_state(self.hop) + self.gather = state.gather + self.zstate = state.zstate + # Same schedule knobs as the into-hop entry (the hop arithmetic and protocol are the + # into-hop entry's). + self.early_poll = into_hop.early_poll + self.router_load = into_hop.router_load + # Tests may use the diagnostic build (``..._diag``, stamps). + self.op_name = op_name + + @classmethod + def create(cls, into_hop, op_name: str = _OP_NAME) -> Optional["HopRoutingTransport"]: + """The routing hop of ``into_hop``'s hop, or None (into-hop path). + + Rank-uniform: ``into_hop`` exists on all ranks of the group or on none, and + the op's availability is agreed over the hop's TP group the first + time, at the same point of model construction on every rank. + """ + reason = ( + "attention_output_into_hop transport unavailable" + if into_hop is None + else _agreed_op_reason(into_hop.hop, op_name) + ) + log_capability(FEATURE, "routing_hop", not reason, reason) + return None if reason else cls(into_hop, op_name) + + def can_run( + self, + residual: torch.Tensor, + norm_weight: torch.Tensor, + router_weight: Optional[torch.Tensor], + routing_bias: Optional[torch.Tensor], + ) -> bool: + """Host-only, rank-uniform eligibility of one call: the into-hop entry's, with router + logits, and a contiguous bf16 ``[256]`` router bias on the device.""" + if router_weight is None or routing_bias is None: + return False + if ( + routing_bias.dtype != torch.bfloat16 + or tuple(routing_bias.shape) != (NUM_EXPERTS,) + or not routing_bias.is_contiguous() + or routing_bias.device != residual.device + ): + return False + return self.into_hop.can_run(residual, norm_weight, router_weight) + + def __call__( + self, + residual: torch.Tensor, + norm_weight: torch.Tensor, + eps: float, + router_weight: torch.Tensor, + routing_bias: torch.Tensor, + logits_in: Optional[torch.Tensor] = None, + stamps: Optional[torch.Tensor] = None, + ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: + """Run the routing hop on the partials the producer already published. + + Returns ``(normed, residual_out, router_logits, routing)`` where + ``routing`` is int32 ``[2, R, 8]``: ``routing[0]`` the expert ids and + ``routing[1]`` the FP32 bits of the weights (see :func:`split_routing`). + ``logits_in`` (tests only) replaces the logits the routing reads. + """ + _count("hop_routing_hop") + normed, residual_out, logits, routing = getattr(torch.ops.trtllm, self.op_name)( + residual.contiguous(), + norm_weight.contiguous(), + float(eps), + router_weight, + routing_bias, + self.hop.uc_buffer, + self.flags, + self.gather, + self.zstate, + self.rank, + NUM_RANKS, + self.early_poll, + self.router_load, + ROUTED_SCALING, + logits_in, + stamps, + ) + return normed, residual_out, logits, routing + + +def split_routing(routing: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + """``(ids int32 [R, 8], weights fp32 [R, 8])`` views of a routing tensor.""" + return routing[0], routing[1].view(torch.float32) + + +def _register_fake() -> None: + if not op_available(): + return + try: + + @torch.library.register_fake(f"trtllm::{_OP_NAME}") + def _( + residual, + gamma, + eps, + router_weight, + routing_bias, + uc_buffer, + flags, + gather, + zstate, + rank, + nranks, + early_poll, + router_load, + routed_scaling, + logits_in=None, + stamps=None, + ): + rows = residual.numel() // HIDDEN_SIZE + return [ + torch.empty_like(residual), + torch.empty_like(residual), + residual.new_empty((rows, NUM_EXPERTS), dtype=torch.float32), + residual.new_empty((2, rows, TOP_K), dtype=torch.int32), + ] + except RuntimeError: + # Already registered by an earlier import in this process. + pass + + +_register_fake() + +__all__ = [ + "GATHER_SENTINEL", + "NUM_STAMPS", + "MAX_GRID_CTAS", + "HopRoutingTransport", + "engagement_counts", + "op_available", + "reset_engagement_counts", + "split_routing", +] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/routed_moe.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/routed_moe.py new file mode 100644 index 000000000000..391112f8835d --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/hop_routing/routed_moe.py @@ -0,0 +1,316 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""routed_moe fork of package hop_routing: the routed MoE consuming the hop's routing. + +The expert-cluster kernel of routed_moe (``routed_moe/kernel_xc.py``) with its routing +phase replaced: instead of loading the router logits and selecting the top 8 +(one warp per token), warp ``t`` reads token ``t``'s eight expert ids and +weights that the hop_routing hop wrote, fills the same shared-memory tables +(``s_topk_id``, ``s_topk_w`` and the expert bitmap) and continues exactly as +routed_moe. Given the same routing, every later step (slot tables, schedule, FC1, +SwiGLU, quantization, FC2, the outputs of each mode) is routed_moe's code, so the +numerics are routed_moe's. + +The routing operand is the hop's int32 ``[2, R, 8]`` tensor (ids, then the +FP32 bits of the weights; see ``hop.split_routing``). It is passed in routed_moe's +router-logits address slot; the weights start ``R * 8`` words after the ids. +It is produced on the stream by the hop_routing hop and read only after +``griddepcontrol.wait`` (routed_moe's own memory-order contract, see +``routed_moe/routed_moe.py``). + +Before the wait nothing else changes, except under package cross_layer_expert_prefetch's speculative +prefetch, which the fork compiles in from the runner key exactly as routed_moe's +runner does (``spec_k``, ``spec_ctas``, ``spec_self`` of ``key[6]``). Then the +TMA warp of each issuing CTA also reads the layer's row of predicted expert +IDs (cross_layer_expert_prefetch's int32 row of ``SPEC_KMAX`` entries) with relaxed loads and issues +L2 bulk prefetches of those experts' weights, which are model parameters +that nothing writes while serving. With cross_layer_expert_prefetch's predictor ``self`` +(``spec_experts.SERVED_CONFIG``) the row has one writer: CTA 0's MMA warp of the +same layer's fork, which stores the call's distinct experts ranked by row count +once the routing tables are +built, i.e. after that call's own dependency wait. A call's pre-wait read +therefore sees the row its layer's previous call stored (that call completed +before this launch, in stream order behind the intervening dependency +waits), or, on a CTA that becomes resident after this call's CTA 0 has +stored, this call's own experts. Either value only selects which immutable +bytes are prefetched (each ID is masked into [0, 256)), never what the +kernel reads or computes. + +Entry points (``None`` means "use the routed_moe path with the logits"; the +decision is host-only and precedes any device work): + +* ``routed_output_into_hop(module, bias, x, logits, routing, hop)``: routed_moe_into_hop, + every routed-input mode of the post_moe_hop; +* ``routed_output(module, bias, x, logits, routing, do_finalize)``. + +Only the expert-cluster schedule is forked; a call that routed_moe would run with +its grid-wide schedule (``v6``) declines. +""" + +import threading +from typing import Dict, List, Optional + +import torch + +from ..routed_moe import routed_moe as routed_moe_mod + +FEATURE = "hop_routing" +TOP_K = routed_moe_mod.TOP_K +MAX_ROWS = routed_moe_mod.MAX_ROWS + +_LOCK = threading.Lock() +_COMPILED: Dict[tuple, object] = {} +_KERNEL_CLASS = None + + +def _kernel_class(): + """``HopRoutedExpertClusterKernel``, defined on first use (imports the CuTe DSL).""" + global _KERNEL_CLASS + if _KERNEL_CLASS is not None: + return _KERNEL_CLASS + import cutlass + import cutlass.cute as cute + from cutlass._mlir.dialects import llvm + from cutlass.cutlass_dsl import Int32, Int64, T, Uint32, dsl_user_op + + from ..routed_moe import ptx + from ..routed_moe.kernel_xc import RoutedMoeExpertClusterKernel + + @dsl_user_op + def ld_nc_u32(addr: Int64, *, loc=None, ip=None) -> Uint32: + return Uint32( + llvm.inline_asm( + T.i32(), + [Int64(addr).ir_value(loc=loc, ip=ip)], + "ld.global.nc.u32 $0, [$1];", + "=r,l", + has_side_effects=True, + is_align_stack=False, + asm_dialect=llvm.AsmDialect.AD_ATT, + loc=loc, + ip=ip, + ) + ) + + class HopRoutedExpertClusterKernel(RoutedMoeExpertClusterKernel): + """routed_moe's expert-cluster kernel reading the routing instead of computing it.""" + + @cute.jit + def _route_tokens( + self, w, lane, num_tokens, logits_addr, bias_r, s_topk_id, s_topk_w, storage + ): + """Warp ``w`` takes token ``w``'s ids and weights from the hop's + routing (``logits_addr``: ids ``[R, 8]`` int32, then weights + ``[R, 8]`` FP32) and marks the experts in the bitmap.""" + if w < num_tokens: + if lane < TOP_K: + idx = w * TOP_K + lane + e = Int32(ld_nc_u32(logits_addr + idx * 4)) + wt = ptx.ld_nc_f32(logits_addr + (num_tokens * TOP_K + idx) * 4) + s_topk_id[idx] = e + s_topk_w[idx] = wt + ptx.atom_or_shared_u32( + Int32(storage.bitmap.data_ptr().toint()) + (e // 32) * 4, + Uint32(1) << Uint32(e % 32), + ) + + del cutlass + _KERNEL_CLASS = HopRoutedExpertClusterKernel + return HopRoutedExpertClusterKernel + + +class HopRoutedMoe(routed_moe_mod.RoutedMoe): + """routed_moe runner whose expert-cluster kernel consumes the hop's routing.""" + + def ready_for_capture( + self, finalize: bool, debug: int = 0, rows: int = MAX_ROWS, red: bool = False + ) -> bool: + key = self._key(finalize, debug, rows, red) + dev = self.device.index if self.device.index is not None else torch.cuda.current_device() + ws = routed_moe_mod._WORKSPACES.get(dev) + return key in _COMPILED and ws is not None and ws.num_ctas >= self.num_ctas + + def _compiled(self, finalize: bool, debug: int, rows: int = MAX_ROWS, red: bool = False): + """routed_moe's compiled launchers with ``HopRoutedExpertClusterKernel`` (expert clusters only).""" + key = self._key(finalize, debug, rows, red) + if key[3] != "xc": + raise ValueError("the hop_routing fork runs the expert-cluster schedule only") + with _LOCK: + fns = _COMPILED.get(key) + if fns is not None: + return fns + import cutlass + import cutlass.cute as cute + from cuda.bindings import driver as cuda + from cutlass.cute.runtime import make_ptr + + from ..routed_moe.kernel_xc import DELAY_FINALIZE, RoutedMoeFinalizeKernel + + ptrs = self._pointers(make_ptr, cute, cutlass) + stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream) + inkernel = finalize and self.inkernel_finalize() + # Package cross_layer_expert_prefetch's speculative expert prefetch + # (``RoutedMoe._key``). + spec = key[6] or (0, 0, False) + kern = _kernel_class()( + self._xc_ctas(rows, finalize, red), + debug=debug, + finalize=inkernel, + red_out=red, + **routed_moe_mod.xc_kernel_args(), + spec_k=spec[0], + spec_ctas=spec[1], + spec_self=spec[2], + ) + ints = [cutlass.Int64(0)] * routed_moe_mod._NUM_XC_ADDRS + routed = cute.compile(kern.run, *ptrs, *ints, cutlass.Int32(1), stream) + if spec[0]: + from ....shared import dispatch + + dispatch.log_capability( + FEATURE, + f"expert_prefetch[k={spec[0]},ctas={spec[1]},self={int(spec[2])}]", + True, + ) + fin = None + if finalize and not inkernel and not red: + delay, delay_ns = routed_moe_mod.DELAY_INJECTION + fin = cute.compile( + RoutedMoeFinalizeKernel(delay_ns=delay_ns if delay & DELAY_FINALIZE else 0).run, + cutlass.Int64(0), + cutlass.Int64(0), + cutlass.Int64(0), + cutlass.Int32(1), + stream, + ) + fns = (routed, fin) + _COMPILED[key] = fns + return fns + + +def routing_reason(routing: Optional[torch.Tensor], x: torch.Tensor) -> str: + """Why ``routing`` cannot feed the fork for activation ``x`` ("" if it can).""" + if routing is None: + return "no routing from the hop" + rows = x.shape[0] + if ( + routing.dtype != torch.int32 + or tuple(routing.shape) != (2, rows, TOP_K) + or not routing.is_contiguous() + or routing.device != x.device + ): + return f"routing {tuple(routing.shape)} {routing.dtype}" + return "" + + +def _engaged_runner( + module, + routing_bias: torch.Tensor, + x: torch.Tensor, + router_logits: torch.Tensor, + routing: Optional[torch.Tensor], + do_finalize: bool, + red: bool = False, +) -> Optional[HopRoutedMoe]: + """The fork's runner for this call, or ``None`` (host-only decision).""" + from ....shared import dispatch + + reason = routing_reason(routing, x) + ok = not reason + if ok: + # routed_moe's own eligibility (shapes, dtypes, device, routing method, expert owner). + ok, reason = routed_moe_mod.eligibility(module, x, router_logits) + owner = routed_moe_mod.resolve_expert_owner(module)[0] if ok else None + if ok: + rm = getattr(owner, "routing_method", None) + native_bias = getattr(rm, "e_score_correction_bias", None) + if ( + routing_bias is None + or routing_bias.dtype != torch.bfloat16 + or tuple(routing_bias.shape) != (routed_moe_mod.NUM_EXPERTS,) + or not routing_bias.is_contiguous() + or routing_bias.device != x.device + ): + ok, reason = False, "router bias is not a contiguous BF16 [256] on the device" + elif native_bias is not None and native_bias.data_ptr() != routing_bias.data_ptr(): + ok, reason = False, "router bias is not the routing method's bias" + runner = getattr(owner, "_hop_routing_runner", None) if ok else None + if ok and runner is not None and runner.bias.data_ptr() != routing_bias.data_ptr(): + runner = None + if ok and runner is None: + runner = HopRoutedMoe(owner, routing_bias) + rows = x.shape[0] + if ok and runner.schedule(rows, do_finalize, red) != "xc": + ok, reason = False, "routed_moe would run its grid-wide schedule" + if ok and torch.cuda.is_current_stream_capturing(): + if getattr(owner, "_hop_routing_runner", None) is None or not runner.ready_for_capture( + do_finalize, rows=rows, red=red + ): + ok, reason = False, "not warmed up before CUDA graph capture" + dispatch.log_capability( + FEATURE, f"routed_moe_fork rows={rows} {reason or 'engaged'}", ok, reason + ) + if not ok: + return None + owner._hop_routing_runner = runner + return runner + + +def routed_output( + module, + routing_bias: torch.Tensor, + x: torch.Tensor, + router_logits: torch.Tensor, + routing: Optional[torch.Tensor], + do_finalize: bool, +) -> Optional[List[torch.Tensor]]: + """routed_moe's ``routed_output`` with the hop's routing; ``None``: use routed_moe.""" + runner = _engaged_runner(module, routing_bias, x, router_logits, routing, do_finalize) + if runner is None: + return None + return runner(x, routing, do_finalize=do_finalize) + + +def routed_output_into_hop( + module, + routing_bias: torch.Tensor, + x: torch.Tensor, + router_logits: torch.Tensor, + routing: Optional[torch.Tensor], + hop, +) -> Optional[List[torch.Tensor]]: + """routed_moe's ``routed_output_into_hop`` (routed_moe_into_hop) with the hop's routing; same + modes and returns. ``None``: use routed_moe.""" + if hop is None or not getattr(hop, "_routed_enabled", False): + return None + rows = x.shape[0] + if not hop.can_run(rows): + return None + mode = getattr(hop, "routed_mode", "row") + if mode == "row": + runner = _engaged_runner(module, routing_bias, x, router_logits, routing, True) + if runner is None: + return None + target = hop.routed_input_buffer(rows) + return [runner(x, routing, do_finalize=True, out=target)] + red = mode == "fp32" + runner = _engaged_runner(module, routing_bias, x, router_logits, routing, False, red) + if runner is None: + return None + if red: + return runner(x, routing, red_out=hop.routed_acc_buffer(rows)) + rows_buf, scales_buf = hop.routed_raw_buffers(rows) + runner(x, routing, do_finalize=False, raw_out=(rows_buf, scales_buf)) + return [rows_buf] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/__init__.py new file mode 100644 index 000000000000..f06123668cf5 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/__init__.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""routed_moe: routed NVFP4 MoE (the routed_moe contract) as one CuTe DSL launch. + +Registered entry points (``dispatch.get_impl("routed_moe", name)``): + +* ``routed_output(module, routing_bias, x, router_logits, do_finalize)`` -> + finalized ``[R, 6144]`` BF16, the native unfinalized triple, or ``None`` + when the call is ineligible (the caller then runs the native path; the + decision is taken before any device work). ``module`` is the served + ``ConfigurableMoE`` (``Deepseekv3MoE.experts``) or its TRT-LLM-Gen NVFP4 + leaf. +* ``eligibility(module, x, router_logits)`` -> ``(bool, reason)``. +* ``routed_output_into_hop(module, routing_bias, x, router_logits, hop)`` + (used only when the ``routed_moe_into_hop`` feature is selected): the finalized routed + output written into the post_moe_hop's routed-input buffer, as ``[routed]``, or + ``None``. +""" + +from ....shared import dispatch +from .routed_moe import eligibility, routed_output, routed_output_into_hop + +dispatch.register("routed_moe", "routed_output", routed_output) +dispatch.register("routed_moe", "eligibility", eligibility) +dispatch.register("routed_moe", "routed_output_into_hop", routed_output_into_hop) + +__all__ = ["eligibility", "routed_output", "routed_output_into_hop"] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/kernel.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/kernel.py new file mode 100644 index 000000000000..f252c60a3f2f --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/kernel.py @@ -0,0 +1,1987 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Single-launch routed NVFP4 MoE for small-M decode (CuTe DSL, SM100 family). + +One persistent kernel computes, for R <= 8 rows of one rank's TP shard of a +256-expert / top-8 / intermediate-256 / hidden-6144 NVFP4 MoE: + +1. routing (sigmoid + bias top-8, scaled-sum normalization), recomputed by + every CTA from the FP32 router logits, so no CTA waits for another; +2. NVFP4 quantization of the activation rows (bulk-copied to shared memory + before any weight traffic) into the MMA B operand, all R rows: unused + columns are masked in the epilogues; +3. FC1 (gate/up) as a split-K partition of all (expert, 128-row tile, + 256-element k-block) work over the grid: TMA streams the stock weight and + scale buffers through a 4-stage ring straight into tcgen05 block-scaled + MMAs (M=128 weight rows, N=8 rows of tokens, K=64 per instruction); +4. the FC1 epilogue (split-K reduction, SwiGLU, NVFP4 quantization of the + intermediate) publishes a per-expert FC2 operand; +5. FC2 as a split partition of (128-row output tile, expert) units; its + weight TMA starts as soon as the CTA's FC1 loads are issued; +6. the top-8 combine (finalize) in the FC2 epilogue, or the native + unfinalized tuple. + +Warp roles: 0-3 quantize, then run the epilogues; 4 issues MMAs; 5 issues +TMA; 6-7 quantize, then fetch the FC2 operands. + +Cross-CTA hand-offs are kept to one round trip on the consumer side. A +finished FC1 tile is stored straight into its expert's FC2 operand image +(the exact shared-memory layout of one B / SFB stage) and published with an +epoch flag; each consumer CTA waits for the flags of its experts and fetches +every image with two bulk copies that complete on a per-expert mbarrier. +Split-tile partials move as LL words (8 bytes ``{payload, epoch}`` per +16-byte store, polled by the reader). A split tile is reduced by a fixed +owner (the CTA holding its first k-block or expert, which it processes last) +in CTA order, so results are deterministic. Each CTA keeps its own launch +counter as the epoch; all CTAs of a launch hold the same value without +synchronizing. The weight stream is loaded with an evict-first L2 policy. + +The weight buffers are consumed in the stock TRT-LLM-Gen layout: gate/up rows +interleaved and shuffled in 32-row blocks, scale factors in the 128x4 +interleaved layout. The epilogues undo the row shuffle with index arithmetic. +""" + +import cutlass +import cutlass.cute as cute +import cutlass.pipeline as pipeline +import cutlass.utils as utils +import cutlass.utils.blackwell_helpers as sm100_utils +import cutlass.utils.blockscaled_layout as blockscaled_utils +from cutlass.cute.nvgpu import tcgen05 +from cutlass.cutlass_dsl import Float32, Int32, Int64, Uint32 + +from . import ptx + +HIDDEN = 6144 +INTER = 256 +NUM_EXPERTS = 256 +TOP_K = 8 +NTOK = 8 +KTILE = 256 +FC1_KB = HIDDEN // KTILE # 24 k-blocks per FC1 tile +FC1_MT = 2 * INTER // 128 # 4 FC1 tiles per expert +FC2_MT = HIDDEN // 128 # 48 FC2 tiles +MAX_SLOTS = NTOK * TOP_K # 64 distinct experts at most +MAX_LOCAL_SLOTS = 24 # FC2 operand cache slots per CTA +ROUTED_SCALING = 2.5 + +# Stock weight geometry in bytes (L2 prefetch address arithmetic). +W13_ROW_BYTES = HIDDEN // 2 +W13_EXPERT_BYTES = 2 * INTER * W13_ROW_BYTES +W13_SF_TILE_BYTES = 128 * HIDDEN // 16 +W13_SF_EXPERT_BYTES = FC1_MT * W13_SF_TILE_BYTES +W13_SF_KB_BYTES = 128 * KTILE // 16 +KB_ROW_BYTES = KTILE // 2 +W2_TILE_BYTES = 128 * INTER // 2 +W2_EXPERT_BYTES = HIDDEN * INTER // 2 +W2_SF_TILE_BYTES = 128 * INTER // 16 +W2_SF_EXPERT_BYTES = HIDDEN * INTER // 16 + +# Workspace: one launch-counter (epoch) word per CTA. +WS_WORDS = 1024 +# LL exchange buffers (8-byte words {payload u32, epoch u32}). +LL_PART_BYTES = 128 * NTOK * 8 # one 128x8 FP32 partial +# Per-expert FC2 operand, stored as the exact shared-memory image of one B +# stage (8 rows x 128 B FP4, 16-byte chunks swizzled by row) followed by one +# compact SFB stage, so a consumer fetches it with two bulk copies. +HIMG_B_BYTES = NTOK * INTER // 2 +HIMG_SF_BYTES = 4 * 128 +HIMG_BYTES = HIMG_B_BYTES + HIMG_SF_BYTES +# LL form of the same operand (hand-off mode 1): 8-byte {4 payload bytes, +# epoch} words; words n*32 + j hold FP4 bytes 4j..4j+3 of row n, words +# 256 + n*4 + mt hold tile mt's four scale bytes of row n. +H_LL_DATA_WORDS = NTOK * INTER // 8 +H_LL_WORDS = H_LL_DATA_WORDS + NTOK * FC1_MT +H_LL_BYTES = H_LL_WORDS * 8 +H_LL_ITEMS = H_LL_BYTES // 16 +H_LL_DATA_ITEMS = H_LL_DATA_WORDS // 2 +HBUF_BYTES = max(HIMG_BYTES, H_LL_BYTES) # per-expert hand-off buffer +# Shared staging of one FC1 tile's output: 8 rows x 32 FP4 bytes, then +# 8 rows x 4 scale bytes. +HSTAGE_SF = NTOK * 32 +HSTAGE_BYTES = HSTAGE_SF + NTOK * 4 +MAX_PEERS = 7 # other CTAs contributing to one split tile +GATHER_PEERS = 4 # peers' partials fetched per round trip + +# Warp roles +EPI_WARPS = 4 +MMA_WARP = 4 +TMA_WARP = 5 +NUM_WARPS = 8 +NUM_THREADS = 32 * NUM_WARPS +QUANT_WARPS = 6 # warps 0-3 and 6-7 quantize +GATHER_WARP0 = 6 # warps 6-7 fetch the FC2 operands +GATHER_THREADS = 64 + +BAR_QUANT = 3 # quantizing warps (debug dump only) +BAR_EPI = 4 # warps 0-3 +BAR_GATHER = 5 # warps 6-7 + +ACC_STAGES = 4 +TMEM_COLS = 128 +TMEM_SFA_COL = 64 +TMEM_SFB_COL = 80 +# Compact SFB: only token rows 0..7 of each 128-row scale chunk are read by +# the N=8 MMA, so consecutive 64-K chunks are stored 128 B apart (overlapping +# the unused rows) instead of 512 B. +SFB_CHUNK_STRIDE = 128 +SFB_STAGE_STRIDE = 4 * SFB_CHUNK_STRIDE +SFB_BYTES = (MAX_LOCAL_SLOTS - 1) * SFB_STAGE_STRIDE + 3 * SFB_CHUNK_STRIDE + 512 +B2_BAR = FC1_KB # first of MAX_LOCAL_SLOTS FC2 operand barriers +X_BAR = B2_BAR + MAX_LOCAL_SLOTS # the staged activation rows +FC1_DONE_BAR = X_BAR + 1 # all FC1 MMAs of this CTA have completed +STG_BAR = FC1_DONE_BAR + 1 # LL operand staged in sX (mode 1), per slot +NUM_BAR = STG_BAR + MAX_LOCAL_SLOTS +A_STAGE_BYTES = 128 * KTILE // 2 +SFA_STAGE_BYTES = 128 * KTILE // 16 +# L2 eviction policy for the weight stream (read once): evict first, so the +# kernel's code, activations and exchange buffers stay in L2. +L2_EVICT_FIRST = 0x12F0000000000000 +X_ROW_BYTES = HIDDEN * 2 + +# L2 prefetch modes (bit mask, ``RoutedMoeKernel(prefetch=...)``; off in this target) +PF_FC1 = 1 # rest of this CTA's FC1 rows, once the ring is primed +PF_FC2_EARLY = 2 # this CTA's FC2 tiles, right after the FC1 prefetch +PF_FC2_LATE = 4 # this CTA's FC2 tiles, after the last FC1 TMA + +# Shared misc words +MISC_NUM_SLOTS = 0 + +DEBUG_ROUTING = 1 +DEBUG_QUANT = 2 +DEBUG_FC1 = 4 +DEBUG_TIMELINE = 8 + +# Stress-test delay injection sites (``delay`` bits; off in production). A +# pseudo-random third of the CTAs, re-drawn every launch from the epoch, +# spins 1..6 units of ``delay_ns`` on the global timer at each enabled site. +DELAY_ENTRY = 1 # whole CTA, right after the launch epoch is read +DELAY_PRODUCER = 2 # before publishing an FC1 operand tile or a split partial +DELAY_OWNER = 4 # split-tile owner, before it fetches its peers' partials +DELAY_CONSUMER = 8 # FC2 operand fetch, before polling the FC1 flags +TL_BYTE_BASE = 1600000 +TL_SLOTS = 48 +# Stamps are SM clock cycles; slot TL_GT0 holds the global timer read next +# to the entry stamp so the host can place every CTA on one time axis. +TL_GT0 = TL_SLOTS - 1 +( + TL_ENTRY, + TL_PDL, + TL_ROUTED, + TL_Q_START, + TL_Q_G0_COMPUTE, + TL_Q_G0_FENCE, + TL_Q_G1_FENCE, + TL_Q_G2_FENCE, + TL_Q_DONE, + TL_MMA_FC1_START, + TL_TMA_FC1, + TL_PF_ISSUED, + TL_MMA_FC1_DONE, + TL_EPI_FIRST_ACC, + TL_EPI_FC1_GATHERED, + TL_EPI_FC1_DONE, + TL_FC2_LL_ISSUED, + TL_FC2_LL_READY, + TL_B2_READY, + TL_TMA_FC2, + TL_MMA_FC2_DONE, + TL_FC2_FIRST_ACC, + TL_FC2_GATHERED, + TL_FC2_DONE, + TL_EPI_START, + TL_FC1_MMA_COMPLETE, + TL_FC2_SENTINELS, + TL_EPI_T_COMPUTED, + TL_EPI_T_STORED, + TL_EPI_T_TMEM, + TL_EPI_T_PARAMS, + TL_G_STS_DONE, + TL_G_FENCE_DONE, + TL_G_BAR0_DONE, + TL_FC2_LAST_SLOT, +) = range(35) + + +def _stamp(enabled, tl_s, idx): + # Timeline stamps go to shared memory (a global store here would wait in + # the same queues the stamps are meant to measure) and are flushed once + # at the end of the kernel. + if enabled: + ptx.sts_u64(tl_s + idx * 8, ptx.clock64()) + + +class RoutedMoeKernel: + """CuTe DSL kernel object; one compiled instance per configuration.""" + + def __init__( + self, + num_ctas: int, + finalize: bool = True, + num_stages: int = 4, + prefetch: int = 0, + min_fc1_kb: int = 12, + acc_stages: int = 4, + handoff: int = 0, + delay: int = 0, + delay_ns: int = 2000, + debug: int = 0, + ): + assert num_ctas <= WS_WORDS + # A split FC1 tile spans at most ceil(24 / min_fc1_kb) + 1 CTAs. + assert (FC1_KB + min_fc1_kb - 1) // min_fc1_kb <= MAX_PEERS + self.num_ctas = num_ctas + self.finalize = finalize + self.num_stages = num_stages + self.prefetch = prefetch + self.min_fc1_kb = min_fc1_kb + # TMEM: acc_stages x 8 accumulator columns, then the scale columns + assert acc_stages * NTOK <= TMEM_SFA_COL + self.acc_stages = acc_stages + # FC1 -> FC2 operand hand-off: 0 = operand image + per-tile release + # flag, bulk-copied into place; 1 = LL words (no producer fence), + # bulk-copied into the freed activation buffer and validated there + self.handoff = handoff + # Stress testing only: DELAY_* site bits and the delay unit + self.delay = delay + self.delay_ns = delay_ns + self.debug = debug + self.sf_vec_size = 16 + self.acc_dtype = Float32 + self.cta_group = tcgen05.CtaGroup.ONE + self.mma_inst_mn = (128, NTOK) + self.mma_tiler = (128, NTOK, KTILE) + + def _setup(self): + self.tiled_mma = sm100_utils.make_blockscaled_trivial_tiled_mma( + cutlass.Float4E2M1FN, + tcgen05.OperandMajorMode.K, + tcgen05.OperandMajorMode.K, + cutlass.Float8E4M3FN, + self.sf_vec_size, + self.cta_group, + self.mma_inst_mn, + ) + self.cluster_layout_vmnk = cute.tiled_divide( + cute.make_layout((1, 1, 1)), (self.tiled_mma.thr_id.shape,) + ) + self.a_smem_layout_staged = sm100_utils.make_smem_layout_a( + self.tiled_mma, self.mma_tiler, cutlass.Float4E2M1FN, self.num_stages + ) + self.sfa_smem_layout_staged = blockscaled_utils.make_smem_layout_sfa( + self.tiled_mma, self.mma_tiler, self.sf_vec_size, self.num_stages + ) + # The B operand is resident: one "stage" per FC1 k-block (FC1) and + # one per local expert slot (FC2); both need 24. + self.b_smem_layout_staged = sm100_utils.make_smem_layout_b( + self.tiled_mma, self.mma_tiler, cutlass.Float4E2M1FN, MAX_LOCAL_SLOTS + ) + # Standard SFB layout (defines the TMEM layout) and the compact SMEM + # layout actually used for storage (see SFB_CHUNK_STRIDE). + self.sfb_smem_layout_std = blockscaled_utils.make_smem_layout_sfb( + self.tiled_mma, self.mma_tiler, self.sf_vec_size, MAX_LOCAL_SLOTS + ) + self.sfb_smem_layout_staged = cute.make_layout( + self.sfb_smem_layout_std.shape, + stride=((((16, 4), 0), (0, 1)), 0, SFB_CHUNK_STRIDE, SFB_STAGE_STRIDE), + ) + self.num_tma_load_bytes = A_STAGE_BYTES + SFA_STAGE_BYTES + + @cute.jit + def run( + self, + w13_ptr: cute.Pointer, + w13_sf_ptr: cute.Pointer, + w2_ptr: cute.Pointer, + w2_sf_ptr: cute.Pointer, + x_addr: Int64, + logits_addr: Int64, + bias_addr: Int64, + alpha1_addr: Int64, + scale_c1_addr: Int64, + alpha2_addr: Int64, + xgs_addr: Int64, + out_addr: Int64, + raw_scale_addr: Int64, + raw_idx_addr: Int64, + ws_addr: Int64, + part_addr: Int64, + himg_addr: Int64, + hflag_addr: Int64, + dbg_addr: Int64, + w13_addr: Int64, + w13_sf_addr: Int64, + w2_addr: Int64, + w2_sf_addr: Int64, + num_tokens: Int32, + stream, + ): + self._setup() + print(f"[routed_moe] a_smem {self.a_smem_layout_staged}") + print(f"[routed_moe] b_smem {self.b_smem_layout_staged}") + print(f"[routed_moe] sfa_smem {self.sfa_smem_layout_staged}") + print(f"[routed_moe] sfb_smem {self.sfb_smem_layout_std} -> {self.sfb_smem_layout_staged}") + # A operands: [rows, K, experts], K-major FP4. + w13 = cute.make_tensor( + w13_ptr, cute.make_ordered_layout((2 * INTER, HIDDEN, NUM_EXPERTS), order=(1, 0, 2)) + ) + w2 = cute.make_tensor( + w2_ptr, cute.make_ordered_layout((HIDDEN, INTER, NUM_EXPERTS), order=(1, 0, 2)) + ) + w13_sf = cute.make_tensor( + w13_sf_ptr, blockscaled_utils.tile_atom_to_shape_SF(w13.shape, self.sf_vec_size) + ) + w2_sf = cute.make_tensor( + w2_sf_ptr, blockscaled_utils.tile_atom_to_shape_SF(w2.shape, self.sf_vec_size) + ) + + a_op = sm100_utils.cluster_shape_to_tma_atom_A((1, 1), self.tiled_mma.thr_id) + a_smem_layout = cute.slice_(self.a_smem_layout_staged, (None, None, None, 0)) + sfa_smem_layout = cute.slice_(self.sfa_smem_layout_staged, (None, None, None, 0)) + tma_atom_a1, tma_a1 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, w13, a_smem_layout, self.mma_tiler, self.tiled_mma, self.cluster_layout_vmnk.shape + ) + tma_atom_sfa1, tma_sfa1 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, + w13_sf, + sfa_smem_layout, + self.mma_tiler, + self.tiled_mma, + self.cluster_layout_vmnk.shape, + internal_type=cutlass.Int16, + ) + tma_atom_a2, tma_a2 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, w2, a_smem_layout, self.mma_tiler, self.tiled_mma, self.cluster_layout_vmnk.shape + ) + tma_atom_sfa2, tma_sfa2 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, + w2_sf, + sfa_smem_layout, + self.mma_tiler, + self.tiled_mma, + self.cluster_layout_vmnk.shape, + internal_type=cutlass.Int16, + ) + + @cute.struct + class SharedStorage: + ring_mbar: cute.struct.MemRange[cutlass.Int64, 2 * self.num_stages] + acc_mbar: cute.struct.MemRange[cutlass.Int64, 2 * self.acc_stages] + b_mbar: cute.struct.MemRange[cutlass.Int64, NUM_BAR] + tmem_holding: cutlass.Int32 + tl_buf: cute.struct.MemRange[cutlass.Int64, TL_SLOTS] + hstage: cute.struct.Align[cute.struct.MemRange[cutlass.Int64, HSTAGE_BYTES // 8], 16] + misc: cute.struct.MemRange[cutlass.Int32, 16] + bitmap: cute.struct.MemRange[cutlass.Uint32, 8] + topk_id: cute.struct.MemRange[cutlass.Int32, NTOK * TOP_K] + topk_w: cute.struct.MemRange[cutlass.Float32, NTOK * TOP_K] + slot_expert: cute.struct.MemRange[cutlass.Int32, MAX_SLOTS] + slot_mask: cute.struct.MemRange[cutlass.Uint32, MAX_SLOTS] + slot_w: cute.struct.MemRange[cutlass.Float32, MAX_SLOTS * NTOK] + slot_k: cute.struct.MemRange[cutlass.Int32, MAX_SLOTS * NTOK] + slot_alpha1: cute.struct.MemRange[cutlass.Float32, MAX_SLOTS] + slot_scale_c1: cute.struct.MemRange[cutlass.Float32, MAX_SLOTS] + slot_alpha2: cute.struct.MemRange[cutlass.Float32, MAX_SLOTS] + expert_slot: cute.struct.MemRange[cutlass.Int32, NUM_EXPERTS] + sA: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float4E2M1FN, cute.cosize(self.a_smem_layout_staged.outer) + ], + 1024, + ] + sSFA: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float8E4M3FN, cute.cosize(self.sfa_smem_layout_staged) + ], + 1024, + ] + sX: cute.struct.Align[cute.struct.MemRange[cutlass.Uint8, NTOK * X_ROW_BYTES], 1024] + sB: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float4E2M1FN, cute.cosize(self.b_smem_layout_staged.outer) + ], + 1024, + ] + sSFB: cute.struct.Align[cute.struct.MemRange[cutlass.Float8E4M3FN, SFB_BYTES], 1024] + + self.shared_storage = SharedStorage + + self.kernel( + self.tiled_mma, + tma_atom_a1, + tma_a1, + tma_atom_sfa1, + tma_sfa1, + tma_atom_a2, + tma_a2, + tma_atom_sfa2, + tma_sfa2, + self.cluster_layout_vmnk, + self.a_smem_layout_staged, + self.sfa_smem_layout_staged, + self.b_smem_layout_staged, + self.sfb_smem_layout_staged, + self.sfb_smem_layout_std, + x_addr, + logits_addr, + bias_addr, + alpha1_addr, + scale_c1_addr, + alpha2_addr, + xgs_addr, + out_addr, + raw_scale_addr, + raw_idx_addr, + ws_addr, + part_addr, + himg_addr, + hflag_addr, + dbg_addr, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + num_tokens, + ).launch( + grid=[self.num_ctas, 1, 1], + block=[NUM_THREADS, 1, 1], + cluster=[1, 1, 1], + smem=self.shared_storage.size_in_bytes(), + min_blocks_per_mp=1, + stream=stream, + use_pdl=True, + ) + + # ------------------------------------------------------------------ + # Device code + # ------------------------------------------------------------------ + @cute.kernel + def kernel( + self, + tiled_mma: cute.TiledMma, + tma_atom_a1: cute.CopyAtom, + mA1: cute.Tensor, + tma_atom_sfa1: cute.CopyAtom, + mSFA1: cute.Tensor, + tma_atom_a2: cute.CopyAtom, + mA2: cute.Tensor, + tma_atom_sfa2: cute.CopyAtom, + mSFA2: cute.Tensor, + cluster_layout_vmnk: cute.Layout, + a_smem_layout_staged: cute.ComposedLayout, + sfa_smem_layout_staged: cute.Layout, + b_smem_layout_staged: cute.ComposedLayout, + sfb_smem_layout_staged: cute.Layout, + sfb_smem_layout_std: cute.Layout, + x_addr: Int64, + logits_addr: Int64, + bias_addr: Int64, + alpha1_addr: Int64, + scale_c1_addr: Int64, + alpha2_addr: Int64, + xgs_addr: Int64, + out_addr: Int64, + raw_scale_addr: Int64, + raw_idx_addr: Int64, + ws_addr: Int64, + part_addr: Int64, + himg_addr: Int64, + hflag_addr: Int64, + dbg_addr: Int64, + w13_addr: Int64, + w13_sf_addr: Int64, + w2_addr: Int64, + w2_sf_addr: Int64, + num_tokens: Int32, + ): + warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) + tidx, _, _ = cute.arch.thread_idx() + lane = tidx % 32 + cta, _, _ = cute.arch.block_idx() + grid, _, _ = cute.arch.grid_dim() + cta = Int32(cta) + grid = Int32(grid) + + if warp_idx == TMA_WARP: + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_a1) + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_sfa1) + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_a2) + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_sfa2) + + smem = utils.SmemAllocator() + storage = smem.allocate(self.shared_storage) + + ring = pipeline.PipelineTmaUmma.create( + barrier_storage=storage.ring_mbar.data_ptr(), + num_stages=self.num_stages, + producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread), + consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, 1), + tx_count=self.num_tma_load_bytes, + cta_layout_vmnk=cluster_layout_vmnk, + ) + acc_pipe = pipeline.PipelineUmmaAsync.create( + barrier_storage=storage.acc_mbar.data_ptr(), + num_stages=self.acc_stages, + producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread), + consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, EPI_WARPS), + cta_layout_vmnk=cluster_layout_vmnk, + ) + b_mbar = storage.b_mbar.data_ptr() + if warp_idx == MMA_WARP: + with cute.arch.elect_one(): + # one arrival per quantized k-block (all rows) + for kb in cutlass.range_constexpr(FC1_KB): + cute.arch.mbarrier_init(b_mbar + kb, 1) + for ls in cutlass.range_constexpr(MAX_LOCAL_SLOTS): + cute.arch.mbarrier_init(b_mbar + B2_BAR + ls, 1) + cute.arch.mbarrier_init(b_mbar + X_BAR, 1) + cute.arch.mbarrier_init(b_mbar + FC1_DONE_BAR, 1) + for ls in cutlass.range_constexpr(MAX_LOCAL_SLOTS): + cute.arch.mbarrier_init(b_mbar + STG_BAR + ls, 1) + + cute.arch.mbarrier_init_fence() + + if warp_idx == 0: + cute.arch.alloc_tmem(TMEM_COLS, storage.tmem_holding) + + # Shared routing tables, zeroed before routing. + s_misc = storage.misc.get_tensor(cute.make_layout(16)) + s_bitmap = storage.bitmap.get_tensor(cute.make_layout(8)) + s_topk_id = storage.topk_id.get_tensor(cute.make_layout(NTOK * TOP_K)) + s_topk_w = storage.topk_w.get_tensor(cute.make_layout(NTOK * TOP_K)) + s_slot_expert = storage.slot_expert.get_tensor(cute.make_layout(MAX_SLOTS)) + s_slot_mask = storage.slot_mask.get_tensor(cute.make_layout(MAX_SLOTS)) + s_slot_w = storage.slot_w.get_tensor(cute.make_layout(MAX_SLOTS * NTOK)) + s_slot_k = storage.slot_k.get_tensor(cute.make_layout(MAX_SLOTS * NTOK)) + s_alpha1 = storage.slot_alpha1.get_tensor(cute.make_layout(MAX_SLOTS)) + s_scale_c1 = storage.slot_scale_c1.get_tensor(cute.make_layout(MAX_SLOTS)) + s_alpha2 = storage.slot_alpha2.get_tensor(cute.make_layout(MAX_SLOTS)) + s_expert_slot = storage.expert_slot.get_tensor(cute.make_layout(NUM_EXPERTS)) + + s_expert_slot[tidx] = Int32(-1) + s_slot_w[tidx] = Float32(0.0) + s_slot_w[tidx + NUM_THREADS] = Float32(0.0) + if tidx < MAX_SLOTS: + s_slot_mask[tidx] = Uint32(0) + if tidx < 16: + s_misc[tidx] = Int32(0) + if cutlass.const_expr(self.debug & DEBUG_TIMELINE): + if tidx < TL_SLOTS: + ptx.sts_u64(Int32(storage.tl_buf.data_ptr().toint()) + tidx * 8, Int64(0)) + if tidx < 8: + s_bitmap[tidx] = Uint32(0) + # The router bias is a parameter, not an upstream output: read it + # before waiting on the previous grid. + bias_r = cute.make_rmem_tensor((8,), Float32) + for j in cutlass.range_constexpr(8): + bias_r[j] = ptx.bf16x2_to_f32(ptx.ld_nc_u16(bias_addr + (lane + 32 * j) * 2), False) + cute.arch.sync_threads() + + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + if tidx == 0: + _stamp(tl, tl_s, TL_ENTRY) + if cutlass.const_expr(tl): + ptx.sts_u64(tl_s + TL_GT0 * 8, ptx.globaltimer_u64()) + # Everything below reads tensors produced by earlier kernels or + # touches the workspace shared with the previous launch. + cute.arch.griddepcontrol_wait() + if tidx == 0: + _stamp(tl, tl_s, TL_PDL) + # Every CTA counts its own launches, so all CTAs of a launch read the + # same value without synchronizing; this launch stamps LL words with + # the incremented value (stale words carry older epochs). + epoch_next = ptx.ld_relaxed_gpu_u32(ws_addr + cta * 4) + Uint32(1) + if cutlass.const_expr(self.delay & DELAY_ENTRY): + self._delay_spin(DELAY_ENTRY * 53, cta, epoch_next) + # Stage the activation rows in shared memory with bulk copies before + # any weight streaming starts, so they never queue behind it. + if warp_idx == TMA_WARP: + with cute.arch.elect_one(): + xbar = Int32((b_mbar + X_BAR).toint()) + sx = Int32(storage.sX.data_ptr().toint()) + ptx.mbar_arrive_expect_tx(xbar, num_tokens * X_ROW_BYTES) + r = Int32(0) + while r < num_tokens: + ptx.bulk_g2s( + sx + r * X_ROW_BYTES, x_addr + r * X_ROW_BYTES, Int32(X_ROW_BYTES), xbar + ) + r = r + 1 + + # -------- routing (warp t handles token t) -> slot tables -------- + self._route_tokens( + warp_idx, lane, num_tokens, logits_addr, bias_r, s_topk_id, s_topk_w, storage + ) + cute.arch.sync_threads() + # Every thread has read the epoch word (barrier above): advance it. + if tidx == 0: + ptx.st_relaxed_gpu_u32(ws_addr + cta * 4, epoch_next) + e = tidx + bw = s_bitmap[e // 32] + prefix = Int32(0) + for w in cutlass.range_constexpr(8): + word = s_bitmap[w] + if w < e // 32: + prefix = prefix + ptx.popc_u32(word) + below = bw & ((Uint32(1) << Uint32(e % 32)) - Uint32(1)) + prefix = prefix + ptx.popc_u32(below) + if ((bw >> Uint32(e % 32)) & Uint32(1)) != Uint32(0): + s_slot_expert[prefix] = e + s_expert_slot[e] = prefix + if tidx == 0: + total = Int32(0) + for w in cutlass.range_constexpr(8): + total = total + ptx.popc_u32(s_bitmap[w]) + s_misc[MISC_NUM_SLOTS] = total + cute.arch.sync_threads() + if tidx < NTOK * TOP_K: + t = tidx // TOP_K + k = tidx % TOP_K + if t < num_tokens: + e = s_topk_id[tidx] + sl = s_expert_slot[e] + s_slot_w[sl * NTOK + t] = s_topk_w[tidx] + s_slot_k[sl * NTOK + t] = k + ptx.atom_or_shared_u32( + Int32(storage.slot_mask.data_ptr().toint()) + sl * 4, Uint32(1) << Uint32(t) + ) + cute.arch.sync_threads() + num_slots = s_misc[MISC_NUM_SLOTS] + if tidx == 0: + _stamp(tl, tl_s, TL_ROUTED) + + # ---------------- static work partition ---------------- + # FC1 runs on p1 <= grid CTAs (at least min_fc1_kb k-blocks each); + # FC2 runs on the whole grid. + t1 = num_slots * (FC1_MT * FC1_KB) + p1 = cutlass.min(grid, t1 // self.min_fc1_kb) + g0 = t1 + g1 = t1 + if cta < p1: + g0 = (cta * t1) // p1 + g1 = ((cta + 1) * t1) // p1 + t2 = num_slots * FC2_MT + u0 = (cta * t2) // grid + u1 = ((cta + 1) * t2) // grid + + if warp_idx == TMA_WARP: + self._tma_producer( + tma_atom_a1, + mA1, + tma_atom_sfa1, + mSFA1, + tma_atom_a2, + mA2, + tma_atom_sfa2, + mSFA2, + a_smem_layout_staged, + sfa_smem_layout_staged, + tiled_mma, + cluster_layout_vmnk, + storage, + ring, + s_slot_expert, + g0, + g1, + u0, + u1, + num_slots, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ) + if warp_idx == MMA_WARP: + self._mma_issuer( + tiled_mma, + storage, + ring, + acc_pipe, + b_mbar, + a_smem_layout_staged, + sfa_smem_layout_staged, + b_smem_layout_staged, + sfb_smem_layout_staged, + sfb_smem_layout_std, + g0, + g1, + u0, + u1, + num_slots, + ) + if (warp_idx != TMA_WARP) & (warp_idx != MMA_WARP): + self._quantize_x( + warp_idx, lane, xgs_addr, num_tokens, g0, g1, storage, dbg_addr, b_mbar + ) + if warp_idx >= GATHER_WARP0: + self._fetch_fc2_operands( + tidx - 32 * GATHER_WARP0, + u0, + u1, + num_slots, + epoch_next, + storage, + b_mbar, + himg_addr, + hflag_addr, + ) + + if cutlass.const_expr(not self.finalize): + if cta == 0: + if tidx < NTOK * TOP_K: + t = tidx // TOP_K + if t < num_tokens: + ptx.st_u16(raw_scale_addr + tidx * 2, ptx.bf16_bits(s_topk_w[tidx])) + ptx.st_u32(raw_idx_addr + tidx * 4, Uint32(tidx)) + + if cutlass.const_expr(self.debug & DEBUG_ROUTING): + if cta == 0: + if tidx < NTOK * TOP_K: + ptx.st_u32(dbg_addr + tidx * 4, Uint32(s_topk_id[tidx])) + ptx.st_f32(dbg_addr + 256 + tidx * 4, s_topk_w[tidx]) + if tidx < MAX_SLOTS: + ptx.st_u32(dbg_addr + 512 + tidx * 4, Uint32(s_slot_expert[tidx])) + ptx.st_u32(dbg_addr + 768 + tidx * 4, s_slot_mask[tidx]) + if tidx == 0: + ptx.st_u32(dbg_addr + 1024, Uint32(num_slots)) + + if warp_idx < EPI_WARPS: + if tidx < num_slots: + e = s_slot_expert[tidx] + s_alpha1[tidx] = ptx.ld_nc_f32(alpha1_addr + e * 4) + s_scale_c1[tidx] = ptx.ld_nc_f32(scale_c1_addr + e * 4) + s_alpha2[tidx] = ptx.ld_nc_f32(alpha2_addr + e * 4) + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + if tidx == 0: + _stamp(tl, tl_s, TL_EPI_START) + self._epilogue( + tiled_mma, + tidx, + warp_idx, + lane, + cta, + grid, + p1, + storage, + acc_pipe, + b_mbar, + s_slot_mask, + s_slot_w, + s_slot_k, + s_alpha1, + s_scale_c1, + s_alpha2, + num_slots, + num_tokens, + epoch_next, + g0, + g1, + t1, + u0, + u1, + t2, + out_addr, + part_addr, + himg_addr, + hflag_addr, + dbg_addr, + ) + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + if cutlass.const_expr(self.debug & DEBUG_TIMELINE): + if tidx < TL_SLOTS: + ptx.st_u64( + dbg_addr + TL_BYTE_BASE + (cta * TL_SLOTS + tidx) * 8, + ptx.lds_u64(tl_s + tidx * 8), + ) + if warp_idx == 0: + tmem_ptr = cute.arch.retrieve_tmem_ptr( + Float32, alignment=16, ptr_to_buffer_holding_addr=storage.tmem_holding + ) + cute.arch.dealloc_tmem(tmem_ptr, TMEM_COLS) + + # ------------------------------------------------------------------ + @cute.jit + def _route_tokens(self, w, lane, num_tokens, logits_addr, bias_r, s_topk_id, s_topk_w, storage): + """Native SigmoidBias + ScaledSumNormalize top-8 for token ``w``. + + Lane ``l`` owns experts ``l + 32 j``. Selection key: twiddled FP32 + ``sigmoid + bias`` bits above ``65535 - expert`` (ties -> lower id), + as the native 64-bit key. + """ + if w < num_tokens: + lg = cute.make_rmem_tensor((8,), Float32) + for j in cutlass.range_constexpr(8): + lg[j] = ptx.ld_nc_f32(logits_addr + (w * NUM_EXPERTS + lane + 32 * j) * 4) + self._route_one(w, lane, lg, 0, bias_r, s_topk_id, s_topk_w, storage) + + @cute.jit + def _route_one(self, t, lane, lg, h, bias_r, s_topk_id, s_topk_w, storage): + hi = cute.make_rmem_tensor((8,), Uint32) + sg = cute.make_rmem_tensor((8,), Float32) + for j in cutlass.range_constexpr(8): + s = Float32(0.5) * cute.math.tanh(Float32(0.5) * lg[8 * h + j]) + Float32(0.5) + score = s + bias_r[j] + sg[j] = score - bias_r[j] + bits = ptx.f32_bits(score) + tw = bits ^ Uint32(0x80000000) + if (bits >> Uint32(31)) != Uint32(0): + tw = bits ^ Uint32(0xFFFFFFFF) + hi[j] = tw + taken = Uint32(0) + sel_e = cute.make_rmem_tensor((8,), Int32) + sig = cute.make_rmem_tensor((8,), Float32) + for k in cutlass.range_constexpr(8): + bh = Uint32(0) + bl = Uint32(0) + bs = Float32(0.0) + for j in cutlass.range_constexpr(8): + lo_j = Uint32(65535 - (lane + 32 * j)) + avail = ((taken >> Uint32(j)) & Uint32(1)) == Uint32(0) + better = (hi[j] > bh) | ((hi[j] == bh) & (lo_j > bl)) + if avail & better: + bh = hi[j] + bl = lo_j + bs = sg[j] + mh = ptx.redux_max_u32(bh) + lo_c = Uint32(0) + if bh == mh: + lo_c = bl + ml = ptx.redux_max_u32(lo_c) + win = Int32(65535) - Int32(ml) + if (win % 32) == lane: + taken = taken | (Uint32(1) << Uint32(win // 32)) + sel_e[k] = win + sig[k] = cute.arch.shuffle_sync(bs, win % 32) + # Butterfly order of the native warp reduction over lanes 0..7. + total = ((sig[0] + sig[4]) + (sig[2] + sig[6])) + ((sig[1] + sig[5]) + (sig[3] + sig[7])) + if lane < TOP_K: + my_sig = sig[0] + my_e = sel_e[0] + for k in cutlass.range_constexpr(1, 8): + if lane == k: + my_sig = sig[k] + my_e = sel_e[k] + wgt = ptx.bf16_round((my_sig * Float32(ROUTED_SCALING)) / total) + s_topk_id[t * TOP_K + lane] = my_e + s_topk_w[t * TOP_K + lane] = wgt + ptx.atom_or_shared_u32( + Int32(storage.bitmap.data_ptr().toint()) + (my_e // 32) * 4, + Uint32(1) << Uint32(my_e % 32), + ) + + # ------------------------------------------------------------------ + @cute.jit + def _prefetch_fc1(self, lane, g_begin, g1, s_slot_expert, w13_addr, w13_sf_addr): + """L2 prefetch of FC1 units [g_begin, g1): per tile segment, each of + the 128 rows' contiguous k-range and the segment's scale bytes.""" + g = g_begin + while g < g1: + tile = g // FC1_KB + tile_base = tile * FC1_KB + seg_end = cutlass.min(g1, tile_base + FC1_KB) + slot = tile // FC1_MT + mt = tile - slot * FC1_MT + expert = Int64(s_slot_expert[slot]) + ka = g - tile_base + nk = seg_end - g + row0 = ( + w13_addr + + expert * W13_EXPERT_BYTES + + Int64(mt * 128) * W13_ROW_BYTES + + Int64(ka * KB_ROW_BYTES) + ) + for i in cutlass.range_constexpr(4): + ptx.prefetch_l2_bulk(row0 + Int64(lane + 32 * i) * W13_ROW_BYTES, nk * KB_ROW_BYTES) + if lane == 0: + ptx.prefetch_l2_bulk( + w13_sf_addr + + expert * W13_SF_EXPERT_BYTES + + Int64(mt * W13_SF_TILE_BYTES + ka * W13_SF_KB_BYTES), + nk * W13_SF_KB_BYTES, + ) + g = seg_end + + @cute.jit + def _prefetch_fc2(self, lane, u0, u1, num_slots, s_slot_expert, w2_addr, w2_sf_addr): + """L2 prefetch of FC2 units [u0, u1): 16 KB weights + 2 KB scales.""" + u = u0 + lane + while u < u1: + mt2 = u // num_slots + slot = u - mt2 * num_slots + expert = Int64(s_slot_expert[slot]) + ptx.prefetch_l2_bulk( + w2_addr + expert * W2_EXPERT_BYTES + Int64(mt2 * W2_TILE_BYTES), + Int32(W2_TILE_BYTES), + ) + ptx.prefetch_l2_bulk( + w2_sf_addr + expert * W2_SF_EXPERT_BYTES + Int64(mt2 * W2_SF_TILE_BYTES), + Int32(W2_SF_TILE_BYTES), + ) + u = u + 32 + + @cute.jit + def _tma_producer( + self, + tma_atom_a1, + mA1, + tma_atom_sfa1, + mSFA1, + tma_atom_a2, + mA2, + tma_atom_sfa2, + mSFA2, + a_smem_layout_staged, + sfa_smem_layout_staged, + tiled_mma, + cluster_layout_vmnk, + storage, + ring, + s_slot_expert, + g0, + g1, + u0, + u1, + num_slots, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ): + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + lane = cute.arch.lane_idx() + lane0 = lane == 0 + sA = storage.sA.get_tensor(a_smem_layout_staged.outer, swizzle=a_smem_layout_staged.inner) + sSFA = storage.sSFA.get_tensor(sfa_smem_layout_staged) + thr_mma = tiled_mma.get_slice(0) + a_cta_layout = cute.make_layout(cute.slice_(cluster_layout_vmnk, (0, 0, None, 0)).shape) + + gA1 = cute.local_tile(mA1, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None)) + gSFA1 = cute.local_tile( + mSFA1, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None) + ) + gA2 = cute.local_tile(mA2, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None)) + gSFA2 = cute.local_tile( + mSFA2, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None) + ) + tAsA, tAgA1 = cute.nvgpu.cpasync.tma_partition( + tma_atom_a1, + 0, + a_cta_layout, + cute.group_modes(sA, 0, 3), + cute.group_modes(thr_mma.partition_A(gA1), 0, 3), + ) + tAsSFA, tAgSFA1 = cute.nvgpu.cpasync.tma_partition( + tma_atom_sfa1, + 0, + a_cta_layout, + cute.group_modes(sSFA, 0, 3), + cute.group_modes(thr_mma.partition_A(gSFA1), 0, 3), + ) + tAsSFA = cute.filter_zeros(tAsSFA) + tAgSFA1 = cute.filter_zeros(tAgSFA1) + _, tAgA2 = cute.nvgpu.cpasync.tma_partition( + tma_atom_a2, + 0, + a_cta_layout, + cute.group_modes(sA, 0, 3), + cute.group_modes(thr_mma.partition_A(gA2), 0, 3), + ) + _, tAgSFA2 = cute.nvgpu.cpasync.tma_partition( + tma_atom_sfa2, + 0, + a_cta_layout, + cute.group_modes(sSFA, 0, 3), + cute.group_modes(thr_mma.partition_A(gSFA2), 0, 3), + ) + tAgSFA2 = cute.filter_zeros(tAgSFA2) + + state = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, self.num_stages) + # Prime the ring first, then hand the rest of this CTA's weight + # bytes to L2 prefetches so DRAM sees long sequential streams. + n_prime = cutlass.min(Int32(self.num_stages), g1 - g0) + g = g0 + while g < g1: + if g - g0 == n_prime: + self._issue_prefetches( + lane, + g0 + n_prime, + g1, + u0, + u1, + num_slots, + s_slot_expert, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ) + if lane0: + _stamp(tl, tl_s, TL_PF_ISSUED) + tile = g // FC1_KB + kb = g - tile * FC1_KB + slot = tile // FC1_MT + mt = tile - slot * FC1_MT + expert = s_slot_expert[slot] + ring.producer_acquire(state) + bar = ring.producer_get_barrier(state) + cute.copy( + tma_atom_a1, + tAgA1[(None, mt, kb, expert)], + tAsA[(None, state.index)], + tma_bar_ptr=bar, + cache_policy=Int64(L2_EVICT_FIRST), + ) + cute.copy( + tma_atom_sfa1, + tAgSFA1[(None, mt, kb, expert)], + tAsSFA[(None, state.index)], + tma_bar_ptr=bar, + cache_policy=Int64(L2_EVICT_FIRST), + ) + state.advance() + g = g + 1 + if g1 - g0 == n_prime: + # Fewer FC1 units than ring stages: nothing left of FC1. + self._issue_prefetches( + lane, + g1, + g1, + u0, + u1, + num_slots, + s_slot_expert, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ) + if lane0: + _stamp(tl, tl_s, TL_PF_ISSUED) + if lane0: + _stamp(tl, tl_s, TL_TMA_FC1) + if cutlass.const_expr(self.prefetch & PF_FC2_LATE): + self._prefetch_fc2(lane, u0, u1, num_slots, s_slot_expert, w2_addr, w2_sf_addr) + u = u0 + while u < u1: + mt2 = u // num_slots + slot = u - mt2 * num_slots + expert = s_slot_expert[slot] + ring.producer_acquire(state) + bar = ring.producer_get_barrier(state) + cute.copy( + tma_atom_a2, + tAgA2[(None, mt2, 0, expert)], + tAsA[(None, state.index)], + tma_bar_ptr=bar, + cache_policy=Int64(L2_EVICT_FIRST), + ) + cute.copy( + tma_atom_sfa2, + tAgSFA2[(None, mt2, 0, expert)], + tAsSFA[(None, state.index)], + tma_bar_ptr=bar, + cache_policy=Int64(L2_EVICT_FIRST), + ) + state.advance() + u = u + 1 + if lane0: + _stamp(tl, tl_s, TL_TMA_FC2) + ring.producer_tail(state) + + @cute.jit + def _issue_prefetches( + self, + lane, + g_begin, + g1, + u0, + u1, + num_slots, + s_slot_expert, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ): + if cutlass.const_expr(self.prefetch & PF_FC1): + self._prefetch_fc1(lane, g_begin, g1, s_slot_expert, w13_addr, w13_sf_addr) + if cutlass.const_expr(self.prefetch & PF_FC2_EARLY): + self._prefetch_fc2(lane, u0, u1, num_slots, s_slot_expert, w2_addr, w2_sf_addr) + + # ------------------------------------------------------------------ + @cute.jit + def _mma_issuer( + self, + tiled_mma, + storage, + ring, + acc_pipe, + b_mbar, + a_smem_layout_staged, + sfa_smem_layout_staged, + b_smem_layout_staged, + sfb_smem_layout_staged, + sfb_smem_layout_std, + g0, + g1, + u0, + u1, + num_slots, + ): + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + lane0 = cute.arch.lane_idx() == 0 + sA = storage.sA.get_tensor(a_smem_layout_staged.outer, swizzle=a_smem_layout_staged.inner) + sB = storage.sB.get_tensor(b_smem_layout_staged.outer, swizzle=b_smem_layout_staged.inner) + sSFA = storage.sSFA.get_tensor(sfa_smem_layout_staged) + sSFB = storage.sSFB.get_tensor(sfb_smem_layout_staged) + tCrA = tiled_mma.make_fragment_A(sA) + tCrB = tiled_mma.make_fragment_B(sB) + acc_shape = tiled_mma.partition_shape_C(self.mma_tiler[:2]) + tCtAcc_fake = tiled_mma.make_fragment_C(cute.append(acc_shape, self.acc_stages)) + + acc_tmem_ptr = cute.arch.retrieve_tmem_ptr( + Float32, alignment=16, ptr_to_buffer_holding_addr=storage.tmem_holding + ) + tCtAcc_base = cute.make_tensor(acc_tmem_ptr, tCtAcc_fake.layout) + sfa_tmem_ptr = cute.recast_ptr(acc_tmem_ptr + TMEM_SFA_COL, dtype=cutlass.Float8E4M3FN) + tCtSFA_layout = blockscaled_utils.make_tmem_layout_sfa( + tiled_mma, + self.mma_tiler, + self.sf_vec_size, + cute.slice_(sfa_smem_layout_staged, (None, None, None, 0)), + ) + tCtSFA = cute.make_tensor(sfa_tmem_ptr, tCtSFA_layout) + sfb_tmem_ptr = cute.recast_ptr(acc_tmem_ptr + TMEM_SFB_COL, dtype=cutlass.Float8E4M3FN) + tCtSFB_layout = blockscaled_utils.make_tmem_layout_sfb( + tiled_mma, + self.mma_tiler, + self.sf_vec_size, + cute.slice_(sfb_smem_layout_std, (None, None, None, 0)), + ) + tCtSFB = cute.make_tensor(sfb_tmem_ptr, tCtSFB_layout) + + copy_s2t_sfa, tCsSFA_s2t, tCtSFA_s2t = self._s2t_partition(sSFA, tCtSFA) + copy_s2t_sfb, tCsSFB_s2t, tCtSFB_s2t = self._s2t_partition(sSFB, tCtSFB) + + ring_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Consumer, self.num_stages + ) + acc_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Producer, self.acc_stages + ) + num_kphases = cute.size(tCrA, mode=[2]) + + # FC1: wait for the quantized activations, then one accumulator per + # split-K segment. + if lane0: + _stamp(tl, tl_s, TL_MMA_FC1_START) + g = g0 + while g < g1: + tile = g // FC1_KB + seg_end = cutlass.min(g1, (tile + 1) * FC1_KB) + acc_pipe.producer_acquire(acc_state) + tCtAcc = tCtAcc_base[(None, None, None, acc_state.index)] + tiled_mma.set(tcgen05.Field.ACCUMULATE, False) + while g < seg_end: + kb = g - (g // FC1_KB) * FC1_KB + cute.arch.mbarrier_wait(b_mbar + kb, 0) + ring.consumer_wait(ring_state) + cute.copy( + copy_s2t_sfa, tCsSFA_s2t[(None, None, None, None, ring_state.index)], tCtSFA_s2t + ) + cute.copy(copy_s2t_sfb, tCsSFB_s2t[(None, None, None, None, kb)], tCtSFB_s2t) + for kp in cutlass.range_constexpr(num_kphases): + tiled_mma.set(tcgen05.Field.SFA, tCtSFA[(None, None, kp)].iterator) + tiled_mma.set(tcgen05.Field.SFB, tCtSFB[(None, None, kp)].iterator) + cute.gemm( + tiled_mma, + tCtAcc, + tCrA[(None, None, kp, ring_state.index)], + tCrB[(None, None, kp, kb)], + tCtAcc, + ) + tiled_mma.set(tcgen05.Field.ACCUMULATE, True) + ring.consumer_release(ring_state) + ring_state.advance() + g = g + 1 + acc_pipe.producer_commit(acc_state) + acc_state.advance() + + # The FC2 operands overwrite the FC1 B operand: tell the gathering + # warps once every FC1 MMA (and scale copy) has completed. + with cute.arch.elect_one(): + tcgen05.commit(b_mbar + FC1_DONE_BAR) + # FC2: one accumulator per (output tile, expert) unit. + if lane0: + _stamp(tl, tl_s, TL_MMA_FC1_DONE) + s_first = u0 - (u0 // num_slots) * num_slots + u = u0 + while u < u1: + mt2 = u // num_slots + slot = u - mt2 * num_slots + ls = slot - s_first + if ls < 0: + ls = ls + num_slots + # operands of local slot ls are in shared memory + cute.arch.mbarrier_wait(b_mbar + B2_BAR + ls, 0) + acc_pipe.producer_acquire(acc_state) + tCtAcc = tCtAcc_base[(None, None, None, acc_state.index)] + tiled_mma.set(tcgen05.Field.ACCUMULATE, False) + ring_state = self._fc2_mma_ring( + tiled_mma, + ring, + ring_state, + tCtAcc, + copy_s2t_sfa, + tCsSFA_s2t, + tCtSFA_s2t, + copy_s2t_sfb, + tCsSFB_s2t, + tCtSFB_s2t, + tCtSFA, + tCtSFB, + tCrA, + tCrB, + ls, + num_kphases, + ) + acc_pipe.producer_commit(acc_state) + acc_state.advance() + u = u + 1 + if lane0: + _stamp(tl, tl_s, TL_MMA_FC2_DONE) + acc_pipe.producer_tail(acc_state) + + @cute.jit + def _fc2_mma_ring( + self, + tiled_mma, + ring, + ring_state, + tCtAcc, + copy_s2t_sfa, + tCsSFA_s2t, + tCtSFA_s2t, + copy_s2t_sfb, + tCsSFB_s2t, + tCtSFB_s2t, + tCtSFA, + tCtSFB, + tCrA, + tCrB, + ls, + num_kphases, + ): + """One FC2 unit from the weight ring (A) and local slot ``ls`` (B).""" + ring.consumer_wait(ring_state) + cute.copy(copy_s2t_sfa, tCsSFA_s2t[(None, None, None, None, ring_state.index)], tCtSFA_s2t) + cute.copy(copy_s2t_sfb, tCsSFB_s2t[(None, None, None, None, ls)], tCtSFB_s2t) + for kp in cutlass.range_constexpr(num_kphases): + tiled_mma.set(tcgen05.Field.SFA, tCtSFA[(None, None, kp)].iterator) + tiled_mma.set(tcgen05.Field.SFB, tCtSFB[(None, None, kp)].iterator) + cute.gemm( + tiled_mma, + tCtAcc, + tCrA[(None, None, kp, ring_state.index)], + tCrB[(None, None, kp, ls)], + tCtAcc, + ) + tiled_mma.set(tcgen05.Field.ACCUMULATE, True) + ring.consumer_release(ring_state) + ring_state.advance() + return ring_state + + def _s2t_partition(self, sSF, tSF): + tCsSF_compact = cute.filter_zeros(sSF) + tCtSF_compact = cute.filter_zeros(tSF) + copy_atom_s2t = cute.make_copy_atom( + tcgen05.Cp4x32x128bOp(self.cta_group), cutlass.Float8E4M3FN + ) + tiled_copy_s2t = tcgen05.make_s2t_copy(copy_atom_s2t, tCtSF_compact) + thr_copy_s2t = tiled_copy_s2t.get_slice(0) + tCsSF_compact_s2t_ = thr_copy_s2t.partition_S(tCsSF_compact) + tCsSF_compact_s2t = tcgen05.get_s2t_smem_desc_tensor(tiled_copy_s2t, tCsSF_compact_s2t_) + tCtSF_compact_s2t = thr_copy_s2t.partition_D(tCtSF_compact) + return tiled_copy_s2t, tCsSF_compact_s2t, tCtSF_compact_s2t + + # ------------------------------------------------------------------ + @cute.jit + def _quantize_x(self, warp_idx, lane, xgs_addr, num_tokens, g0, g1, storage, dbg_addr, b_mbar): + """NVFP4-quantize the activation k-blocks this CTA's FC1 range uses. + + One warp task is one k-block for all rows: 256 BF16 elements per row, + 8 per lane, two lanes per 16-element scale block, read from the + staged rows in shared memory; the rows are independent, so their + dependency chains interleave. Same arithmetic as the stock + ``fp4_quantize`` (E4M3 scale ``gs * amax * rcp(6)``, RN e2m1). Warps + take k-blocks in the MMA's order and each finished k-block arrives on + its mbarrier, so the first FC1 MMAs start while later k-blocks are + still being quantized. Padded rows (>= num_tokens) are quantized from + zeros: zero data and zero scales. + """ + sb_base = Int32(storage.sB.data_ptr().toint()) + sfb_base = Int32(storage.sSFB.data_ptr().toint()) + gs = ptx.ld_nc_f32(xgs_addr) + rgs = ptx.rcp_approx_ftz(gs) + rcp6 = ptx.rcp_approx_ftz(Float32(6.0)) + nkb = cutlass.min(g1 - g0, FC1_KB) + kb_first = g0 - (g0 // FC1_KB) * FC1_KB + if cutlass.const_expr(self.debug & DEBUG_QUANT): + nkb = Int32(FC1_KB) + kb_first = Int32(0) + qw = warp_idx + if warp_idx > TMA_WARP: + qw = warp_idx - 2 + sx = Int32(storage.sX.data_ptr().toint()) + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + stamp_q = (warp_idx == 0) & (lane == 0) + blk = lane // 2 + sf_off = (blk // 4) * SFB_CHUNK_STRIDE + blk % 4 + cute.arch.mbarrier_wait(b_mbar + X_BAR, 0) + if stamp_q: + _stamp(tl, tl_s, TL_Q_START) + j = Int32(0) + while qw + QUANT_WARPS * j < nkb: + kb = kb_first + qw + QUANT_WARPS * j + if kb >= FC1_KB: + kb = kb - FC1_KB + self._quant_rows(0, kb, lane, num_tokens, sx, sb_base, sfb_base, sf_off, gs, rgs, rcp6) + if num_tokens > NTOK // 2: + self._quant_rows( + 1, kb, lane, num_tokens, sx, sb_base, sfb_base, sf_off, gs, rgs, rcp6 + ) + else: + # rows 4..7 are padding: zero their scales + if lane < NTOK // 2 * 4: + n = NTOK // 2 + lane // 4 + ptx.sts_u32( + sfb_base + kb * SFB_STAGE_STRIDE + (lane % 4) * SFB_CHUNK_STRIDE + n * 16, + Uint32(0), + ) + if stamp_q: + if j == 0: + _stamp(tl, tl_s, TL_Q_G0_COMPUTE) + ptx.fence_proxy_async_shared() + cute.arch.sync_warp() + if lane == 0: + cute.arch.mbarrier_arrive(b_mbar + kb) + if stamp_q: + if j < 3: + _stamp(tl, tl_s, TL_Q_G0_FENCE + j) + j = j + 1 + if stamp_q: + _stamp(tl, tl_s, TL_Q_DONE) + if cutlass.const_expr(self.debug & DEBUG_QUANT): + # debug only: wait for every k-block, then CTA 0 dumps B / SFB + qt = warp_idx * 32 + lane + if warp_idx > TMA_WARP: + qt = qt - 64 + ptx.bar_sync(BAR_QUANT, 32 * QUANT_WARPS) + cta_q, _, _ = cute.arch.block_idx() + if cta_q == 0: + w = qt + while w < (FC1_KB * 1024 + SFB_BYTES) // 4: + if w < FC1_KB * 256: + ptx.st_u32(dbg_addr + 4096 + w * 4, ptx.lds_u32(sb_base + w * 4)) + else: + r2 = w - FC1_KB * 256 + ptx.st_u32(dbg_addr + 4096 + w * 4, ptx.lds_u32(sfb_base + r2 * 4)) + w = w + 32 * QUANT_WARPS + + @cute.jit + def _quant_rows(self, half, kb, lane, num_tokens, sx, sb_base, sfb_base, sf_off, gs, rgs, rcp6): + """Quantize rows ``4*half .. 4*half+3`` of k-block ``kb``; rows at or + beyond ``num_tokens`` get zero data and zero scales.""" + rows = NTOK // 2 + # All four rows' loads first (independent chains); rows past + # num_tokens read stale shared memory and are masked to zero below. + d = cute.make_rmem_tensor((rows * 4,), Uint32) + for r in cutlass.range_constexpr(rows): + n = half * rows + r + x4 = ptx.lds_v4_u32(sx + n * X_ROW_BYTES + kb * 512 + lane * 16) + for c in cutlass.range_constexpr(4): + d[4 * r + c] = x4[c] + for r in cutlass.range_constexpr(rows): + n = half * rows + r + vals = cute.make_rmem_tensor((8,), Float32) + for i in cutlass.range_constexpr(4): + vals[2 * i] = ptx.bf16x2_to_f32(d[4 * r + i], False) + vals[2 * i + 1] = ptx.bf16x2_to_f32(d[4 * r + i], True) + amax = Float32(0.0) + for i in cutlass.range_constexpr(8): + amax = cute.arch.fmax(amax, cute.math.absf(vals[i])) + amax = cute.arch.fmax(amax, cute.arch.shuffle_sync_bfly(amax, 1)) + sf = ptx.cvt_e4m3_rn_sat(gs * (amax * rcp6)) + sf_f = ptx.e4m3_to_f32(sf) + oscale = Float32(0.0) + if amax != Float32(0.0): + oscale = ptx.rcp_approx_ftz(sf_f * rgs) + word = ptx.cvt_e2m1x8( + vals[0] * oscale, + vals[1] * oscale, + vals[2] * oscale, + vals[3] * oscale, + vals[4] * oscale, + vals[5] * oscale, + vals[6] * oscale, + vals[7] * oscale, + ) + if n >= num_tokens: + word = Uint32(0) + sf = Uint32(0) + ptx.sts_u32( + sb_base + kb * 1024 + n * 128 + (((lane // 4) ^ n) * 16) + (lane % 4) * 4, word + ) + if lane % 2 == 0: + ptx.sts_u8(sfb_base + kb * SFB_STAGE_STRIDE + n * 16 + sf_off, sf) + + # ------------------------------------------------------------------ + @cute.jit + def _fetch_fc2_operands( + self, gt, u0, u1, num_slots, epoch_next, storage, b_mbar, himg_addr, hflag_addr + ): + """Warps 6-7: fetch this CTA's FC2 operands into shared memory. + + Thread ``ls`` (< local slots) waits for the four FC1 tile flags of + expert slot ``s_first + ls``, then bulk-copies the slot's operand + image into B stage ``ls`` / compact SFB stage ``ls``, completing on + that slot's mbarrier, so the FC2 MMAs of early slots start while + later ones are in flight. Shared memory is written only after every + FC1 MMA of this CTA has completed. + """ + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + nls = cutlass.min(num_slots, u1 - u0) + s_first = u0 - (u0 // num_slots) * num_slots + cta, _, _ = cute.arch.block_idx() + if cutlass.const_expr(self.delay & DELAY_CONSUMER): + self._delay_spin(DELAY_CONSUMER * 53, Int32(cta), epoch_next) + if cutlass.const_expr(self.handoff == 1): + self._fetch_fc2_operands_ll( + gt, nls, s_first, num_slots, epoch_next, storage, b_mbar, himg_addr, tl, tl_s + ) + if cutlass.const_expr(self.handoff == 0): + if gt < nls: + slot = s_first + gt + if slot >= num_slots: + slot = slot - num_slots + fa = hflag_addr + slot * (FC1_MT * 4) + f0 = ptx.ld_acquire_gpu_u32(fa) + f1 = ptx.ld_acquire_gpu_u32(fa + 4) + f2 = ptx.ld_acquire_gpu_u32(fa + 8) + f3 = ptx.ld_acquire_gpu_u32(fa + 12) + while ( + (f0 != epoch_next) + | (f1 != epoch_next) + | (f2 != epoch_next) + | (f3 != epoch_next) + ): + f0 = ptx.ld_acquire_gpu_u32(fa) + f1 = ptx.ld_acquire_gpu_u32(fa + 4) + f2 = ptx.ld_acquire_gpu_u32(fa + 8) + f3 = ptx.ld_acquire_gpu_u32(fa + 12) + if gt == 0: + _stamp(tl, tl_s, TL_FC2_SENTINELS) + if gt == nls - 1: + _stamp(tl, tl_s, TL_FC2_LAST_SLOT) + # the images were written by generic stores of other CTAs; the + # bulk copy reads them through the async proxy + ptx.fence_proxy_async_global() + cute.arch.mbarrier_wait(b_mbar + FC1_DONE_BAR, 0) + if gt == 0: + _stamp(tl, tl_s, TL_FC1_MMA_COMPLETE) + bar = Int32((b_mbar + B2_BAR).toint()) + gt * 8 + ptx.mbar_arrive_expect_tx(bar, Int32(HIMG_BYTES)) + src = himg_addr + slot * HBUF_BYTES + ptx.bulk_g2s( + Int32(storage.sB.data_ptr().toint()) + gt * HIMG_B_BYTES, + src, + Int32(HIMG_B_BYTES), + bar, + ) + ptx.bulk_g2s( + Int32(storage.sSFB.data_ptr().toint()) + gt * SFB_STAGE_STRIDE, + src + HIMG_B_BYTES, + Int32(HIMG_SF_BYTES), + bar, + ) + if gt == 0: + _stamp(tl, tl_s, TL_B2_READY) + + @cute.jit + def _fetch_fc2_operands_ll( + self, gt, nls, s_first, num_slots, epoch_next, storage, b_mbar, hll_addr, tl, tl_s + ): + """Hand-off mode 1: LL operands staged through the activation buffer. + + Thread ``ls`` waits for one LL word of each of its expert's four FC1 + tiles (relaxed polls, no fence), then bulk-copies the expert's LL + words into sX; all 64 threads then validate every word (re-polling + global memory for the rare stale one) and scatter the payload into + B / compact SFB stage ``ls``, expert by expert, releasing each to the + MMA warp with its own mbarrier. + """ + sx = Int32(storage.sX.data_ptr().toint()) + if gt < nls: + slot = s_first + gt + if slot >= num_slots: + slot = slot - num_slots + base = hll_addr + slot * HBUF_BYTES + # epoch half of the row-7 scale word of each tile + sa = base + (H_LL_DATA_WORDS + (NTOK - 1) * FC1_MT) * 8 + 4 + f0 = ptx.ld_relaxed_gpu_u32(sa) + f1 = ptx.ld_relaxed_gpu_u32(sa + 8) + f2 = ptx.ld_relaxed_gpu_u32(sa + 16) + f3 = ptx.ld_relaxed_gpu_u32(sa + 24) + while (f0 != epoch_next) | (f1 != epoch_next) | (f2 != epoch_next) | (f3 != epoch_next): + f0 = ptx.ld_relaxed_gpu_u32(sa) + f1 = ptx.ld_relaxed_gpu_u32(sa + 8) + f2 = ptx.ld_relaxed_gpu_u32(sa + 16) + f3 = ptx.ld_relaxed_gpu_u32(sa + 24) + if gt == 0: + _stamp(tl, tl_s, TL_FC2_SENTINELS) + if gt == nls - 1: + _stamp(tl, tl_s, TL_FC2_LAST_SLOT) + # sX is free: quantization is complete once all FC1 MMAs are + cute.arch.mbarrier_wait(b_mbar + FC1_DONE_BAR, 0) + bar = Int32((b_mbar + STG_BAR).toint()) + gt * 8 + ptx.mbar_arrive_expect_tx(bar, Int32(H_LL_BYTES)) + ptx.bulk_g2s(sx + gt * H_LL_BYTES, base, Int32(H_LL_BYTES), bar) + sb_base = Int32(storage.sB.data_ptr().toint()) + sfb_base = Int32(storage.sSFB.data_ptr().toint()) + ls = Int32(0) + while ls < nls: + cute.arch.mbarrier_wait(b_mbar + STG_BAR + ls, 0) + if gt == 0: + if ls == 0: + _stamp(tl, tl_s, TL_FC1_MMA_COMPLETE) + i = gt + while i < H_LL_ITEMS: + d = ptx.lds_v4_u32(sx + ls * H_LL_BYTES + i * 16) + if (d[1] != epoch_next) | (d[3] != epoch_next): + slot = s_first + ls + if slot >= num_slots: + slot = slot - num_slots + a = hll_addr + slot * HBUF_BYTES + i * 16 + d = ptx.ld_ll2(a) + while (d[1] != epoch_next) | (d[3] != epoch_next): + d = ptx.ld_ll2(a) + if i < H_LL_DATA_ITEMS: + # words (n, w), (n, w + 1) of the FP4 row n + n = i // 16 + w = 2 * (i - n * 16) + ptx.sts_v2_u32( + sb_base + ls * HIMG_B_BYTES + n * 128 + (((w // 4) ^ n) * 16) + (w % 4) * 4, + d[0], + d[2], + ) + else: + # scale words (n, mt), (n, mt + 1) + rr = i - H_LL_DATA_ITEMS + n = rr // 2 + mt0 = 2 * (rr - n * 2) + ptx.sts_u32( + sfb_base + ls * SFB_STAGE_STRIDE + mt0 * SFB_CHUNK_STRIDE + n * 16, d[0] + ) + ptx.sts_u32( + sfb_base + ls * SFB_STAGE_STRIDE + (mt0 + 1) * SFB_CHUNK_STRIDE + n * 16, + d[2], + ) + i = i + GATHER_THREADS + ptx.fence_proxy_async_shared() + ptx.bar_sync(BAR_GATHER, GATHER_THREADS) + if gt == 0: + cute.arch.mbarrier_arrive(b_mbar + B2_BAR + ls) + if ls == 0: + _stamp(tl, tl_s, TL_B2_READY) + ls = ls + 1 + + # ------------------------------------------------------------------ + @cute.jit + def _delay_spin(self, salt, cta, epoch): + """Stress testing: spin on the global timer. + + The delayed CTAs and the delay length are a hash of (CTA, launch + epoch, site salt), so every launch perturbs a different set of + producers and consumers. Call sites are compiled only when their + ``DELAY_*`` bit is set. + """ + h = ( + Uint32(cta) * Uint32(37) + (epoch & Uint32(0xFFFF)) * Uint32(101) + Uint32(salt) + ) % Uint32(17) + if h < Uint32(6): + d = Int64(h + Uint32(1)) * Int64(self.delay_ns) + t0 = ptx.globaltimer_u64() + t = t0 + while t - t0 < d: + t = ptx.globaltimer_u64() + + @cute.jit + def _cta_of(self, g, total, parts): + """CTA whose range [c*total//parts, (c+1)*total//parts) holds g.""" + return ((g + 1) * parts - 1) // total + + @cute.jit + def _ll_publish(self, v, tidx, cta, base, flag): + """Fire-and-forget LL store of this thread's 8 partial values.""" + a = base + cta * LL_PART_BYTES + tidx * (NTOK * 8) + for j in cutlass.range_constexpr(NTOK // 2): + ptx.st_ll2(a + j * 16, ptx.f32_bits(v[2 * j]), ptx.f32_bits(v[2 * j + 1]), flag) + + @cute.jit + def _ll_issue(self, buf, tidx, c_begin, c_end, base): + """Start loading the LL partials of CTAs c_begin.. (at most + GATHER_PEERS of them) into ``buf``; the owner issues this before its + own last segment, so the round trip overlaps that work.""" + for p in cutlass.range_constexpr(GATHER_PEERS): + if c_begin + p <= c_end: + a = base + (c_begin + p) * LL_PART_BYTES + tidx * (NTOK * 8) + for j in cutlass.range_constexpr(NTOK // 2): + d = ptx.ld_ll2(a + j * 16) + for k in cutlass.range_constexpr(4): + buf[p * 2 * NTOK + j * 4 + k] = d[k] + + @cute.jit + def _ll_finish(self, v, buf, tidx, c_begin, c_end, base, flag): + """v += the LL partials of CTAs c_begin..c_end, summed in CTA order. + + The first GATHER_PEERS peers' words are already in flight in + ``buf``; stale words are re-polled in rounds. Further peers (only + with very small splits) are fetched GATHER_PEERS at a time. + """ + c0 = c_begin + while c0 <= c_end: + if c0 != c_begin: + self._ll_issue(buf, tidx, c0, c_end, base) + pending = Int32(1) + while pending != 0: + pending = Int32(0) + for p in cutlass.range_constexpr(GATHER_PEERS): + if c0 + p <= c_end: + a = base + (c0 + p) * LL_PART_BYTES + tidx * (NTOK * 8) + for j in cutlass.range_constexpr(NTOK // 2): + o = p * 2 * NTOK + j * 4 + if (buf[o + 1] != flag) | (buf[o + 3] != flag): + d = ptx.ld_ll2(a + j * 16) + for k in cutlass.range_constexpr(4): + buf[o + k] = d[k] + pending = Int32(1) + for p in cutlass.range_constexpr(GATHER_PEERS): + if c0 + p <= c_end: + for j in cutlass.range_constexpr(NTOK // 2): + o = p * 2 * NTOK + j * 4 + v[2 * j] = v[2 * j] + ptx.bits_f32(buf[o]) + v[2 * j + 1] = v[2 * j + 1] + ptx.bits_f32(buf[o + 2]) + c0 = c0 + GATHER_PEERS + + @cute.jit + def _epilogue( + self, + tiled_mma, + tidx, + warp_idx, + lane, + cta, + grid, + p1, + storage, + acc_pipe, + b_mbar, + s_slot_mask, + s_slot_w, + s_slot_k, + s_alpha1, + s_scale_c1, + s_alpha2, + num_slots, + num_tokens, + epoch_next, + g0, + g1, + t1, + u0, + u1, + t2, + out_addr, + part_addr, + himg_addr, + hflag_addr, + dbg_addr, + ): + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + hst = Int32(storage.hstage.data_ptr().toint()) + acc_tmem_ptr = cute.arch.retrieve_tmem_ptr( + Float32, alignment=16, ptr_to_buffer_holding_addr=storage.tmem_holding + ) + acc_shape = tiled_mma.partition_shape_C(self.mma_tiler[:2]) + tCtAcc_fake = tiled_mma.make_fragment_C(cute.append(acc_shape, self.acc_stages)) + tCtAcc_base = cute.make_tensor(acc_tmem_ptr, tCtAcc_fake.layout) + lane_off = (warp_idx * 32) << 16 + acc_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Consumer, self.acc_stages + ) + v = cute.make_rmem_tensor((NTOK,), Float32) + part1 = part_addr + part2 = part_addr + grid * LL_PART_BYTES + + # ---------------- FC1 epilogue ---------------- + # A tile split over CTAs is finished by the CTA holding its k-block + # 0 (its last segment); the others publish their partial (their + # first segment) as LL words. + pbuf = cute.make_rmem_tensor((GATHER_PEERS * 2 * NTOK,), Uint32) + g = g0 + while g < g1: + tile = g // FC1_KB + tile_base = tile * FC1_KB + seg_end = cutlass.min(g1, tile_base + FC1_KB) + split_head = (g == tile_base) & (seg_end != tile_base + FC1_KB) + c_last = self._cta_of(tile_base + FC1_KB - 1, t1, p1) + if split_head: + if cutlass.const_expr(self.delay & DELAY_OWNER): + self._delay_spin(DELAY_OWNER * 53, cta, epoch_next) + self._ll_issue(pbuf, tidx, cta + 1, c_last, part1) + acc_pipe.consumer_wait(acc_state) + if tidx == 0: + if g == g0: + _stamp(tl, tl_s, TL_EPI_FIRST_ACC) + vals = ptx.tmem_ld_32x32b_x8( + Int32(tCtAcc_base[(None, None, None, acc_state.index)].iterator.toint()) + lane_off + ) + for n in cutlass.range_constexpr(NTOK): + v[n] = vals[n] + cute.arch.fence_view_async_tmem_load() + if tidx == 0: + _stamp(tl, tl_s, TL_EPI_T_TMEM) + with cute.arch.elect_one(): + acc_pipe.consumer_release(acc_state) + acc_state.advance() + if g == tile_base: + if split_head: + self._ll_finish(v, pbuf, tidx, cta + 1, c_last, part1, epoch_next) + if tidx == 0: + _stamp(tl, tl_s, TL_EPI_FC1_GATHERED) + self._fc1_tile_epilogue( + v, + tile, + warp_idx, + lane, + tidx, + s_slot_mask, + s_alpha1, + s_scale_c1, + num_tokens, + epoch_next, + himg_addr, + hflag_addr, + hst, + tl_s, + dbg_addr, + ) + else: + if cutlass.const_expr(self.delay & DELAY_PRODUCER): + self._delay_spin(DELAY_PRODUCER * 53, cta, epoch_next) + self._ll_publish(v, tidx, cta, part1, epoch_next) + g = seg_end + if tidx == 0: + _stamp(tl, tl_s, TL_EPI_FC1_DONE) + + # ---------------- FC2 epilogue ---------------- + o = cute.make_rmem_tensor((NTOK,), Float32) + hid = warp_idx * 32 + (lane % 8) * 4 + lane // 8 + u = u0 + while u < u1: + mt2 = u // num_slots + seg_start = u + seg_end = cutlass.min(u1, (mt2 + 1) * num_slots) + tile_base = mt2 * num_slots + c_last = self._cta_of(tile_base + num_slots - 1, t2, grid) + split_head = (seg_start == tile_base) & (seg_end != tile_base + num_slots) + if cutlass.const_expr(self.finalize): + if split_head: + if cutlass.const_expr(self.delay & DELAY_OWNER): + self._delay_spin(DELAY_OWNER * 53, cta, epoch_next) + self._ll_issue(pbuf, tidx, cta + 1, c_last, part2) + for n in cutlass.range_constexpr(NTOK): + o[n] = Float32(0.0) + while u < seg_end: + slot = u - mt2 * num_slots + mask = s_slot_mask[slot] + a2 = s_alpha2[slot] + acc_pipe.consumer_wait(acc_state) + if tidx == 0: + if u == u0: + _stamp(tl, tl_s, TL_FC2_FIRST_ACC) + vals = ptx.tmem_ld_32x32b_x8( + Int32(tCtAcc_base[(None, None, None, acc_state.index)].iterator.toint()) + + lane_off + ) + cute.arch.fence_view_async_tmem_load() + with cute.arch.elect_one(): + acc_pipe.consumer_release(acc_state) + acc_state.advance() + for n in cutlass.range_constexpr(NTOK): + y = ptx.bf16_round(vals[n] * a2) + if ((mask >> Uint32(n)) & Uint32(1)) != Uint32(0): + if cutlass.const_expr(self.finalize): + o[n] = o[n] + s_slot_w[slot * NTOK + n] * y + else: + k = s_slot_k[slot * NTOK + n] + row = n * TOP_K + k + ptx.st_u16( + out_addr + (row * HIDDEN + mt2 * 128 + hid) * 2, ptx.bf16_bits(y) + ) + u = u + 1 + if cutlass.const_expr(self.finalize): + if seg_start == tile_base: + if split_head: + self._ll_finish(o, pbuf, tidx, cta + 1, c_last, part2, epoch_next) + if tidx == 0: + _stamp(tl, tl_s, TL_FC2_GATHERED) + for n in cutlass.range_constexpr(NTOK): + if n < num_tokens: + ptx.st_u16( + out_addr + (n * HIDDEN + mt2 * 128 + hid) * 2, ptx.bf16_bits(o[n]) + ) + else: + if cutlass.const_expr(self.delay & DELAY_PRODUCER): + self._delay_spin(DELAY_PRODUCER * 53, cta, epoch_next) + self._ll_publish(o, tidx, cta, part2, epoch_next) + if tidx == 0: + _stamp(tl, tl_s, TL_FC2_DONE) + + @cute.jit + def _fc1_tile_epilogue( + self, + v, + tile, + warp_idx, + lane, + tidx, + s_slot_mask, + s_alpha1, + s_scale_c1, + num_tokens, + epoch_next, + himg_addr, + hflag_addr, + hst, + tl_s, + dbg_addr, + ): + """SwiGLU + NVFP4 quantization of one 128-row FC1 tile. + + Row ``32w + q`` of the stock layout holds intermediate index + ``64*mt + 16*w + j`` with ``j = 2*(q % 8) + (q // 16)`` as the up row + when ``q % 16 < 8`` and its gate row at lane ``q + 8``, so one warp + owns exactly one 16-element scale block of the intermediate. The + tile's 256 FP4 bytes and 32 scale bytes are gathered in shared memory, + stored into the expert's operand image and published with the + tile's epoch flag. + """ + slot = tile // FC1_MT + mt = tile - slot * FC1_MT + mask = s_slot_mask[slot] + a_gate = s_alpha1[slot] + s_up = s_scale_c1[slot] + is_up = (lane % 16) < 8 + rcp6 = ptx.rcp_approx_ftz(Float32(6.0)) + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + if tidx == 0: + _stamp(tl, tl_s, TL_EPI_T_PARAMS) + if cutlass.const_expr(self.debug & DEBUG_FC1): + for n in cutlass.range_constexpr(NTOK): + ptx.st_f32( + dbg_addr + 4 * (65536 + ((slot * FC1_MT + mt) * 128 + tidx) * NTOK + n), v[n] + ) + for n in cutlass.range_constexpr(NTOK): + gate_raw = cute.arch.shuffle_sync_down(v[n], 8) + gr = gate_raw * a_gate + up = v[n] * s_up + sig = ptx.rcp_approx_ftz( + Float32(1.0) + ptx.ex2_approx(gr * Float32(-1.4426950408889634)) + ) + h = Float32(0.0) + if is_up: + h = up * (gr * sig) + am = cute.math.absf(h) + am = cute.arch.fmax(am, cute.arch.shuffle_sync_bfly(am, 1)) + am = cute.arch.fmax(am, cute.arch.shuffle_sync_bfly(am, 2)) + am = cute.arch.fmax(am, cute.arch.shuffle_sync_bfly(am, 4)) + am = cute.arch.fmax(am, cute.arch.shuffle_sync_bfly(am, 16)) + sf = ptx.cvt_e4m3_rn_sat(am * rcp6) + sf_f = ptx.e4m3_to_f32(sf) + osc = Float32(0.0) + if am != Float32(0.0): + osc = ptx.rcp_approx_ftz(sf_f) + partner = cute.arch.shuffle_sync_down(h, 16) + byte = ptx.cvt_e2m1x2(h * osc, partner * osc) + routed = (((mask >> Uint32(n)) & Uint32(1)) != Uint32(0)) & (n < num_tokens) + if not routed: + byte = Uint32(0) + sf = Uint32(0) + if lane < 8: + ptx.sts_u8(hst + n * 32 + warp_idx * 8 + lane, byte) + if lane == 0: + ptx.sts_u8(hst + HSTAGE_SF + n * 4 + warp_idx, sf) + if tidx == 0: + _stamp(tl, tl_s, TL_EPI_T_COMPUTED) + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + cta, _, _ = cute.arch.block_idx() + if cutlass.const_expr(self.delay & DELAY_PRODUCER): + self._delay_spin(DELAY_PRODUCER * 53, Int32(cta), epoch_next) + if cutlass.const_expr(self.handoff == 1): + # LL words: payload and epoch in one store, no fence + base = himg_addr + slot * HBUF_BYTES + if tidx < 32: + r = tidx // 4 + p = tidx - r * 4 + w = ptx.lds_v2_u32(hst + r * 32 + p * 8) + ptx.st_ll2(base + (r * 32 + mt * 8 + 2 * p) * 8, w[0], w[1], epoch_next) + if (tidx >= 32) & (tidx < 32 + NTOK): + r = tidx - 32 + w1 = ptx.lds_u32(hst + HSTAGE_SF + r * 4) + ptx.st_ll1(base + (H_LL_DATA_WORDS + r * FC1_MT + mt) * 8, w1, epoch_next) + if tidx == 0: + _stamp(tl, tl_s, TL_EPI_T_STORED) + # the staging buffer is reused by the next tile + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + else: + img = himg_addr + slot * HBUF_BYTES + if tidx < 2 * NTOK: + # 16-byte chunk c16 = 2*mt + half of row r, swizzled as in sB + r = tidx // 2 + half = tidx - r * 2 + c16 = 2 * mt + half + d = ptx.lds_v4_u32(hst + r * 32 + half * 16) + ptx.st_v4_u32(img + r * 128 + ((c16 ^ r) * 16), d[0], d[1], d[2], d[3]) + ptx.fence_proxy_async_global() + if (tidx >= 2 * NTOK) & (tidx < 3 * NTOK): + # the tile's four scale bytes of row r, compact SFB chunk mt + r = tidx - 2 * NTOK + w1 = ptx.lds_u32(hst + HSTAGE_SF + r * 4) + ptx.st_u32(img + HIMG_B_BYTES + mt * SFB_CHUNK_STRIDE + r * 16, w1) + ptx.fence_proxy_async_global() + if tidx == 0: + _stamp(tl, tl_s, TL_EPI_T_STORED) + # all image stores (and staging reads) precede the flag; the staging + # buffer is free for the next tile after this barrier + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + if tidx == 0: + ptx.fence_acq_rel_gpu() + ptx.st_relaxed_gpu_u32(hflag_addr + (slot * FC1_MT + mt) * 4, epoch_next) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/kernel_xc.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/kernel_xc.py new file mode 100644 index 000000000000..6d905fed2d14 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/kernel_xc.py @@ -0,0 +1,1775 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Expert-cluster schedule of the routed_moe routed NVFP4 MoE (CuTe DSL, SM100 family). + +The grid is a set of 4-CTA thread block clusters. After every CTA has +recomputed the routing, each cluster owns one expert per round (one round when +the number of distinct experts D is at most the number of clusters, two +otherwise): cluster ``c`` of round ``rho`` serves expert slot ``v % D`` with +``v = rho * clusters + c``, so an expert is served by ``r`` clusters when there +are more clusters than experts, and each of those clusters computes the whole +FC1 of the expert (the repeated weight reads are L2 hits) and its share of the +48 FC2 output tiles. + +Inside a cluster, CTA ``k`` stages and NVFP4-quantizes activation k-blocks +``[6k, 6k + 6)`` of every row and bulk-copies each finished k-block into the +three peers (distributed shared memory, completing on their k-block +mbarriers), so every CTA holds the whole FC1 B operand after a quarter of the +quantization work. CTA ``k`` then computes FC1 tile ``k`` (128 of the 512 +stock gate/up rows, all 24 k-blocks, so no split-K) through an 8-stage TMA +ring, applies SwiGLU and the NVFP4 quantization of the intermediate, and +stores its 64 intermediate columns of every token straight into the FC2 +operand stage of all four CTAs, then release-arrives on each CTA's operand +mbarrier. Every CTA then runs its FC2 tiles from that operand. Nothing crosses +a cluster: there are no global flags, split partials or epochs, and a cluster +that starts late (for example behind a co-running kernel) delays only its own +expert. The host sizes the grid so that every cluster is co-resident and +SMs remain for the concurrently running shared expert (on sm_103 32 clusters, +26 for the raw tuple consumed by the post-MoE hop when the router logits come +from the pre-MoE hop; see ``routed_moe.RoutedMoe._xc_ctas``); with +``min_clusters`` set, calls with +few distinct experts use only ``clamp(D, min_clusters, clusters)`` clusters +and the rest exit after routing. + +The kernel writes the native unfinalized tuple (FC2 rows ``t * 8 + k``, +routing weights, identity permutation). The finalized output is produced by +``RoutedMoeFinalizeKernel``, launched right after with programmatic dependent +launch: the native finalize arithmetic (FP32 FMA over the top-k in order, one +BF16 rounding). +""" + +import cutlass +import cutlass.cute as cute +import cutlass.pipeline as pipeline +import cutlass.utils as utils +import cutlass.utils.blackwell_helpers as sm100_utils +import cutlass.utils.blockscaled_layout as blockscaled_utils +from cutlass.cute.nvgpu import tcgen05 +from cutlass.cutlass_dsl import Float32, Int32, Int64, Uint32 + +from . import ptx +from .kernel import ( + A_STAGE_BYTES, + BAR_EPI, + BAR_QUANT, + DEBUG_FC1, + DEBUG_QUANT, + DEBUG_ROUTING, + DEBUG_TIMELINE, + DELAY_ENTRY, + DELAY_PRODUCER, + EPI_WARPS, + FC1_KB, + FC1_MT, + FC2_MT, + HIDDEN, + HSTAGE_BYTES, + HSTAGE_SF, + L2_EVICT_FIRST, + MAX_SLOTS, + MMA_WARP, + NTOK, + NUM_EXPERTS, + NUM_THREADS, + QUANT_WARPS, + SFA_STAGE_BYTES, + SFB_BYTES, + SFB_CHUNK_STRIDE, + SFB_STAGE_STRIDE, + TL_BYTE_BASE, + TL_SLOTS, + TMA_WARP, + TMEM_COLS, + TMEM_SFA_COL, + TMEM_SFB_COL, + TOP_K, + W2_EXPERT_BYTES, + W2_SF_EXPERT_BYTES, + W2_SF_TILE_BYTES, + W2_TILE_BYTES, + W13_EXPERT_BYTES, + W13_ROW_BYTES, + W13_SF_EXPERT_BYTES, + W13_SF_TILE_BYTES, + X_ROW_BYTES, + RoutedMoeKernel, + _stamp, +) + +DELAY_FINALIZE = 16 # RoutedMoeFinalizeKernel CTAs, before their output stores +# L2 prefetch (``l2pf`` bits; 0 = off): the TMA warp issues +# ``cp.async.bulk.prefetch.L2`` for weight bytes its ring reaches later, so +# that those ring loads hit L2 instead of waiting for DRAM. +L2PF_FC1_R1 = 1 # round-1 FC1 tile (by the first cluster of the round-1 slot) +L2PF_FC2 = 2 # this CTA's FC2 tiles of both rounds +L2PF_EARLY = 4 # issue them before the first ring load (else after the first ring fill) +FC1_TILE_BYTES = 128 * W13_ROW_BYTES +PF_FC1_CHUNK = FC1_TILE_BYTES // 32 # one bulk prefetch per lane +CLUSTER = 4 # CTAs per cluster = FC1 tiles per expert +# Speculative prefetch of predicted experts (``spec_k`` > 0, package cross_layer_expert_prefetch): +# 16 KB chunks of one expert's FC1 weight, FC1 scales, FC2 weight and FC2 +# scales, in that order, for ``SPEC_KMAX``-wide rows of ranked expert IDs. +SPEC_CHUNK = 16384 +SPEC_C13 = W13_EXPERT_BYTES // SPEC_CHUNK +SPEC_C13SF = W13_SF_EXPERT_BYTES // SPEC_CHUNK +SPEC_C2 = W2_EXPERT_BYTES // SPEC_CHUNK +SPEC_C2SF = W2_SF_EXPERT_BYTES // SPEC_CHUNK +SPEC_CPE = SPEC_C13 + SPEC_C13SF + SPEC_C2 + SPEC_C2SF +SPEC_KMAX = 32 +assert SPEC_CPE * SPEC_CHUNK == ( + W13_EXPERT_BYTES + W13_SF_EXPERT_BYTES + W2_EXPERT_BYTES + W2_SF_EXPERT_BYTES +) +MAX_ROUNDS = 2 # expert rounds per cluster (D <= 2 * clusters) +L2_EVICT_NORMAL = 0x1000000000000000 +HIMG_B_BYTES = NTOK * 128 # one FC2 B operand stage (8 rows x 128 B FP4) +SFB2_BYTES = (MAX_ROUNDS - 1) * SFB_STAGE_STRIDE + 3 * SFB_CHUNK_STRIDE + 512 + +# mbarriers: one per quantized k-block, the staged activation rows, one FC2 +# operand barrier per round (4 arrivals: one per cluster CTA) +X_BAR = FC1_KB +B2_BAR = X_BAR + 1 +NUM_BAR = B2_BAR + MAX_ROUNDS +# Activation quantization is split over the cluster: CTA k stages and +# quantizes k-blocks [6k, 6k + 6) of every row and bulk-copies each finished +# k-block (1 KB B stage + 512 B compact SFB stage) into the three peers. +KB_PER_CTA = FC1_KB // CLUSTER +XS_ROW_BYTES = KB_PER_CTA * 512 # staged columns of one activation row +KB_TX_BYTES = 1024 + SFB_STAGE_STRIDE # one k-block's B + SFB stage + +# Timeline slots (debug builds only) +( + XT_ENTRY, + XT_PDL, + XT_ROUTED, + XT_Q_DONE, + XT_TMA_FC1, + XT_MMA_FC1, + XT_EPI_FC1_ACC, + XT_EPI_FC1_PUB, + XT_B2_READY, + XT_TMA_FC2, + XT_FC2_FIRST_ACC, + XT_MMA_FC2, + XT_FC2_DONE, + XT_EXIT, + XT_MMA_FC1_R0, + XT_EPI_FC1_PUB_R1, + XT_B2_READY_R1, + XT_FC2_R0_DONE, + XT_TMA_FC1_R0, + XT_PF_ISSUED, + XT_EPI_FC1_ACC_R1, + XT_EPI_FC1_SWIGLU, + XT_EPI_FC1_SWIGLU_R1, + XT_EPI_FC1_TMEM, # 23, 24: accumulator loaded from TMEM (rounds 0, 1) + XT_EPI_FC1_TMEM_R1, + XT_EPI_FC1_SWQ, # 25, 26: SwiGLU + quantization done, before the barrier + XT_EPI_FC1_SWQ_R1, + XT_EPI_FC1_SWQ_W_R1, # 27..30: the same, round 1, epilogue warp w (lane 0) +) = range(28) +# slots 28..30 are XT_EPI_FC1_SWQ_W_R1 + 1..3 +XT_GT0 = TL_SLOTS - 1 + + +class RoutedMoeExpertClusterKernel(RoutedMoeKernel): + """Expert-cluster kernel; raw output only (see module docstring).""" + + def __init__( + self, + num_ctas: int, + num_stages: int = 8, + debug: int = 0, + delay: int = 0, + delay_ns: int = 2000, + finalize: bool = False, + trigger: str = "late", + min_clusters: int = 0, + red_out: bool = False, + l2pf: int = 0, + two_round_clusters: int = 0, + acc_stages: int = 4, + spec_k: int = 0, + spec_ctas: int = 0, + spec_self: bool = False, + ): + assert num_ctas % CLUSTER == 0 + assert 0 <= spec_k <= SPEC_KMAX + # Speculative L2 prefetch (cross_layer_expert_prefetch): before its dependency wait, the TMA + # warp of each of the first ``spec_ctas`` CTAs (0 = all) issues + # prefetches of the first ``spec_k`` experts listed at + # ``spec_ids_addr`` (int32, ``SPEC_KMAX`` wide); 0 compiles it out. + # ``spec_self``: after routing, CTA 0 also writes this call's distinct + # experts there, ranked by row count, for the next call of the layer. + self.spec_k = spec_k + self.spec_ctas = spec_ctas + self.spec_self = spec_self + # 0: a two-round call (D > clusters) uses every cluster. Otherwise it + # uses this many (the rest exit after routing and leave their SMs to + # the shared expert), as long as two rounds of them cover D. + self.two_round_clusters = two_round_clusters + # L2 prefetch bits (L2PF_*); 0 compiles the prefetch out + self.l2pf = l2pf + # routed_moe_into_hop "fp32" entry: instead of the raw rows, every FC2 output adds + # its weighted contribution w[t, k] * bf16(y) (exact in FP32) to an + # FP32 [R, H] accumulator; the post-MoE hop rounds and clears it. + self.red_out = red_out + # programmatic-dependent-launch trigger: "late" after each CTA's last + # FC2 tile, "early" right after the dependency wait (the successor's + # CTAs may then wait on the free SMs), "none" (the successor launches + # when the grid completes) + assert trigger in ("late", "early", "none") + self.trigger = trigger + # 0: every cluster works. Otherwise, when D <= clusters, only + # clamp(D, min_clusters, clusters) clusters work and the others exit + # right after routing, leaving their SMs to concurrent kernels (the + # shared expert on the aux stream). + self.min_clusters = min_clusters + # Two rounds cover at most 2 x clusters distinct experts; the host + # launches this kernel only for calls with rows x 8 within that bound + # (``RoutedMoe.schedule``). + super().__init__( + num_ctas, + finalize=finalize, + num_stages=num_stages, + debug=debug, + delay=delay, + delay_ns=delay_ns, + acc_stages=acc_stages, + ) + + def _setup(self): + super()._setup() + self.b2_smem_layout_staged = sm100_utils.make_smem_layout_b( + self.tiled_mma, self.mma_tiler, cutlass.Float4E2M1FN, MAX_ROUNDS + ) + sfb2_std = blockscaled_utils.make_smem_layout_sfb( + self.tiled_mma, self.mma_tiler, self.sf_vec_size, MAX_ROUNDS + ) + self.sfb2_smem_layout_staged = cute.make_layout( + sfb2_std.shape, + stride=((((16, 4), 0), (0, 1)), 0, SFB_CHUNK_STRIDE, SFB_STAGE_STRIDE), + ) + + @cute.jit + def run( + self, + w13_ptr: cute.Pointer, + w13_sf_ptr: cute.Pointer, + w2_ptr: cute.Pointer, + w2_sf_ptr: cute.Pointer, + x_addr: Int64, + logits_addr: Int64, + bias_addr: Int64, + alpha1_addr: Int64, + scale_c1_addr: Int64, + alpha2_addr: Int64, + xgs_addr: Int64, + rows_addr: Int64, + raw_scale_addr: Int64, + raw_idx_addr: Int64, + out_addr: Int64, + counter_addr: Int64, + dbg_addr: Int64, + w13_addr: Int64, + w13_sf_addr: Int64, + w2_addr: Int64, + w2_sf_addr: Int64, + spec_ids_addr: Int64, + num_tokens: Int32, + stream, + ): + self._setup() + w13 = cute.make_tensor( + w13_ptr, cute.make_ordered_layout((2 * 256, HIDDEN, NUM_EXPERTS), order=(1, 0, 2)) + ) + w2 = cute.make_tensor( + w2_ptr, cute.make_ordered_layout((HIDDEN, 256, NUM_EXPERTS), order=(1, 0, 2)) + ) + w13_sf = cute.make_tensor( + w13_sf_ptr, blockscaled_utils.tile_atom_to_shape_SF(w13.shape, self.sf_vec_size) + ) + w2_sf = cute.make_tensor( + w2_sf_ptr, blockscaled_utils.tile_atom_to_shape_SF(w2.shape, self.sf_vec_size) + ) + a_op = sm100_utils.cluster_shape_to_tma_atom_A((1, 1), self.tiled_mma.thr_id) + a_smem_layout = cute.slice_(self.a_smem_layout_staged, (None, None, None, 0)) + sfa_smem_layout = cute.slice_(self.sfa_smem_layout_staged, (None, None, None, 0)) + tma_atom_a1, tma_a1 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, w13, a_smem_layout, self.mma_tiler, self.tiled_mma, self.cluster_layout_vmnk.shape + ) + tma_atom_sfa1, tma_sfa1 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, + w13_sf, + sfa_smem_layout, + self.mma_tiler, + self.tiled_mma, + self.cluster_layout_vmnk.shape, + internal_type=cutlass.Int16, + ) + tma_atom_a2, tma_a2 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, w2, a_smem_layout, self.mma_tiler, self.tiled_mma, self.cluster_layout_vmnk.shape + ) + tma_atom_sfa2, tma_sfa2 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, + w2_sf, + sfa_smem_layout, + self.mma_tiler, + self.tiled_mma, + self.cluster_layout_vmnk.shape, + internal_type=cutlass.Int16, + ) + + @cute.struct + class SharedStorage: + ring_mbar: cute.struct.MemRange[cutlass.Int64, 2 * self.num_stages] + acc_mbar: cute.struct.MemRange[cutlass.Int64, 2 * self.acc_stages] + b_mbar: cute.struct.MemRange[cutlass.Int64, NUM_BAR] + tmem_holding: cutlass.Int32 + tl_buf: cute.struct.MemRange[cutlass.Int64, TL_SLOTS] + hstage: cute.struct.Align[cute.struct.MemRange[cutlass.Int64, HSTAGE_BYTES // 8], 16] + misc: cute.struct.MemRange[cutlass.Int32, 16] + closed: cute.struct.MemRange[cutlass.Int32, MAX_ROUNDS * FC2_MT] + bitmap: cute.struct.MemRange[cutlass.Uint32, 8] + topk_id: cute.struct.MemRange[cutlass.Int32, NTOK * TOP_K] + topk_w: cute.struct.MemRange[cutlass.Float32, NTOK * TOP_K] + slot_expert: cute.struct.MemRange[cutlass.Int32, MAX_SLOTS] + slot_mask: cute.struct.MemRange[cutlass.Uint32, MAX_SLOTS] + slot_w: cute.struct.MemRange[cutlass.Float32, MAX_SLOTS * NTOK] + slot_k: cute.struct.MemRange[cutlass.Int32, MAX_SLOTS * NTOK] + expert_slot: cute.struct.MemRange[cutlass.Int32, NUM_EXPERTS] + sA: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float4E2M1FN, cute.cosize(self.a_smem_layout_staged.outer) + ], + 1024, + ] + sSFA: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float8E4M3FN, cute.cosize(self.sfa_smem_layout_staged) + ], + 1024, + ] + # this CTA's share of the activation rows (NTOK x 6 k-blocks) + sX: cute.struct.Align[cute.struct.MemRange[cutlass.Uint8, NTOK * XS_ROW_BYTES], 1024] + sB: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float4E2M1FN, cute.cosize(self.b_smem_layout_staged.outer) + ], + 1024, + ] + sSFB: cute.struct.Align[cute.struct.MemRange[cutlass.Float8E4M3FN, SFB_BYTES], 1024] + sB2: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float4E2M1FN, cute.cosize(self.b2_smem_layout_staged.outer) + ], + 1024, + ] + sSFB2: cute.struct.Align[cute.struct.MemRange[cutlass.Float8E4M3FN, SFB2_BYTES], 1024] + + self.shared_storage = SharedStorage + + self.kernel( + self.tiled_mma, + tma_atom_a1, + tma_a1, + tma_atom_sfa1, + tma_sfa1, + tma_atom_a2, + tma_a2, + tma_atom_sfa2, + tma_sfa2, + self.cluster_layout_vmnk, + self.a_smem_layout_staged, + self.sfa_smem_layout_staged, + self.b_smem_layout_staged, + self.sfb_smem_layout_staged, + self.sfb_smem_layout_std, + self.b2_smem_layout_staged, + self.sfb2_smem_layout_staged, + x_addr, + logits_addr, + bias_addr, + alpha1_addr, + scale_c1_addr, + alpha2_addr, + xgs_addr, + rows_addr, + raw_scale_addr, + raw_idx_addr, + out_addr, + counter_addr, + dbg_addr, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + spec_ids_addr, + num_tokens, + ).launch( + grid=[self.num_ctas, 1, 1], + block=[NUM_THREADS, 1, 1], + cluster=[CLUSTER, 1, 1], + smem=self.shared_storage.size_in_bytes(), + min_blocks_per_mp=1, + stream=stream, + use_pdl=True, + ) + + # ------------------------------------------------------------------ + @cute.kernel + def kernel( + self, + tiled_mma: cute.TiledMma, + tma_atom_a1: cute.CopyAtom, + mA1: cute.Tensor, + tma_atom_sfa1: cute.CopyAtom, + mSFA1: cute.Tensor, + tma_atom_a2: cute.CopyAtom, + mA2: cute.Tensor, + tma_atom_sfa2: cute.CopyAtom, + mSFA2: cute.Tensor, + cluster_layout_vmnk: cute.Layout, + a_smem_layout_staged: cute.ComposedLayout, + sfa_smem_layout_staged: cute.Layout, + b_smem_layout_staged: cute.ComposedLayout, + sfb_smem_layout_staged: cute.Layout, + sfb_smem_layout_std: cute.Layout, + b2_smem_layout_staged: cute.ComposedLayout, + sfb2_smem_layout_staged: cute.Layout, + x_addr: Int64, + logits_addr: Int64, + bias_addr: Int64, + alpha1_addr: Int64, + scale_c1_addr: Int64, + alpha2_addr: Int64, + xgs_addr: Int64, + rows_addr: Int64, + raw_scale_addr: Int64, + raw_idx_addr: Int64, + out_addr: Int64, + counter_addr: Int64, + dbg_addr: Int64, + w13_addr: Int64, + w13_sf_addr: Int64, + w2_addr: Int64, + w2_sf_addr: Int64, + spec_ids_addr: Int64, + num_tokens: Int32, + ): + warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) + tidx, _, _ = cute.arch.thread_idx() + lane = tidx % 32 + bidx, _, _ = cute.arch.block_idx() + grid, _, _ = cute.arch.grid_dim() + bidx = Int32(bidx) + rk = ptx.cluster_ctarank() + cl = bidx // CLUSTER + ncl = Int32(grid) // CLUSTER + + if warp_idx == TMA_WARP: + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_a1) + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_sfa1) + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_a2) + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_sfa2) + if cutlass.const_expr(self.spec_k > 0): + self._xc_spec_prefetch( + bidx, + Int32(grid), + lane, + spec_ids_addr, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ) + + smem = utils.SmemAllocator() + storage = smem.allocate(self.shared_storage) + ring = pipeline.PipelineTmaUmma.create( + barrier_storage=storage.ring_mbar.data_ptr(), + num_stages=self.num_stages, + producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread), + consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, 1), + tx_count=A_STAGE_BYTES + SFA_STAGE_BYTES, + cta_layout_vmnk=cluster_layout_vmnk, + ) + acc_pipe = pipeline.PipelineUmmaAsync.create( + barrier_storage=storage.acc_mbar.data_ptr(), + num_stages=self.acc_stages, + producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread), + consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, EPI_WARPS), + cta_layout_vmnk=cluster_layout_vmnk, + ) + b_mbar = storage.b_mbar.data_ptr() + if warp_idx == MMA_WARP: + with cute.arch.elect_one(): + for kb in cutlass.range_constexpr(FC1_KB): + cute.arch.mbarrier_init(b_mbar + kb, 1) + cute.arch.mbarrier_init(b_mbar + X_BAR, 1) + for rho in cutlass.range_constexpr(MAX_ROUNDS): + cute.arch.mbarrier_init(b_mbar + B2_BAR + rho, CLUSTER) + # k-blocks quantized by a peer complete on its bulk copies + for kb in cutlass.range_constexpr(FC1_KB): + if Int32(kb // KB_PER_CTA) != rk: + ptx.mbar_arrive_expect_tx(Int32((b_mbar + kb).toint()), Int32(KB_TX_BYTES)) + cute.arch.mbarrier_init_fence() + # Peers arrive on this CTA's operand barriers: their initialization + # must be visible cluster-wide first (waited before the first remote + # arrive, below). + cute.arch.cluster_arrive_relaxed() + + if warp_idx == 0: + cute.arch.alloc_tmem(TMEM_COLS, storage.tmem_holding) + + s_misc = storage.misc.get_tensor(cute.make_layout(16)) + s_bitmap = storage.bitmap.get_tensor(cute.make_layout(8)) + s_topk_id = storage.topk_id.get_tensor(cute.make_layout(NTOK * TOP_K)) + s_topk_w = storage.topk_w.get_tensor(cute.make_layout(NTOK * TOP_K)) + s_slot_expert = storage.slot_expert.get_tensor(cute.make_layout(MAX_SLOTS)) + s_slot_mask = storage.slot_mask.get_tensor(cute.make_layout(MAX_SLOTS)) + s_slot_w = storage.slot_w.get_tensor(cute.make_layout(MAX_SLOTS * NTOK)) + s_slot_k = storage.slot_k.get_tensor(cute.make_layout(MAX_SLOTS * NTOK)) + s_expert_slot = storage.expert_slot.get_tensor(cute.make_layout(NUM_EXPERTS)) + + s_expert_slot[tidx] = Int32(-1) + s_slot_w[tidx] = Float32(0.0) + s_slot_w[tidx + NUM_THREADS] = Float32(0.0) + if tidx < MAX_SLOTS: + s_slot_mask[tidx] = Uint32(0) + if tidx < 16: + s_misc[tidx] = Int32(0) + if cutlass.const_expr(self.debug & DEBUG_TIMELINE): + if tidx < TL_SLOTS: + ptx.sts_u64(Int32(storage.tl_buf.data_ptr().toint()) + tidx * 8, Int64(0)) + if tidx < 8: + s_bitmap[tidx] = Uint32(0) + bias_r = cute.make_rmem_tensor((8,), Float32) + for j in cutlass.range_constexpr(8): + bias_r[j] = ptx.bf16x2_to_f32(ptx.ld_nc_u16(bias_addr + (lane + 32 * j) * 2), False) + cute.arch.sync_threads() + + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + if tidx == 0: + _stamp(tl, tl_s, XT_ENTRY) + if cutlass.const_expr(tl): + ptx.sts_u64(tl_s + XT_GT0 * 8, ptx.globaltimer_u64()) + cute.arch.griddepcontrol_wait() + if tidx == 0: + _stamp(tl, tl_s, XT_PDL) + if cutlass.const_expr(self.delay & DELAY_ENTRY): + self._xc_delay(DELAY_ENTRY * 53, bidx) + if warp_idx == TMA_WARP: + with cute.arch.elect_one(): + xbar = Int32((b_mbar + X_BAR).toint()) + sx = Int32(storage.sX.data_ptr().toint()) + ptx.mbar_arrive_expect_tx(xbar, num_tokens * XS_ROW_BYTES) + r = Int32(0) + while r < num_tokens: + ptx.bulk_g2s( + sx + r * XS_ROW_BYTES, + x_addr + r * X_ROW_BYTES + rk * XS_ROW_BYTES, + Int32(XS_ROW_BYTES), + xbar, + ) + r = r + 1 + # Every CTA of the cluster has initialized its barriers: remote + # copies and arrivals may start. + cute.arch.cluster_wait() + if cutlass.const_expr(self.trigger == "early"): + cute.arch.griddepcontrol_launch_dependents() + + # -------- routing (warp t handles token t) -> slot tables -------- + self._route_tokens( + warp_idx, lane, num_tokens, logits_addr, bias_r, s_topk_id, s_topk_w, storage + ) + cute.arch.sync_threads() + e = tidx + bw = s_bitmap[e // 32] + prefix = Int32(0) + for w in cutlass.range_constexpr(8): + word = s_bitmap[w] + if w < e // 32: + prefix = prefix + ptx.popc_u32(word) + below = bw & ((Uint32(1) << Uint32(e % 32)) - Uint32(1)) + prefix = prefix + ptx.popc_u32(below) + if ((bw >> Uint32(e % 32)) & Uint32(1)) != Uint32(0): + s_slot_expert[prefix] = e + s_expert_slot[e] = prefix + if tidx == 0: + total = Int32(0) + for w in cutlass.range_constexpr(8): + total = total + ptx.popc_u32(s_bitmap[w]) + s_misc[0] = total + cute.arch.sync_threads() + if tidx < NTOK * TOP_K: + t = tidx // TOP_K + k = tidx % TOP_K + if t < num_tokens: + e = s_topk_id[tidx] + sl = s_expert_slot[e] + s_slot_w[sl * NTOK + t] = s_topk_w[tidx] + s_slot_k[sl * NTOK + t] = k + ptx.atom_or_shared_u32( + Int32(storage.slot_mask.data_ptr().toint()) + sl * 4, Uint32(1) << Uint32(t) + ) + cute.arch.sync_threads() + num_slots = s_misc[0] + if tidx == 0: + _stamp(tl, tl_s, XT_ROUTED) + + # -------- expert rounds of this cluster -------- + # Round 0 spreads min(D, clusters) experts over all clusters (an + # expert served by r clusters splits its FC2 tiles r ways); when + # D > clusters, round 1 spreads the remaining D - clusters experts + # over all clusters the same way. Every cluster then streams two FC1 + # tiles per CTA and an even share of the FC2 tiles. + nact = ncl + if cutlass.const_expr(self.min_clusters > 0): + if num_slots <= ncl: + nact = cutlass.max(num_slots, Int32(self.min_clusters)) + nact = cutlass.min(nact, ncl) + if cutlass.const_expr(self.two_round_clusters > 0): + if num_slots > ncl: + if num_slots <= Int32(MAX_ROUNDS * self.two_round_clusters): + nact = cutlass.min(Int32(self.two_round_clusters), ncl) + rounds = (num_slots + nact - 1) // nact + if cl >= nact: + rounds = Int32(0) + slots = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + shares = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + firsts = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + t_lo = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + t_hi = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + for rho in cutlass.range_constexpr(MAX_ROUNDS): + base = Int32(0) + count = cutlass.min(num_slots, nact) + if cutlass.const_expr(rho == 1): + base = nact + count = cutlass.max(num_slots - nact, Int32(1)) + i = cl % count + j = cl // count + nshare = (nact - 1 - i) // count + 1 + a = (j * FC2_MT) // nshare + n = ((j + 1) * FC2_MT) // nshare - a + slots[rho] = base + i + shares[rho] = nshare + firsts[rho] = Int32(0) + if j == 0: + firsts[rho] = Int32(1) + t_lo[rho] = a + (rk * n) // CLUSTER + t_hi[rho] = a + ((rk + 1) * n) // CLUSTER + + if warp_idx == TMA_WARP: + self._xc_tma_producer( + tma_atom_a1, + mA1, + tma_atom_sfa1, + mSFA1, + tma_atom_a2, + mA2, + tma_atom_sfa2, + mSFA2, + a_smem_layout_staged, + sfa_smem_layout_staged, + tiled_mma, + cluster_layout_vmnk, + storage, + ring, + b_mbar, + s_slot_expert, + rk, + rounds, + slots, + shares, + firsts, + t_lo, + t_hi, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ) + if cutlass.const_expr(self.spec_self): + if bidx == 0: + if warp_idx == MMA_WARP: + self._xc_spec_store(lane, num_slots, spec_ids_addr, storage, s_slot_expert) + if warp_idx == MMA_WARP: + self._xc_mma_issuer( + tiled_mma, + storage, + ring, + acc_pipe, + b_mbar, + a_smem_layout_staged, + sfa_smem_layout_staged, + b_smem_layout_staged, + sfb_smem_layout_staged, + sfb_smem_layout_std, + b2_smem_layout_staged, + sfb2_smem_layout_staged, + rounds, + t_lo, + t_hi, + ) + if (warp_idx != TMA_WARP) & (warp_idx != MMA_WARP): + if rounds > 0: + self._xc_quantize_x( + warp_idx, lane, xgs_addr, num_tokens, storage, b_mbar, dbg_addr, rk + ) + else: + # an idle cluster still lets its activation copy land + cute.arch.mbarrier_wait(b_mbar + X_BAR, 0) + + if bidx == 0: + if tidx < NTOK * TOP_K: + t = tidx // TOP_K + if t < num_tokens: + ptx.st_u16(raw_scale_addr + tidx * 2, ptx.bf16_bits(s_topk_w[tidx])) + ptx.st_u32(raw_idx_addr + tidx * 4, Uint32(tidx)) + if cutlass.const_expr(self.debug & DEBUG_ROUTING): + if bidx == 0: + if tidx < NTOK * TOP_K: + ptx.st_u32(dbg_addr + tidx * 4, Uint32(s_topk_id[tidx])) + ptx.st_f32(dbg_addr + 256 + tidx * 4, s_topk_w[tidx]) + if tidx < MAX_SLOTS: + ptx.st_u32(dbg_addr + 512 + tidx * 4, Uint32(s_slot_expert[tidx])) + ptx.st_u32(dbg_addr + 768 + tidx * 4, s_slot_mask[tidx]) + if tidx == 0: + ptx.st_u32(dbg_addr + 1024, Uint32(num_slots)) + + if warp_idx < EPI_WARPS: + self._xc_epilogue( + tiled_mma, + tidx, + warp_idx, + lane, + storage, + acc_pipe, + b_mbar, + s_slot_expert, + s_slot_mask, + s_slot_k, + rk, + rounds, + slots, + firsts, + t_lo, + t_hi, + num_tokens, + alpha1_addr, + scale_c1_addr, + alpha2_addr, + rows_addr, + out_addr, + counter_addr, + s_topk_w, + num_slots, + bidx, + dbg_addr, + ) + # No CTA leaves while a peer may still store into its shared memory. + cute.arch.cluster_arrive() + cute.arch.cluster_wait() + if warp_idx < EPI_WARPS: + if cutlass.const_expr(self.debug & DEBUG_TIMELINE): + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + if tidx == 0: + _stamp(tl, tl_s, XT_EXIT) + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + if tidx < TL_SLOTS: + ptx.st_u64( + dbg_addr + TL_BYTE_BASE + (bidx * TL_SLOTS + tidx) * 8, + ptx.lds_u64(tl_s + tidx * 8), + ) + if warp_idx == 0: + tmem_ptr = cute.arch.retrieve_tmem_ptr( + Float32, alignment=16, ptr_to_buffer_holding_addr=storage.tmem_holding + ) + cute.arch.dealloc_tmem(tmem_ptr, TMEM_COLS) + + # ------------------------------------------------------------------ + @cute.jit + def _xc_tma_producer( + self, + tma_atom_a1, + mA1, + tma_atom_sfa1, + mSFA1, + tma_atom_a2, + mA2, + tma_atom_sfa2, + mSFA2, + a_smem_layout_staged, + sfa_smem_layout_staged, + tiled_mma, + cluster_layout_vmnk, + storage, + ring, + b_mbar, + s_slot_expert, + rk, + rounds, + slots, + shares, + firsts, + t_lo, + t_hi, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ): + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + lane0 = cute.arch.lane_idx() == 0 + sA = storage.sA.get_tensor(a_smem_layout_staged.outer, swizzle=a_smem_layout_staged.inner) + sSFA = storage.sSFA.get_tensor(sfa_smem_layout_staged) + thr_mma = tiled_mma.get_slice(0) + a_cta_layout = cute.make_layout(cute.slice_(cluster_layout_vmnk, (0, 0, None, 0)).shape) + gA1 = cute.local_tile(mA1, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None)) + gSFA1 = cute.local_tile( + mSFA1, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None) + ) + gA2 = cute.local_tile(mA2, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None)) + gSFA2 = cute.local_tile( + mSFA2, cute.slice_(self.mma_tiler, (None, 0, None)), (None, None, None) + ) + tAsA, tAgA1 = cute.nvgpu.cpasync.tma_partition( + tma_atom_a1, + 0, + a_cta_layout, + cute.group_modes(sA, 0, 3), + cute.group_modes(thr_mma.partition_A(gA1), 0, 3), + ) + tAsSFA, tAgSFA1 = cute.nvgpu.cpasync.tma_partition( + tma_atom_sfa1, + 0, + a_cta_layout, + cute.group_modes(sSFA, 0, 3), + cute.group_modes(thr_mma.partition_A(gSFA1), 0, 3), + ) + tAsSFA = cute.filter_zeros(tAsSFA) + tAgSFA1 = cute.filter_zeros(tAgSFA1) + _, tAgA2 = cute.nvgpu.cpasync.tma_partition( + tma_atom_a2, + 0, + a_cta_layout, + cute.group_modes(sA, 0, 3), + cute.group_modes(thr_mma.partition_A(gA2), 0, 3), + ) + _, tAgSFA2 = cute.nvgpu.cpasync.tma_partition( + tma_atom_sfa2, + 0, + a_cta_layout, + cute.group_modes(sSFA, 0, 3), + cute.group_modes(thr_mma.partition_A(gSFA2), 0, 3), + ) + tAgSFA2 = cute.filter_zeros(tAgSFA2) + + state = pipeline.make_pipeline_state(pipeline.PipelineUserType.Producer, self.num_stages) + if cutlass.const_expr(self.l2pf & L2PF_EARLY): + self._xc_l2_prefetch( + tl, + tl_s, + rk, + rounds, + slots, + firsts, + t_lo, + t_hi, + s_slot_expert, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ) + # FC1: tile rk of each round's expert, all k-blocks. A weight read by + # several clusters stays in L2 for the others (evict-normal). + for rho in cutlass.range_constexpr(MAX_ROUNDS): + if rho < rounds: + expert = s_slot_expert[slots[rho]] + policy = Int64(L2_EVICT_FIRST) + if shares[rho] > 1: + policy = Int64(L2_EVICT_NORMAL) + kb = Int32(0) + while kb < FC1_KB: + if cutlass.const_expr( + rho == 0 and self.l2pf != 0 and not (self.l2pf & L2PF_EARLY) + ): + # after the first ring fill of round 0 + if kb == Int32(self.num_stages): + self._xc_l2_prefetch( + tl, + tl_s, + rk, + rounds, + slots, + firsts, + t_lo, + t_hi, + s_slot_expert, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ) + ring.producer_acquire(state) + bar = ring.producer_get_barrier(state) + cute.copy( + tma_atom_a1, + tAgA1[(None, rk, kb, expert)], + tAsA[(None, state.index)], + tma_bar_ptr=bar, + cache_policy=policy, + ) + cute.copy( + tma_atom_sfa1, + tAgSFA1[(None, rk, kb, expert)], + tAsSFA[(None, state.index)], + tma_bar_ptr=bar, + cache_policy=policy, + ) + state.advance() + kb = kb + 1 + if cutlass.const_expr(rho == 0): + if lane0: + _stamp(tl, tl_s, XT_TMA_FC1_R0) + if lane0: + _stamp(tl, tl_s, XT_TMA_FC1) + for rho in cutlass.range_constexpr(MAX_ROUNDS): + if rho < rounds: + expert = s_slot_expert[slots[rho]] + t = t_lo[rho] + while t < t_hi[rho]: + ring.producer_acquire(state) + bar = ring.producer_get_barrier(state) + cute.copy( + tma_atom_a2, + tAgA2[(None, t, 0, expert)], + tAsA[(None, state.index)], + tma_bar_ptr=bar, + cache_policy=Int64(L2_EVICT_FIRST), + ) + cute.copy( + tma_atom_sfa2, + tAgSFA2[(None, t, 0, expert)], + tAsSFA[(None, state.index)], + tma_bar_ptr=bar, + cache_policy=Int64(L2_EVICT_FIRST), + ) + state.advance() + t = t + 1 + if lane0: + _stamp(tl, tl_s, XT_TMA_FC2) + ring.producer_tail(state) + + # ------------------------------------------------------------------ + @cute.jit + def _xc_l2_prefetch( + self, + tl, + tl_s, + rk, + rounds, + slots, + firsts, + t_lo, + t_hi, + s_slot_expert, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ): + """Bulk L2 prefetch of weight bytes this CTA's ring reaches later. + + ``L2PF_FC1_R1``: FC1 tile ``rk`` of the round-1 expert, by the first + cluster serving that slot only (the others read it from L2 anyway); + one 12 KB chunk per lane, lane 0 also the tile's scales. + ``L2PF_FC2``: this CTA's FC2 tiles of both rounds, one tile (16 KB + + 2 KB scales) per lane. Prefetches change no data, only where the + later TMA loads find it. + """ + lane = cute.arch.lane_idx() + if cutlass.const_expr(self.l2pf & L2PF_FC1_R1): + if rounds > 1: + if firsts[1] != 0: + e1 = Int64(s_slot_expert[slots[1]]) + base = w13_addr + e1 * W13_EXPERT_BYTES + Int64(rk) * Int64(FC1_TILE_BYTES) + ptx.prefetch_l2_bulk( + base + Int64(lane) * Int64(PF_FC1_CHUNK), Int32(PF_FC1_CHUNK) + ) + if lane == 0: + ptx.prefetch_l2_bulk( + w13_sf_addr + + e1 * W13_SF_EXPERT_BYTES + + Int64(rk) * Int64(W13_SF_TILE_BYTES), + Int32(W13_SF_TILE_BYTES), + ) + if cutlass.const_expr(self.l2pf & L2PF_FC2): + for rho in cutlass.range_constexpr(MAX_ROUNDS): + if rho < rounds: + e2 = Int64(s_slot_expert[slots[rho]]) + t = t_lo[rho] + lane + if t < t_hi[rho]: + ptx.prefetch_l2_bulk( + w2_addr + e2 * W2_EXPERT_BYTES + Int64(t) * Int64(W2_TILE_BYTES), + Int32(W2_TILE_BYTES), + ) + ptx.prefetch_l2_bulk( + w2_sf_addr + + e2 * W2_SF_EXPERT_BYTES + + Int64(t) * Int64(W2_SF_TILE_BYTES), + Int32(W2_SF_TILE_BYTES), + ) + if lane == 0: + _stamp(tl, tl_s, XT_PF_ISSUED) + + # ------------------------------------------------------------------ + @cute.jit + def _xc_spec_prefetch( + self, bidx, grid, lane, ids_addr, w13_addr, w13_sf_addr, w2_addr, w2_sf_addr + ): + """Bulk L2 prefetch of the first ``spec_k`` experts at ``ids_addr``. + + Runs before the dependency wait: the IDs are written on another stream + (cross_layer_expert_prefetch) and only choose which weight bytes are prefetched, so + a stale or concurrently updated ID costs bandwidth, never correctness + (each ID is masked into [0, 256)). Chunk ``c`` of the ``spec_k`` x + ``SPEC_CPE`` chunks (expert-rank major) is issued by lane ``c % + (32 * n)`` of the first ``n`` CTAs, so the best-ranked experts go + first. + """ + nsp = grid + if cutlass.const_expr(self.spec_ctas > 0): + nsp = cutlass.min(grid, Int32(self.spec_ctas)) + if bidx < nsp: + c = bidx * 32 + lane + while c < Int32(self.spec_k * SPEC_CPE): + slot = c // SPEC_CPE + off = c - slot * SPEC_CPE + ex = Int64( + Int32( + ptx.ld_relaxed_gpu_u32(ids_addr + Int64(slot) * 4) & Uint32(NUM_EXPERTS - 1) + ) + ) + o = Int64(off) * SPEC_CHUNK + addr = w13_addr + ex * W13_EXPERT_BYTES + o + if off >= SPEC_C13: + addr = w13_sf_addr + ex * W13_SF_EXPERT_BYTES + o - SPEC_C13 * SPEC_CHUNK + if off >= SPEC_C13 + SPEC_C13SF: + addr = w2_addr + ex * W2_EXPERT_BYTES + o - (SPEC_C13 + SPEC_C13SF) * SPEC_CHUNK + if off >= SPEC_C13 + SPEC_C13SF + SPEC_C2: + addr = ( + w2_sf_addr + + ex * W2_SF_EXPERT_BYTES + + o + - (SPEC_C13 + SPEC_C13SF + SPEC_C2) * SPEC_CHUNK + ) + ptx.prefetch_l2_bulk(addr, Int32(SPEC_CHUNK)) + c = c + nsp * 32 + + # ------------------------------------------------------------------ + @cute.jit + def _xc_spec_store(self, lane, num_slots, ids_addr, storage, s_slot_expert): + """Write this call's distinct experts to ``ids_addr``, ranked by the + number of rows that select them (ties: lower expert ID first); slots + past ``num_slots`` keep their previous IDs.""" + mask_s = Int32(storage.slot_mask.data_ptr().toint()) + for h in cutlass.range_constexpr(MAX_SLOTS // 32): + i = lane + 32 * h + if i < num_slots: + ci = ptx.popc_u32(ptx.lds_u32(mask_s + i * 4)) + rank = Int32(0) + j = Int32(0) + while j < num_slots: + cj = ptx.popc_u32(ptx.lds_u32(mask_s + j * 4)) + if cj > ci: + rank = rank + 1 + if cj == ci: + if j < i: + rank = rank + 1 + j = j + 1 + if rank < SPEC_KMAX: + ptx.st_relaxed_gpu_u32(ids_addr + Int64(rank) * 4, Uint32(s_slot_expert[i])) + + # ------------------------------------------------------------------ + @cute.jit + def _xc_mma_issuer( + self, + tiled_mma, + storage, + ring, + acc_pipe, + b_mbar, + a_smem_layout_staged, + sfa_smem_layout_staged, + b_smem_layout_staged, + sfb_smem_layout_staged, + sfb_smem_layout_std, + b2_smem_layout_staged, + sfb2_smem_layout_staged, + rounds, + t_lo, + t_hi, + ): + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + lane0 = cute.arch.lane_idx() == 0 + sA = storage.sA.get_tensor(a_smem_layout_staged.outer, swizzle=a_smem_layout_staged.inner) + sB = storage.sB.get_tensor(b_smem_layout_staged.outer, swizzle=b_smem_layout_staged.inner) + sB2 = storage.sB2.get_tensor( + b2_smem_layout_staged.outer, swizzle=b2_smem_layout_staged.inner + ) + sSFA = storage.sSFA.get_tensor(sfa_smem_layout_staged) + sSFB = storage.sSFB.get_tensor(sfb_smem_layout_staged) + sSFB2 = storage.sSFB2.get_tensor(sfb2_smem_layout_staged) + tCrA = tiled_mma.make_fragment_A(sA) + tCrB = tiled_mma.make_fragment_B(sB) + tCrB2 = tiled_mma.make_fragment_B(sB2) + acc_shape = tiled_mma.partition_shape_C(self.mma_tiler[:2]) + tCtAcc_fake = tiled_mma.make_fragment_C(cute.append(acc_shape, self.acc_stages)) + acc_tmem_ptr = cute.arch.retrieve_tmem_ptr( + Float32, alignment=16, ptr_to_buffer_holding_addr=storage.tmem_holding + ) + tCtAcc_base = cute.make_tensor(acc_tmem_ptr, tCtAcc_fake.layout) + sfa_tmem_ptr = cute.recast_ptr(acc_tmem_ptr + TMEM_SFA_COL, dtype=cutlass.Float8E4M3FN) + tCtSFA_layout = blockscaled_utils.make_tmem_layout_sfa( + tiled_mma, + self.mma_tiler, + self.sf_vec_size, + cute.slice_(sfa_smem_layout_staged, (None, None, None, 0)), + ) + tCtSFA = cute.make_tensor(sfa_tmem_ptr, tCtSFA_layout) + sfb_tmem_ptr = cute.recast_ptr(acc_tmem_ptr + TMEM_SFB_COL, dtype=cutlass.Float8E4M3FN) + tCtSFB_layout = blockscaled_utils.make_tmem_layout_sfb( + tiled_mma, + self.mma_tiler, + self.sf_vec_size, + cute.slice_(sfb_smem_layout_std, (None, None, None, 0)), + ) + tCtSFB = cute.make_tensor(sfb_tmem_ptr, tCtSFB_layout) + copy_s2t_sfa, tCsSFA_s2t, tCtSFA_s2t = self._s2t_partition(sSFA, tCtSFA) + copy_s2t_sfb, tCsSFB_s2t, tCtSFB_s2t = self._s2t_partition(sSFB, tCtSFB) + _, tCsSFB2_s2t, _ = self._s2t_partition(sSFB2, tCtSFB) + + ring_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Consumer, self.num_stages + ) + acc_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Producer, self.acc_stages + ) + num_kphases = cute.size(tCrA, mode=[2]) + + for rho in cutlass.range_constexpr(MAX_ROUNDS): + if rho < rounds: + acc_pipe.producer_acquire(acc_state) + tCtAcc = tCtAcc_base[(None, None, None, acc_state.index)] + tiled_mma.set(tcgen05.Field.ACCUMULATE, False) + kb = Int32(0) + while kb < FC1_KB: + cute.arch.mbarrier_wait(b_mbar + kb, 0) + ring.consumer_wait(ring_state) + cute.copy( + copy_s2t_sfa, + tCsSFA_s2t[(None, None, None, None, ring_state.index)], + tCtSFA_s2t, + ) + cute.copy(copy_s2t_sfb, tCsSFB_s2t[(None, None, None, None, kb)], tCtSFB_s2t) + for kp in cutlass.range_constexpr(num_kphases): + tiled_mma.set(tcgen05.Field.SFA, tCtSFA[(None, None, kp)].iterator) + tiled_mma.set(tcgen05.Field.SFB, tCtSFB[(None, None, kp)].iterator) + cute.gemm( + tiled_mma, + tCtAcc, + tCrA[(None, None, kp, ring_state.index)], + tCrB[(None, None, kp, kb)], + tCtAcc, + ) + tiled_mma.set(tcgen05.Field.ACCUMULATE, True) + ring.consumer_release(ring_state) + ring_state.advance() + kb = kb + 1 + acc_pipe.producer_commit(acc_state) + acc_state.advance() + if cutlass.const_expr(rho == 0): + if lane0: + _stamp(tl, tl_s, XT_MMA_FC1_R0) + if lane0: + _stamp(tl, tl_s, XT_MMA_FC1) + + for rho in cutlass.range_constexpr(MAX_ROUNDS): + if rho < rounds: + # this round's FC2 operand: the four FC1 tiles of the cluster + bar = Int32((b_mbar + B2_BAR + rho).toint()) + done = ptx.mbar_try_wait_cluster(bar, Int32(0)) + while done == 0: + done = ptx.mbar_try_wait_cluster(bar, Int32(0)) + ptx.fence_proxy_async_shared() + if cutlass.const_expr(rho == 0): + if lane0: + _stamp(tl, tl_s, XT_B2_READY) + else: + if lane0: + _stamp(tl, tl_s, XT_B2_READY_R1) + t = t_lo[rho] + while t < t_hi[rho]: + acc_pipe.producer_acquire(acc_state) + tCtAcc = tCtAcc_base[(None, None, None, acc_state.index)] + tiled_mma.set(tcgen05.Field.ACCUMULATE, False) + ring.consumer_wait(ring_state) + cute.copy( + copy_s2t_sfa, + tCsSFA_s2t[(None, None, None, None, ring_state.index)], + tCtSFA_s2t, + ) + cute.copy(copy_s2t_sfb, tCsSFB2_s2t[(None, None, None, None, rho)], tCtSFB_s2t) + for kp in cutlass.range_constexpr(num_kphases): + tiled_mma.set(tcgen05.Field.SFA, tCtSFA[(None, None, kp)].iterator) + tiled_mma.set(tcgen05.Field.SFB, tCtSFB[(None, None, kp)].iterator) + cute.gemm( + tiled_mma, + tCtAcc, + tCrA[(None, None, kp, ring_state.index)], + tCrB2[(None, None, kp, rho)], + tCtAcc, + ) + tiled_mma.set(tcgen05.Field.ACCUMULATE, True) + ring.consumer_release(ring_state) + ring_state.advance() + acc_pipe.producer_commit(acc_state) + acc_state.advance() + t = t + 1 + if lane0: + _stamp(tl, tl_s, XT_MMA_FC2) + acc_pipe.producer_tail(acc_state) + + # ------------------------------------------------------------------ + @cute.jit + def _xc_quantize_x(self, warp_idx, lane, xgs_addr, num_tokens, storage, b_mbar, dbg_addr, rk): + """NVFP4-quantize this CTA's six activation k-blocks (same arithmetic + as the stock ``fp4_quantize``), one k-block per quantizing warp, into + the FC1 B operand of every CTA of the cluster. + + The k-block is written to the local B / SFB stage and released on the + local mbarrier, then bulk-copied into the same stage of the three + peers, completing on their k-block mbarriers (which expect the bytes). + """ + sb_base = Int32(storage.sB.data_ptr().toint()) + sfb_base = Int32(storage.sSFB.data_ptr().toint()) + gs = ptx.ld_nc_f32(xgs_addr) + rgs = ptx.rcp_approx_ftz(gs) + rcp6 = ptx.rcp_approx_ftz(Float32(6.0)) + qw = warp_idx + if warp_idx > TMA_WARP: + qw = warp_idx - 2 + sx = Int32(storage.sX.data_ptr().toint()) + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + blk = lane // 2 + sf_off = (blk // 4) * SFB_CHUNK_STRIDE + blk % 4 + cute.arch.mbarrier_wait(b_mbar + X_BAR, 0) + kb = rk * KB_PER_CTA + qw + self._quant_rows_xs( + 0, kb, qw, lane, num_tokens, sx, sb_base, sfb_base, sf_off, gs, rgs, rcp6 + ) + if num_tokens > NTOK // 2: + self._quant_rows_xs( + 1, kb, qw, lane, num_tokens, sx, sb_base, sfb_base, sf_off, gs, rgs, rcp6 + ) + else: + if lane < NTOK // 2 * 4: + n = NTOK // 2 + lane // 4 + ptx.sts_u32( + sfb_base + kb * SFB_STAGE_STRIDE + (lane % 4) * SFB_CHUNK_STRIDE + n * 16, + Uint32(0), + ) + ptx.fence_proxy_async_shared() + cute.arch.sync_warp() + kbar = Int32((b_mbar + kb).toint()) + if lane == 0: + cute.arch.mbarrier_arrive(b_mbar + kb) + # lanes 1..6: (peer, B or SFB) bulk copies into the other CTAs + if (lane >= 1) & (lane <= 2 * (CLUSTER - 1)): + peer = (lane - 1) // 2 + if peer >= rk: + peer = peer + 1 + src = sb_base + kb * 1024 + size = Int32(1024) + if ((lane - 1) % 2) != 0: + src = sfb_base + kb * SFB_STAGE_STRIDE + size = Int32(SFB_STAGE_STRIDE) + ptx.bulk_s2cluster(ptx.mapa_u32(src, peer), src, size, ptx.mapa_u32(kbar, peer)) + if (warp_idx == 0) & (lane == 0): + _stamp(tl, tl_s, XT_Q_DONE) + if cutlass.const_expr(self.debug & DEBUG_QUANT): + # debug only: wait for every k-block (local and peers'), then CTA 0 + # dumps B / SFB + for k2 in cutlass.range_constexpr(FC1_KB): + cute.arch.mbarrier_wait(b_mbar + k2, 0) + qt = warp_idx * 32 + lane + if warp_idx > TMA_WARP: + qt = qt - 64 + ptx.bar_sync(BAR_QUANT, 32 * QUANT_WARPS) + cta_q, _, _ = cute.arch.block_idx() + if cta_q == 0: + w = qt + while w < (FC1_KB * 1024 + SFB_BYTES) // 4: + if w < FC1_KB * 256: + ptx.st_u32(dbg_addr + 4096 + w * 4, ptx.lds_u32(sb_base + w * 4)) + else: + r2 = w - FC1_KB * 256 + ptx.st_u32(dbg_addr + 4096 + w * 4, ptx.lds_u32(sfb_base + r2 * 4)) + w = w + 32 * QUANT_WARPS + + @cute.jit + def _quant_rows_xs( + self, half, kb, kb_local, lane, num_tokens, sx, sb_base, sfb_base, sf_off, gs, rgs, rcp6 + ): + """Rows ``4*half .. 4*half+3`` of k-block ``kb``, read from this CTA's + staged share of the activation rows (``kb_local`` within it); rows at + or beyond ``num_tokens`` get zero data and zero scales.""" + rows = NTOK // 2 + d = cute.make_rmem_tensor((rows * 4,), Uint32) + for r in cutlass.range_constexpr(rows): + n = half * rows + r + x4 = ptx.lds_v4_u32(sx + n * XS_ROW_BYTES + kb_local * 512 + lane * 16) + for c in cutlass.range_constexpr(4): + d[4 * r + c] = x4[c] + for r in cutlass.range_constexpr(rows): + n = half * rows + r + vals = cute.make_rmem_tensor((8,), Float32) + for i in cutlass.range_constexpr(4): + vals[2 * i] = ptx.bf16x2_to_f32(d[4 * r + i], False) + vals[2 * i + 1] = ptx.bf16x2_to_f32(d[4 * r + i], True) + amax = Float32(0.0) + for i in cutlass.range_constexpr(8): + amax = cute.arch.fmax(amax, cute.math.absf(vals[i])) + amax = cute.arch.fmax(amax, cute.arch.shuffle_sync_bfly(amax, 1)) + sf = ptx.cvt_e4m3_rn_sat(gs * (amax * rcp6)) + sf_f = ptx.e4m3_to_f32(sf) + oscale = Float32(0.0) + if amax != Float32(0.0): + oscale = ptx.rcp_approx_ftz(sf_f * rgs) + word = ptx.cvt_e2m1x8( + vals[0] * oscale, + vals[1] * oscale, + vals[2] * oscale, + vals[3] * oscale, + vals[4] * oscale, + vals[5] * oscale, + vals[6] * oscale, + vals[7] * oscale, + ) + if n >= num_tokens: + word = Uint32(0) + sf = Uint32(0) + ptx.sts_u32( + sb_base + kb * 1024 + n * 128 + (((lane // 4) ^ n) * 16) + (lane % 4) * 4, word + ) + if lane % 2 == 0: + ptx.sts_u8(sfb_base + kb * SFB_STAGE_STRIDE + n * 16 + sf_off, sf) + + # ------------------------------------------------------------------ + @cute.jit + def _xc_epilogue( + self, + tiled_mma, + tidx, + warp_idx, + lane, + storage, + acc_pipe, + b_mbar, + s_slot_expert, + s_slot_mask, + s_slot_k, + rk, + rounds, + slots, + firsts, + t_lo, + t_hi, + num_tokens, + alpha1_addr, + scale_c1_addr, + alpha2_addr, + rows_addr, + out_addr, + counter_addr, + s_topk_w, + num_slots, + bidx, + dbg_addr, + ): + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + hst = Int32(storage.hstage.data_ptr().toint()) + acc_tmem_ptr = cute.arch.retrieve_tmem_ptr( + Float32, alignment=16, ptr_to_buffer_holding_addr=storage.tmem_holding + ) + acc_shape = tiled_mma.partition_shape_C(self.mma_tiler[:2]) + tCtAcc_fake = tiled_mma.make_fragment_C(cute.append(acc_shape, self.acc_stages)) + tCtAcc_base = cute.make_tensor(acc_tmem_ptr, tCtAcc_fake.layout) + lane_off = (warp_idx * 32) << 16 + acc_state = pipeline.make_pipeline_state( + pipeline.PipelineUserType.Consumer, self.acc_stages + ) + v = cute.make_rmem_tensor((NTOK,), Float32) + sb2 = Int32(storage.sB2.data_ptr().toint()) + ssfb2 = Int32(storage.sSFB2.data_ptr().toint()) + b2bar = Int32((b_mbar + B2_BAR).toint()) + + # ---------------- FC1 tile epilogue -> cluster operand ---------------- + for rho in cutlass.range_constexpr(MAX_ROUNDS): + if rho < rounds: + slot = slots[rho] + expert = s_slot_expert[slot] + mask = s_slot_mask[slot] + a_gate = ptx.ld_nc_f32(alpha1_addr + expert * 4) + s_up = ptx.ld_nc_f32(scale_c1_addr + expert * 4) + acc_pipe.consumer_wait(acc_state) + if tidx == 0: + _stamp(tl, tl_s, XT_EPI_FC1_ACC if rho == 0 else XT_EPI_FC1_ACC_R1) + vals = ptx.tmem_ld_32x32b_x8( + Int32(tCtAcc_base[(None, None, None, acc_state.index)].iterator.toint()) + + lane_off + ) + for n in cutlass.range_constexpr(NTOK): + v[n] = vals[n] + cute.arch.fence_view_async_tmem_load() + if tidx == 0: + _stamp(tl, tl_s, XT_EPI_FC1_TMEM if rho == 0 else XT_EPI_FC1_TMEM_R1) + with cute.arch.elect_one(): + acc_pipe.consumer_release(acc_state) + acc_state.advance() + if cutlass.const_expr(self.debug & DEBUG_FC1): + # the first cluster of a slot dumps its tile's accumulators + if firsts[rho] != 0: + for n in cutlass.range_constexpr(NTOK): + ptx.st_f32( + dbg_addr + + 4 * (65536 + ((slot * FC1_MT + rk) * 128 + tidx) * NTOK + n), + v[n], + ) + self._swiglu_quant_to_stage(v, warp_idx, lane, mask, a_gate, s_up, num_tokens, hst) + if tidx == 0: + _stamp(tl, tl_s, XT_EPI_FC1_SWQ if rho == 0 else XT_EPI_FC1_SWQ_R1) + if cutlass.const_expr(rho == 1): + if lane == 0: + _stamp(tl, tl_s, XT_EPI_FC1_SWQ_W_R1 + warp_idx) + if cutlass.const_expr(self.delay & DELAY_PRODUCER): + self._xc_delay(DELAY_PRODUCER * 53, bidx) + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + if tidx == 0: + _stamp(tl, tl_s, XT_EPI_FC1_SWIGLU if rho == 0 else XT_EPI_FC1_SWIGLU_R1) + # 64 threads: 16-byte FP4 chunks (row r, half h) for CTA d; + # 32 threads: the tile's four scale bytes of row r for CTA d. + if tidx < 4 * 2 * NTOK: + d = tidx // (2 * NTOK) + q = tidx - d * (2 * NTOK) + r = q // 2 + h = q - r * 2 + c16 = 2 * rk + h + w = ptx.lds_v4_u32(hst + r * 32 + h * 16) + dst = ptx.mapa_u32(sb2 + rho * HIMG_B_BYTES + r * 128 + ((c16 ^ r) * 16), d) + ptx.st_cluster_v4_u32(dst, w[0], w[1], w[2], w[3]) + ptx.fence_proxy_async_shared_cluster() + if (tidx >= 4 * 2 * NTOK) & (tidx < 4 * 3 * NTOK): + q = tidx - 4 * 2 * NTOK + d = q // NTOK + r = q - d * NTOK + w1 = ptx.lds_u32(hst + HSTAGE_SF + r * 4) + dst = ptx.mapa_u32( + ssfb2 + rho * SFB_STAGE_STRIDE + rk * SFB_CHUNK_STRIDE + r * 16, d + ) + ptx.st_cluster_u32(dst, w1) + ptx.fence_proxy_async_shared_cluster() + # all operand stores (and staging reads) precede the arrivals; + # the staging buffer is free for the next round afterwards + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + if tidx < CLUSTER: + ptx.mbar_arrive_remote(ptx.mapa_u32(b2bar + rho * 8, tidx)) + if cutlass.const_expr(rho == 0): + if tidx == 0: + _stamp(tl, tl_s, XT_EPI_FC1_PUB) + else: + if tidx == 0: + _stamp(tl, tl_s, XT_EPI_FC1_PUB_R1) + + # ---------------- FC2 epilogue: raw rows t * 8 + k ---------------- + hid = warp_idx * 32 + (lane % 8) * 4 + lane // 8 + first = Int32(1) + for rho in cutlass.range_constexpr(MAX_ROUNDS): + if rho < rounds: + slot = slots[rho] + expert = s_slot_expert[slot] + mask = s_slot_mask[slot] + a2 = ptx.ld_nc_f32(alpha2_addr + expert * 4) + kk = cute.make_rmem_tensor((NTOK,), Int32) + for n in cutlass.range_constexpr(NTOK): + kk[n] = s_slot_k[slot * NTOK + n] + t = t_lo[rho] + while t < t_hi[rho]: + acc_pipe.consumer_wait(acc_state) + if tidx == 0: + if first != 0: + _stamp(tl, tl_s, XT_FC2_FIRST_ACC) + first = Int32(0) + vals = ptx.tmem_ld_32x32b_x8( + Int32(tCtAcc_base[(None, None, None, acc_state.index)].iterator.toint()) + + lane_off + ) + cute.arch.fence_view_async_tmem_load() + with cute.arch.elect_one(): + acc_pipe.consumer_release(acc_state) + acc_state.advance() + for n in cutlass.range_constexpr(NTOK): + if ((mask >> Uint32(n)) & Uint32(1)) != Uint32(0): + y = ptx.bf16_round(vals[n] * a2) + if cutlass.const_expr(self.red_out): + w = s_topk_w[n * TOP_K + kk[n]] + ptx.red_add_f32(out_addr + (n * HIDDEN + t * 128 + hid) * 4, w * y) + else: + row = n * TOP_K + kk[n] + ptx.st_u16( + rows_addr + (row * HIDDEN + t * 128 + hid) * 2, + ptx.bf16_bits(y), + ) + t = t + 1 + if cutlass.const_expr(rho == 0): + if tidx == 0: + _stamp(tl, tl_s, XT_FC2_R0_DONE) + if tidx == 0: + _stamp(tl, tl_s, XT_FC2_DONE) + if cutlass.const_expr(self.finalize): + self._xc_finalize_last_arriver( + tidx, + rounds, + t_lo, + t_hi, + num_slots, + num_tokens, + s_topk_w, + rows_addr, + out_addr, + counter_addr, + storage, + ) + # the successor (finalize or the post-MoE hop) may start launching + if cutlass.const_expr(self.trigger == "late"): + cute.arch.griddepcontrol_launch_dependents() + + @cute.jit + def _xc_finalize_last_arriver( + self, + tidx, + rounds, + t_lo, + t_hi, + num_slots, + num_tokens, + s_topk_w, + rows_addr, + out_addr, + counter_addr, + storage, + ): + """In-kernel native finalize, by the last contributor of each tile. + + Every (expert, output tile) unit is computed by exactly one CTA, so + an output tile is complete once ``num_slots`` units have counted in. + After its raw rows are stored, a CTA releases them (one GPU-scope + fence), counts each of its units into the tile's counter, and + finalizes every tile for which it was the last arrival: FP32 FMA over + the top-k in order and one BF16 rounding (the native finalize), then + resets the counter for the next launch. Nobody waits for anyone. + """ + # every thread releases its own row stores, then the units count in + ptx.fence_acq_rel_gpu() + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + closed_base = Int32(storage.closed.data_ptr().toint()) + u = Int32(0) + for rho in cutlass.range_constexpr(MAX_ROUNDS): + if rho < rounds: + n = t_hi[rho] - t_lo[rho] + if (tidx >= u) & (tidx < u + n): + t = t_lo[rho] + (tidx - u) + old = ptx.atom_add_relaxed_gpu_u32(counter_addr + t * 4, Uint32(1)) + closed = Int32(0) + if old + Uint32(1) == Uint32(num_slots): + closed = Int32(1) + ptx.st_relaxed_gpu_u32(counter_addr + t * 4, Uint32(0)) + ptx.fence_acq_rel_gpu() + ptx.sts_u32(closed_base + tidx * 4, Uint32(closed)) + u = u + n + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + # one (token, 8 columns) task per thread: 16 x num_tokens <= 128 + n_tok = tidx // 16 + c8 = (tidx % 16) * 8 + u = Int32(0) + for rho in cutlass.range_constexpr(MAX_ROUNDS): + if rho < rounds: + n = t_hi[rho] - t_lo[rho] + i = Int32(0) + while i < n: + if ptx.lds_u32(closed_base + (u + i) * 4) != Uint32(0): + if n_tok < num_tokens: + col = (t_lo[rho] + i) * 128 + c8 + acc = cute.make_rmem_tensor((8,), Float32) + for j in cutlass.range_constexpr(8): + acc[j] = Float32(0.0) + for k in cutlass.range_constexpr(TOP_K): + w = s_topk_w[n_tok * TOP_K + k] + y = ptx.ld_ll2(rows_addr + ((n_tok * TOP_K + k) * HIDDEN + col) * 2) + for j in cutlass.range_constexpr(4): + acc[2 * j] = ptx.fma_rn( + w, ptx.bf16x2_to_f32(y[j], False), acc[2 * j] + ) + acc[2 * j + 1] = ptx.fma_rn( + w, ptx.bf16x2_to_f32(y[j], True), acc[2 * j + 1] + ) + ptx.st_v4_u32( + out_addr + (n_tok * HIDDEN + col) * 2, + ptx.pack_bf16x2(acc[0], acc[1]), + ptx.pack_bf16x2(acc[2], acc[3]), + ptx.pack_bf16x2(acc[4], acc[5]), + ptx.pack_bf16x2(acc[6], acc[7]), + ) + i = i + 1 + u = u + n + + @cute.jit + def _xc_delay(self, salt, bidx): + """Stress testing: a pseudo-random third of the CTAs (re-drawn per + launch from the global timer) spins 1..6 units of ``delay_ns``.""" + t0 = ptx.globaltimer_u64() + seed = Uint32(Int32(t0 >> Int64(12)) & Int32(0xFFFF)) + h = (Uint32(bidx) * Uint32(37) + seed * Uint32(101) + Uint32(salt)) % Uint32(17) + if h < Uint32(6): + d = Int64(h + Uint32(1)) * Int64(self.delay_ns) + t = t0 + while t - t0 < d: + t = ptx.globaltimer_u64() + + @cute.jit + def _swiglu_quant_to_stage(self, v, warp_idx, lane, mask, a_gate, s_up, num_tokens, hst): + """SwiGLU + NVFP4 quantization of one 128-row FC1 tile into the + staging buffer: 8 rows x 32 FP4 bytes, then 8 rows x 4 scale bytes. + + Row ``32w + q`` of the stock layout holds intermediate index + ``64*mt + 16*w + j`` with ``j = 2*(q % 8) + (q // 16)`` as the up row + when ``q % 16 < 8`` and its gate row at lane ``q + 8``, so one warp + owns exactly one 16-element scale block of the intermediate. Tokens + not routed to the expert get zero bytes and zero scales. + """ + is_up = (lane % 16) < 8 + rcp6 = ptx.rcp_approx_ftz(Float32(6.0)) + for n in cutlass.range_constexpr(NTOK): + gate_raw = cute.arch.shuffle_sync_down(v[n], 8) + gr = gate_raw * a_gate + up = v[n] * s_up + sig = ptx.rcp_approx_ftz( + Float32(1.0) + ptx.ex2_approx(gr * Float32(-1.4426950408889634)) + ) + h = Float32(0.0) + if is_up: + h = up * (gr * sig) + am = cute.math.absf(h) + am = cute.arch.fmax(am, cute.arch.shuffle_sync_bfly(am, 1)) + am = cute.arch.fmax(am, cute.arch.shuffle_sync_bfly(am, 2)) + am = cute.arch.fmax(am, cute.arch.shuffle_sync_bfly(am, 4)) + am = cute.arch.fmax(am, cute.arch.shuffle_sync_bfly(am, 16)) + sf = ptx.cvt_e4m3_rn_sat(am * rcp6) + sf_f = ptx.e4m3_to_f32(sf) + osc = Float32(0.0) + if am != Float32(0.0): + osc = ptx.rcp_approx_ftz(sf_f) + partner = cute.arch.shuffle_sync_down(h, 16) + byte = ptx.cvt_e2m1x2(h * osc, partner * osc) + routed = (((mask >> Uint32(n)) & Uint32(1)) != Uint32(0)) & (n < num_tokens) + if not routed: + byte = Uint32(0) + sf = Uint32(0) + if lane < 8: + ptx.sts_u8(hst + n * 32 + warp_idx * 8 + lane, byte) + if lane == 0: + ptx.sts_u8(hst + HSTAGE_SF + n * 4 + warp_idx, sf) + + +class RoutedMoeFinalizeKernel: + """Native finalize of the raw tuple: ``out[t] = bf16(sum_k fma(w[t, k], + rows[t * 8 + k]))`` in top-k order with FP32 accumulation; each thread owns + 8 hidden columns of one token. Launched with programmatic dependent launch + right after the routed kernel. + + ``delay_ns`` (stress testing only, 0 compiles it out): a pseudo-random + third of the CTAs, re-drawn every launch from the global timer, spins 1..6 + units before storing, so a consumer that read the output before this + grid completed would see stale rows. + """ + + THREADS = 128 + + def __init__(self, delay_ns: int = 0): + self.delay_ns = delay_ns + + @cute.jit + def run(self, rows_addr: Int64, scale_addr: Int64, out_addr: Int64, num_tokens: Int32, stream): + self.kernel(rows_addr, scale_addr, out_addr).launch( + grid=[HIDDEN // (8 * self.THREADS), num_tokens, 1], + block=[self.THREADS, 1, 1], + stream=stream, + use_pdl=True, + ) + + @cute.kernel + def kernel(self, rows_addr: Int64, scale_addr: Int64, out_addr: Int64): + tidx, _, _ = cute.arch.thread_idx() + bx, t, _ = cute.arch.block_idx() + col = (Int32(bx) * self.THREADS + Int32(tidx)) * 8 + t = Int32(t) + cute.arch.griddepcontrol_wait() + acc = cute.make_rmem_tensor((8,), Float32) + for i in cutlass.range_constexpr(8): + acc[i] = Float32(0.0) + # written by the routed kernel: plain (coherent) loads after the wait + ws = ptx.ld_v4_u32(scale_addr + t * (TOP_K * 2)) + for k in cutlass.range_constexpr(TOP_K): + w = ptx.bf16x2_to_f32(ws[k // 2], k % 2 == 1) + y = ptx.ld_v4_u32(rows_addr + ((t * TOP_K + k) * HIDDEN + col) * 2) + for i in cutlass.range_constexpr(4): + acc[2 * i] = ptx.fma_rn(w, ptx.bf16x2_to_f32(y[i], False), acc[2 * i]) + acc[2 * i + 1] = ptx.fma_rn(w, ptx.bf16x2_to_f32(y[i], True), acc[2 * i + 1]) + if cutlass.const_expr(self.delay_ns > 0): + t0 = ptx.globaltimer_u64() + seed = Uint32(Int32(t0 >> Int64(12)) & Int32(0xFFFF)) + h = ( + Uint32(bx + t * 8) * Uint32(37) + seed * Uint32(101) + Uint32(DELAY_FINALIZE) + ) % Uint32(17) + if h < Uint32(6): + d = Int64(h + Uint32(1)) * Int64(self.delay_ns) + tn = t0 + while tn - t0 < d: + tn = ptx.globaltimer_u64() + ptx.st_v4_u32( + out_addr + (t * HIDDEN + col) * 2, + ptx.pack_bf16x2(acc[0], acc[1]), + ptx.pack_bf16x2(acc[2], acc[3]), + ptx.pack_bf16x2(acc[4], acc[5]), + ptx.pack_bf16x2(acc[6], acc[7]), + ) + cute.arch.griddepcontrol_launch_dependents() diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/ptx.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/ptx.py new file mode 100644 index 000000000000..556b138389c9 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/ptx.py @@ -0,0 +1,844 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Inline-PTX primitives used by the routed-MoE kernel.""" + +from cutlass._mlir.dialects import llvm +from cutlass.cutlass_dsl import Float32, Int32, Int64, T, Uint32, dsl_user_op + + +def _asm(res, operands, text, constraints, *, loc=None, ip=None): + return llvm.inline_asm( + res, + operands, + text, + constraints, + has_side_effects=True, + is_align_stack=False, + asm_dialect=llvm.AsmDialect.AD_ATT, + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def f32_bits(x: Float32, *, loc=None, ip=None) -> Uint32: + return Uint32(llvm.bitcast(T.i32(), Float32(x).ir_value(loc=loc, ip=ip), loc=loc, ip=ip)) + + +@dsl_user_op +def bits_f32(x: Uint32, *, loc=None, ip=None) -> Float32: + return Float32(llvm.bitcast(T.f32(), Uint32(x).ir_value(loc=loc, ip=ip), loc=loc, ip=ip)) + + +@dsl_user_op +def redux_max_u32(x: Uint32, *, loc=None, ip=None) -> Uint32: + """Warp-wide unsigned max (all 32 lanes must participate).""" + return Uint32( + _asm( + T.i32(), + [Uint32(x).ir_value(loc=loc, ip=ip)], + "redux.sync.max.u32 $0, $1, 0xffffffff;", + "=r,r", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def cvt_e4m3_rn_sat(x: Float32, *, loc=None, ip=None) -> Uint32: + """FP32 -> E4M3 (RN, satfinite), returned in the low byte.""" + return Uint32( + _asm( + T.i32(), + [Float32(x).ir_value(loc=loc, ip=ip)], + "{\n.reg .b16 h;\n.reg .f32 z;\n" + "mov.f32 z, 0f00000000;\n" + "cvt.rn.satfinite.e4m3x2.f32 h, z, $1;\n" + "cvt.u32.u16 $0, h;\n}", + "=r,f", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def e4m3_to_f32(byte: Uint32, *, loc=None, ip=None) -> Float32: + """E4M3 in the low byte -> FP32 (exact).""" + return Float32( + _asm( + T.f32(), + [Uint32(byte).ir_value(loc=loc, ip=ip)], + "{\n.reg .b16 lo;\n.reg .b32 hx2;\n.reg .b16 h0, h1;\n" + "cvt.u16.u32 lo, $1;\n" + "cvt.rn.f16x2.e4m3x2 hx2, lo;\n" + "mov.b32 {h0, h1}, hx2;\n" + "cvt.f32.f16 $0, h0;\n}", + "=f,r", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def cvt_e2m1x2(lo: Float32, hi: Float32, *, loc=None, ip=None) -> Uint32: + """Two FP32 -> one byte of E2M1 (RN, satfinite); ``lo`` in bits 0..3.""" + return Uint32( + _asm( + T.i32(), + [Float32(hi).ir_value(loc=loc, ip=ip), Float32(lo).ir_value(loc=loc, ip=ip)], + "{\n.reg .b8 b;\n.reg .b32 t;\n" + "cvt.rn.satfinite.e2m1x2.f32 b, $1, $2;\n" + "mov.b32 t, {b, b, b, b};\n" + "and.b32 $0, t, 255;\n}", + "=r,f,f", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def rcp_approx_ftz(x: Float32, *, loc=None, ip=None) -> Float32: + return Float32( + _asm( + T.f32(), + [Float32(x).ir_value(loc=loc, ip=ip)], + "rcp.approx.ftz.f32 $0, $1;", + "=f,f", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def ex2_approx(x: Float32, *, loc=None, ip=None) -> Float32: + return Float32( + _asm( + T.f32(), + [Float32(x).ir_value(loc=loc, ip=ip)], + "ex2.approx.ftz.f32 $0, $1;", + "=f,f", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def bf16_round(x: Float32, *, loc=None, ip=None) -> Float32: + """Round FP32 to BF16 (RN) and back to FP32.""" + return Float32( + _asm( + T.f32(), + [Float32(x).ir_value(loc=loc, ip=ip)], + "{\n.reg .b16 h;\ncvt.rn.bf16.f32 h, $1;\ncvt.f32.bf16 $0, h;\n}", + "=f,f", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def bf16_bits(x: Float32, *, loc=None, ip=None) -> Uint32: + """FP32 -> BF16 (RN) bits in the low half.""" + return Uint32( + _asm( + T.i32(), + [Float32(x).ir_value(loc=loc, ip=ip)], + "{\n.reg .b16 h;\ncvt.rn.bf16.f32 h, $1;\ncvt.u32.u16 $0, h;\n}", + "=r,f", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def bf16x2_to_f32(packed: Uint32, upper: bool, *, loc=None, ip=None) -> Float32: + """One BF16 lane of a packed pair -> FP32 (exact).""" + text = ( + "{\n.reg .b32 t;\nand.b32 t, $1, 0xFFFF0000;\nmov.b32 $0, t;\n}" + if upper + else "{\n.reg .b32 t;\nshl.b32 t, $1, 16;\nmov.b32 $0, t;\n}" + ) + return Float32( + _asm(T.f32(), [Uint32(packed).ir_value(loc=loc, ip=ip)], text, "=f,r", loc=loc, ip=ip) + ) + + +@dsl_user_op +def tmem_ld_32x32b_x8(taddr: Int32, *, loc=None, ip=None): + """tcgen05.ld 32x32b.x8: this thread's lane, 8 consecutive columns.""" + res = _asm( + llvm.StructType.get_literal([T.f32()] * 8), + [Int32(taddr).ir_value(loc=loc, ip=ip)], + "tcgen05.ld.sync.aligned.32x32b.x8.b32 " + "{$0, $1, $2, $3, $4, $5, $6, $7}, [$8];\n" + "tcgen05.wait::ld.sync.aligned;", + "=f,=f,=f,=f,=f,=f,=f,=f,r", + loc=loc, + ip=ip, + ) + return [Float32(llvm.extractvalue(T.f32(), res, [i], loc=loc, ip=ip)) for i in range(8)] + + +@dsl_user_op +def st_relaxed_gpu_u32(addr: Int64, value: Uint32, *, loc=None, ip=None): + _asm( + None, + [Int64(addr).ir_value(loc=loc, ip=ip), Uint32(value).ir_value(loc=loc, ip=ip)], + "st.relaxed.gpu.global.u32 [$0], $1;", + "l,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def ld_relaxed_gpu_u32(addr: Int64, *, loc=None, ip=None) -> Uint32: + return Uint32( + _asm( + T.i32(), + [Int64(addr).ir_value(loc=loc, ip=ip)], + "ld.relaxed.gpu.global.u32 $0, [$1];", + "=r,l", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def st_v4_u32(addr: Int64, a: Uint32, b: Uint32, c: Uint32, d: Uint32, *, loc=None, ip=None): + _asm( + None, + [ + Int64(addr).ir_value(loc=loc, ip=ip), + Uint32(a).ir_value(loc=loc, ip=ip), + Uint32(b).ir_value(loc=loc, ip=ip), + Uint32(c).ir_value(loc=loc, ip=ip), + Uint32(d).ir_value(loc=loc, ip=ip), + ], + "st.global.v4.u32 [$0], {$1, $2, $3, $4};", + "l,r,r,r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def st_u32(addr: Int64, value: Uint32, *, loc=None, ip=None): + _asm( + None, + [Int64(addr).ir_value(loc=loc, ip=ip), Uint32(value).ir_value(loc=loc, ip=ip)], + "st.global.u32 [$0], $1;", + "l,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def st_u16(addr: Int64, value: Uint32, *, loc=None, ip=None): + _asm( + None, + [Int64(addr).ir_value(loc=loc, ip=ip), Uint32(value).ir_value(loc=loc, ip=ip)], + "{\n.reg .b16 h;\ncvt.u16.u32 h, $1;\nst.global.u16 [$0], h;\n}", + "l,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def red_add_f32(addr: Int64, value: Float32, *, loc=None, ip=None): + """Relaxed GPU-scope FP32 atomic add without a return value.""" + _asm( + None, + [Int64(addr).ir_value(loc=loc, ip=ip), Float32(value).ir_value(loc=loc, ip=ip)], + "red.relaxed.gpu.global.add.f32 [$0], $1;", + "l,f", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def st_f32(addr: Int64, value: Float32, *, loc=None, ip=None): + _asm( + None, + [Int64(addr).ir_value(loc=loc, ip=ip), Float32(value).ir_value(loc=loc, ip=ip)], + "st.global.f32 [$0], $1;", + "l,f", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def sts_u32(smem_addr: Int32, value: Uint32, *, loc=None, ip=None): + _asm( + None, + [Int32(smem_addr).ir_value(loc=loc, ip=ip), Uint32(value).ir_value(loc=loc, ip=ip)], + "st.shared.u32 [$0], $1;", + "r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def sts_u8(smem_addr: Int32, value: Uint32, *, loc=None, ip=None): + _asm( + None, + [Int32(smem_addr).ir_value(loc=loc, ip=ip), Uint32(value).ir_value(loc=loc, ip=ip)], + "{\n.reg .b16 h;\ncvt.u16.u32 h, $1;\nst.shared.u8 [$0], h;\n}", + "r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def lds_u32(smem_addr: Int32, *, loc=None, ip=None) -> Uint32: + return Uint32( + _asm( + T.i32(), + [Int32(smem_addr).ir_value(loc=loc, ip=ip)], + "ld.shared.u32 $0, [$1];", + "=r,r", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def atom_or_shared_u32(smem_addr: Int32, value: Uint32, *, loc=None, ip=None): + _asm( + None, + [Int32(smem_addr).ir_value(loc=loc, ip=ip), Uint32(value).ir_value(loc=loc, ip=ip)], + "red.shared.or.b32 [$0], $1;", + "r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def popc_u32(x: Uint32, *, loc=None, ip=None) -> Int32: + return Int32( + _asm( + T.i32(), + [Uint32(x).ir_value(loc=loc, ip=ip)], + "popc.b32 $0, $1;", + "=r,r", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def ld_nc_f32(addr: Int64, *, loc=None, ip=None) -> Float32: + return Float32( + _asm( + T.f32(), + [Int64(addr).ir_value(loc=loc, ip=ip)], + "ld.global.nc.f32 $0, [$1];", + "=f,l", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def ld_nc_u16(addr: Int64, *, loc=None, ip=None) -> Uint32: + return Uint32( + _asm( + T.i32(), + [Int64(addr).ir_value(loc=loc, ip=ip)], + "{\n.reg .b16 h;\nld.global.nc.u16 h, [$1];\ncvt.u32.u16 $0, h;\n}", + "=r,l", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def lds_v4_u32(smem_addr: Int32, *, loc=None, ip=None): + res = _asm( + llvm.StructType.get_literal([T.i32()] * 4), + [Int32(smem_addr).ir_value(loc=loc, ip=ip)], + "ld.shared.v4.u32 {$0, $1, $2, $3}, [$4];", + "=r,=r,=r,=r,r", + loc=loc, + ip=ip, + ) + return [Uint32(llvm.extractvalue(T.i32(), res, [i], loc=loc, ip=ip)) for i in range(4)] + + +@dsl_user_op +def fence_acq_rel_gpu(*, loc=None, ip=None): + _asm(None, [], "fence.acq_rel.gpu;", "", loc=loc, ip=ip) + + +@dsl_user_op +def fence_proxy_async_shared(*, loc=None, ip=None): + _asm(None, [], "fence.proxy.async.shared::cta;", "", loc=loc, ip=ip) + + +@dsl_user_op +def bar_sync(bar_id: int, num_threads: int, *, loc=None, ip=None): + _asm( + None, + [Int32(bar_id).ir_value(loc=loc, ip=ip), Int32(num_threads).ir_value(loc=loc, ip=ip)], + "bar.sync $0, $1;", + "r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def globaltimer_u64(*, loc=None, ip=None) -> Int64: + return Int64(_asm(T.i64(), [], "mov.u64 $0, %globaltimer;", "=l", loc=loc, ip=ip)) + + +@dsl_user_op +def clock64(*, loc=None, ip=None) -> Int64: + return Int64(_asm(T.i64(), [], "mov.u64 $0, %clock64;", "=l", loc=loc, ip=ip)) + + +@dsl_user_op +def st_u64(addr: Int64, value: Int64, *, loc=None, ip=None): + _asm( + None, + [Int64(addr).ir_value(loc=loc, ip=ip), Int64(value).ir_value(loc=loc, ip=ip)], + "st.global.u64 [$0], $1;", + "l,l", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def bulk_g2s(smem_dst: Int32, gmem_src: Int64, nbytes: Int32, mbar: Int32, *, loc=None, ip=None): + """cp.async.bulk global -> shared, completing on ``mbar`` (tx bytes).""" + _asm( + None, + [ + Int32(smem_dst).ir_value(loc=loc, ip=ip), + Int64(gmem_src).ir_value(loc=loc, ip=ip), + Int32(nbytes).ir_value(loc=loc, ip=ip), + Int32(mbar).ir_value(loc=loc, ip=ip), + ], + "cp.async.bulk.shared::cluster.global.mbarrier::complete_tx::bytes [$0], [$1], $2, [$3];", + "r,l,r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def mbar_arrive_expect_tx(mbar: Int32, nbytes: Int32, *, loc=None, ip=None): + _asm( + None, + [Int32(mbar).ir_value(loc=loc, ip=ip), Int32(nbytes).ir_value(loc=loc, ip=ip)], + "mbarrier.arrive.expect_tx.shared::cta.b64 _, [$0], $1;", + "r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def sts_u64(smem_addr: Int32, value: Int64, *, loc=None, ip=None): + _asm( + None, + [Int32(smem_addr).ir_value(loc=loc, ip=ip), Int64(value).ir_value(loc=loc, ip=ip)], + "st.shared.u64 [$0], $1;", + "r,l", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def lds_u64(smem_addr: Int32, *, loc=None, ip=None) -> Int64: + return Int64( + _asm( + T.i64(), + [Int32(smem_addr).ir_value(loc=loc, ip=ip)], + "ld.shared.u64 $0, [$1];", + "=l,r", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def cvt_e2m1x8( + v0: Float32, + v1: Float32, + v2: Float32, + v3: Float32, + v4: Float32, + v5: Float32, + v6: Float32, + v7: Float32, + *, + loc=None, + ip=None, +) -> Uint32: + """Eight FP32 -> eight E2M1 (RN, satfinite); ``v0`` in bits 0..3.""" + ops = [Float32(v).ir_value(loc=loc, ip=ip) for v in (v1, v0, v3, v2, v5, v4, v7, v6)] + return Uint32( + _asm( + T.i32(), + ops, + "{\n.reg .b8 b0, b1, b2, b3;\n" + "cvt.rn.satfinite.e2m1x2.f32 b0, $1, $2;\n" + "cvt.rn.satfinite.e2m1x2.f32 b1, $3, $4;\n" + "cvt.rn.satfinite.e2m1x2.f32 b2, $5, $6;\n" + "cvt.rn.satfinite.e2m1x2.f32 b3, $7, $8;\n" + "mov.b32 $0, {b0, b1, b2, b3};\n}", + "=r,f,f,f,f,f,f,f,f", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def st_ll2(addr: Int64, d0: Uint32, d1: Uint32, flag: Uint32, *, loc=None, ip=None): + """Two LL words ``{d0, flag}, {d1, flag}`` in one 16-byte store. + + LL protocol (as NCCL's LL): every 4-byte payload travels with the epoch + of the launch that wrote it, so a reader that sees the expected epoch in + an 8-byte half also sees that half's payload; no fence or flag is needed. + GPU-scope relaxed (not volatile, which is system scope) so the stores + and polls stay within the GPU's L2. + """ + _asm( + None, + [ + Int64(addr).ir_value(loc=loc, ip=ip), + Uint32(d0).ir_value(loc=loc, ip=ip), + Uint32(flag).ir_value(loc=loc, ip=ip), + Uint32(d1).ir_value(loc=loc, ip=ip), + ], + "st.relaxed.gpu.global.v4.u32 [$0], {$1, $2, $3, $2};", + "l,r,r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def ld_ll2(addr: Int64, *, loc=None, ip=None): + """16-byte poll of two LL words: ``[d0, flag0, d1, flag1]``.""" + res = _asm( + llvm.StructType.get_literal([T.i32()] * 4), + [Int64(addr).ir_value(loc=loc, ip=ip)], + "ld.relaxed.gpu.global.v4.u32 {$0, $1, $2, $3}, [$4];", + "=r,=r,=r,=r,l", + loc=loc, + ip=ip, + ) + return [Uint32(llvm.extractvalue(T.i32(), res, [i], loc=loc, ip=ip)) for i in range(4)] + + +@dsl_user_op +def prefetch_l2_bulk(addr: Int64, nbytes: Int32, *, loc=None, ip=None): + """Bulk L2 prefetch of ``nbytes`` (multiple of 16) contiguous bytes.""" + _asm( + None, + [Int64(addr).ir_value(loc=loc, ip=ip), Int32(nbytes).ir_value(loc=loc, ip=ip)], + "cp.async.bulk.prefetch.L2.global [$0], $1;", + "l,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def fence_proxy_async_global(*, loc=None, ip=None): + """Order generic-proxy global accesses with async-proxy (bulk copy) + accesses of this thread.""" + _asm(None, [], "fence.proxy.async.global;", "", loc=loc, ip=ip) + + +@dsl_user_op +def ld_acquire_gpu_u32(addr: Int64, *, loc=None, ip=None) -> Uint32: + return Uint32( + _asm( + T.i32(), + [Int64(addr).ir_value(loc=loc, ip=ip)], + "ld.acquire.gpu.global.u32 $0, [$1];", + "=r,l", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def st_ll1(addr: Int64, d0: Uint32, flag: Uint32, *, loc=None, ip=None): + """One LL word ``{d0, flag}`` (8-byte GPU-scope relaxed store).""" + _asm( + None, + [ + Int64(addr).ir_value(loc=loc, ip=ip), + Uint32(d0).ir_value(loc=loc, ip=ip), + Uint32(flag).ir_value(loc=loc, ip=ip), + ], + "st.relaxed.gpu.global.v2.u32 [$0], {$1, $2};", + "l,r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def lds_v2_u32(smem_addr: Int32, *, loc=None, ip=None): + res = _asm( + llvm.StructType.get_literal([T.i32()] * 2), + [Int32(smem_addr).ir_value(loc=loc, ip=ip)], + "ld.shared.v2.u32 {$0, $1}, [$2];", + "=r,=r,r", + loc=loc, + ip=ip, + ) + return [Uint32(llvm.extractvalue(T.i32(), res, [i], loc=loc, ip=ip)) for i in range(2)] + + +@dsl_user_op +def sts_v2_u32(smem_addr: Int32, a: Uint32, b: Uint32, *, loc=None, ip=None): + _asm( + None, + [ + Int32(smem_addr).ir_value(loc=loc, ip=ip), + Uint32(a).ir_value(loc=loc, ip=ip), + Uint32(b).ir_value(loc=loc, ip=ip), + ], + "st.shared.v2.u32 [$0], {$1, $2};", + "r,r,r", + loc=loc, + ip=ip, + ) + + +# --------------------------------------------------------------------------- +# Thread block cluster primitives (expert-cluster schedule) +# --------------------------------------------------------------------------- +@dsl_user_op +def cluster_ctarank(*, loc=None, ip=None) -> Int32: + return Int32(_asm(T.i32(), [], "mov.u32 $0, %cluster_ctarank;", "=r", loc=loc, ip=ip)) + + +@dsl_user_op +def mapa_u32(smem_addr: Int32, cta_rank: Int32, *, loc=None, ip=None) -> Int32: + """Shared::cluster address of ``smem_addr`` in cluster CTA ``cta_rank``.""" + return Int32( + _asm( + T.i32(), + [Int32(smem_addr).ir_value(loc=loc, ip=ip), Int32(cta_rank).ir_value(loc=loc, ip=ip)], + "mapa.shared::cluster.u32 $0, $1, $2;", + "=r,r,r", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def st_cluster_v4_u32( + cluster_addr: Int32, a: Uint32, b: Uint32, c: Uint32, d: Uint32, *, loc=None, ip=None +): + _asm( + None, + [Int32(cluster_addr).ir_value(loc=loc, ip=ip)] + + [Uint32(v).ir_value(loc=loc, ip=ip) for v in (a, b, c, d)], + "st.shared::cluster.v4.u32 [$0], {$1, $2, $3, $4};", + "r,r,r,r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def st_cluster_u32(cluster_addr: Int32, value: Uint32, *, loc=None, ip=None): + _asm( + None, + [Int32(cluster_addr).ir_value(loc=loc, ip=ip), Uint32(value).ir_value(loc=loc, ip=ip)], + "st.shared::cluster.u32 [$0], $1;", + "r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def fence_proxy_async_shared_cluster(*, loc=None, ip=None): + """Order this thread's generic-proxy shared::cluster stores before later + async-proxy (UMMA, tcgen05.cp) reads of the same bytes.""" + _asm(None, [], "fence.proxy.async.shared::cluster;", "", loc=loc, ip=ip) + + +@dsl_user_op +def mbar_arrive_remote(cluster_addr: Int32, *, loc=None, ip=None): + """Release-arrive (cluster scope) on an mbarrier of a cluster CTA.""" + _asm( + None, + [Int32(cluster_addr).ir_value(loc=loc, ip=ip)], + "mbarrier.arrive.release.cluster.shared::cluster.b64 _, [$0];", + "r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def mbar_try_wait_cluster(mbar: Int32, phase: Int32, *, loc=None, ip=None) -> Int32: + """One acquire (cluster scope) try-wait on a local mbarrier phase; 1 when done.""" + return Int32( + _asm( + T.i32(), + [Int32(mbar).ir_value(loc=loc, ip=ip), Int32(phase).ir_value(loc=loc, ip=ip)], + "{\n.reg .pred p;\n" + "mbarrier.try_wait.parity.acquire.cluster.shared::cta.b64 p, [$1], $2;\n" + "selp.u32 $0, 1, 0, p;\n}", + "=r,r,r", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def ld_nc_v4_u32(addr: Int64, *, loc=None, ip=None): + res = _asm( + llvm.StructType.get_literal([T.i32()] * 4), + [Int64(addr).ir_value(loc=loc, ip=ip)], + "ld.global.nc.v4.u32 {$0, $1, $2, $3}, [$4];", + "=r,=r,=r,=r,l", + loc=loc, + ip=ip, + ) + return [Uint32(llvm.extractvalue(T.i32(), res, [i], loc=loc, ip=ip)) for i in range(4)] + + +@dsl_user_op +def ld_v4_u32(addr: Int64, *, loc=None, ip=None): + """Plain (coherent) 16-byte global load.""" + res = _asm( + llvm.StructType.get_literal([T.i32()] * 4), + [Int64(addr).ir_value(loc=loc, ip=ip)], + "ld.global.v4.u32 {$0, $1, $2, $3}, [$4];", + "=r,=r,=r,=r,l", + loc=loc, + ip=ip, + ) + return [Uint32(llvm.extractvalue(T.i32(), res, [i], loc=loc, ip=ip)) for i in range(4)] + + +@dsl_user_op +def fma_rn(a: Float32, b: Float32, c: Float32, *, loc=None, ip=None) -> Float32: + return Float32( + _asm( + T.f32(), + [Float32(v).ir_value(loc=loc, ip=ip) for v in (a, b, c)], + "fma.rn.f32 $0, $1, $2, $3;", + "=f,f,f,f", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def pack_bf16x2(lo: Float32, hi: Float32, *, loc=None, ip=None) -> Uint32: + """Two FP32 values -> one word of two BF16 (RN), ``lo`` in the low half.""" + return Uint32( + _asm( + T.i32(), + [Float32(lo).ir_value(loc=loc, ip=ip), Float32(hi).ir_value(loc=loc, ip=ip)], + "cvt.rn.bf16x2.f32 $0, $2, $1;", + "=r,f,f", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def atom_add_relaxed_gpu_u32(addr: Int64, value: Uint32, *, loc=None, ip=None) -> Uint32: + return Uint32( + _asm( + T.i32(), + [Int64(addr).ir_value(loc=loc, ip=ip), Uint32(value).ir_value(loc=loc, ip=ip)], + "atom.relaxed.gpu.global.add.u32 $0, [$1], $2;", + "=r,l,r", + loc=loc, + ip=ip, + ) + ) + + +@dsl_user_op +def smid_u32(*, loc=None, ip=None) -> Int32: + return Int32(_asm(T.i32(), [], "mov.u32 $0, %smid;", "=r", loc=loc, ip=ip)) + + +@dsl_user_op +def bar_arrive(bar_id: int, num_threads: int, *, loc=None, ip=None): + _asm( + None, + [Int32(bar_id).ir_value(loc=loc, ip=ip), Int32(num_threads).ir_value(loc=loc, ip=ip)], + "bar.arrive $0, $1;", + "r,r", + loc=loc, + ip=ip, + ) + + +@dsl_user_op +def bulk_s2cluster( + dst_cluster: Int32, src_cta: Int32, nbytes: Int32, mbar_cluster: Int32, *, loc=None, ip=None +): + """cp.async.bulk shared::cta -> shared::cluster (a peer CTA), completing + on the peer's mbarrier (tx bytes).""" + _asm( + None, + [ + Int32(dst_cluster).ir_value(loc=loc, ip=ip), + Int32(src_cta).ir_value(loc=loc, ip=ip), + Int32(nbytes).ir_value(loc=loc, ip=ip), + Int32(mbar_cluster).ir_value(loc=loc, ip=ip), + ], + "cp.async.bulk.shared::cluster.shared::cta.mbarrier::complete_tx::bytes [$0], [$1], $2, [$3];", + "r,r,r,r", + loc=loc, + ip=ip, + ) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/routed_moe.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/routed_moe.py new file mode 100644 index 000000000000..c1a8096cf8ed --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/routed_moe/routed_moe.py @@ -0,0 +1,880 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Host side of the routed_moe routed NVFP4 MoE (the routed_moe contract). + +``RoutedMoe`` wraps one stock TRT-LLM-Gen NVFP4 MoE leaf (one rank's TP +shard) and runs the single-launch CuTe DSL kernel on its weight buffers in +place: no packed copies, no layout conversion. The call site may pass the +served ``ConfigurableMoE`` wrapper or the bare leaf; the leaf that owns the +expert parameters is ``wrapper.backend``. The schedule is the +expert-cluster kernel (``kernel_xc.py``, raw tuple, plus a native-order +finalize kernel for finalized output); a call whose rows need more +co-resident clusters than the grid holds runs the grid-wide kernel +(``kernel.py``, ``RoutedMoe.schedule``). ``eligibility`` is decided from +host-side state (types, shapes, dtypes, routing configuration and the device) +before any device work, so an ineligible call site keeps the native path with +no side effect. + +Outputs follow the routed_moe contract: the finalized routed output ``[R, 6144]`` BF16, or +the native unfinalized tuple ``(fc2_output [R*8, 6144] BF16, expert scales +[R, 8] BF16, expanded_idx_to_permuted_idx [R, 8] int32)`` whose permutation is +the identity ``t*8 + k`` (every row valid; a stock finalizer sums +``scale[t, k] * fc2_output[idx[t, k]]``). + +Physical layout of the raw tuple. ``fc2_output`` is token-major: row +``t * 8 + k`` holds the FC2 output of token ``t``'s ``k``-th expert, with a row +pitch of 6144 elements, and the permutation maps ``(t, k)`` to that row. The +native runner groups its rows by expert and pads them; both layouts are read +through the permutation, so the native unfinalized consumers (the single-node +``MoEAllReduce`` finalize + all-reduce and the post_moe_hop) take either +one. The expert scales carry the native dtype, values and top-k order. + +Programmatic dependent launch (PDL). Both kernels are launched as PDL +secondaries of whatever kernel precedes them on the stream (the router GEMM, +router_gemm's router or a pre-MoE hop): + +* before ``griddepcontrol.wait`` they only set up shared memory, barriers and + tensor memory and read the router bias, which no kernel writes during + serving (it is a model parameter); every input produced on the stream + (activation, router logits) and every output store comes after the wait. + With package cross_layer_expert_prefetch's speculative prefetch, the expert-cluster kernel also + reads the predicted expert IDs there and issues L2 prefetches of those + experts' weights; the IDs are written on another stream (or by the + previous call of the same layer) and only steer the prefetch, so any value + read is harmless; +* the expert-cluster kernel issues ``griddepcontrol.launch_dependents`` after + each CTA's last FC2 tile; that only lets the successor's CTAs be scheduled + earlier, and the successor (the finalize kernel or the post-MoE hop) reads + the routed output only after its own ``griddepcontrol.wait``, which returns + when this grid has completed and its memory operations are visible; +* the finalize kernel reads the raw rows after its ``griddepcontrol.wait`` + and triggers its dependents after its stores; +* outputs written into a consumer's buffer (routed_moe_into_hop) are not reused by this + package; the consumer owns their lifetime and clears the FP32 accumulator + after reading it. + +Work on other streams (the shared expert on the aux stream) joins through +events as in the native model, not through PDL. + +Numerics. The expert-cluster schedule computes every token's FC1, SwiGLU, +intermediate quantization and FC2 independently of the other rows of the +call (no split-K), and the finalize kernel sums each token's top-k in order, +so the raw rows and the finalized output of a live row do not depend on the +content of padded rows (bit for bit). The FP32 accumulator output (routed_moe_into_hop +"fp32") adds the eight weighted rows of a token with atomic FP32 adds whose +order depends on the schedule, so it is run-to-run nondeterministic within +the FP32 reorder bound. The grid-wide ``v6`` schedule splits FC1 over the +distinct experts of the whole call and is not padding-invariant. +""" + +import contextlib +import threading +from typing import Dict, List, Optional, Tuple + +import torch + +HIDDEN = 6144 +INTER = 256 +NUM_EXPERTS = 256 +TOP_K = 8 +MAX_ROWS = 8 +ROUTED_SCALING = 2.5 + +_NUM_ADDRS = 19 +# Expert-cluster schedule defaults for the served neighbours (attention +# weight stream, pre-MoE hop, the BF16 shared expert on the aux stream, post_moe_hop / +# MNNVL all-reduce hop). The shared expert runs concurrently with the routed +# kernel and needs SMs and DRAM latency headroom; how much slack it has +# depends on the output mode: +# * finalized: the finalize kernel, the add and the all-reduce follow on the +# main stream, so at most 32 four-CTA clusters work; +# * raw tuple (into the post_moe_hop, which joins the shared branch right after the +# routed kernel) with the router logits supplied by the pre-MoE hop (post_attention_hop or +# router_gemm selected: the MoE's router GEMM is skipped, so the shared expert's +# first GEMM starts together with the routed kernel instead of during the +# router GEMM): 26 clusters, raised to cover rows x top-8 in two rounds for +# R > 6; +# * the same with routed_moe_into_hop's FP32 accumulator output: the routed kernel writes R +# rows instead of R x 8 and finishes earlier, so the shared expert is the +# critical path at 26 clusters; 24 clusters (raised the same way for +# R > 6); +# * raw tuple after the MoE's own router GEMM: 32 clusters. +# For D <= clusters distinct experts only clamp(D, 24, clusters) clusters +# work; the others exit after routing and leave their SMs to the shared +# expert. +XC_MAX_CLUSTERS = 32 +XC_RAW_CLUSTERS = 26 +XC_RED_CLUSTERS = 24 +XC_MIN_CLUSTERS = 24 +# selector IDs of the pre-MoE hops that emit the router logits (the router contract) +_LOGIT_HOPS = ("post_attention_hop", "router_gemm") + + +#: Stress-test delay injection of the routed kernels: ``(site bits, unit in ns)``, the sites being +#: ``kernel.DELAY_*`` / ``kernel_xc.DELAY_*``. Serving delays no site; tests set it to hold CTAs at +#: those sites. It is part of every runner's compile key. +DELAY_INJECTION = (0, 2000) + + +def xc_kernel_args() -> dict: + """Launch parameters of the expert-cluster kernel (``kernel_xc``) and of its forks. + + Ring depth 8, TMEM accumulator stages 4, PDL trigger ``late``, at least + ``XC_MIN_CLUSTERS`` working clusters, no expert-cluster L2 prefetch + (``l2pf`` 0), every cluster working in a two-round call + (``two_round_clusters`` 0) and ``DELAY_INJECTION``. + """ + delay, delay_ns = DELAY_INJECTION + return dict( + num_stages=8, + delay=delay, + delay_ns=delay_ns, + trigger="late", + min_clusters=XC_MIN_CLUSTERS, + l2pf=0, + two_round_clusters=0, + acc_stages=4, + ) + + +def _logits_from_hop() -> bool: + """Whether a pre-MoE hop supplies the router logits (rank-identical).""" + from ....shared import dispatch + + return any(dispatch.is_enabled(f) for f in _LOGIT_HOPS) + + +_NUM_XC_ADDRS = 18 + +_LOCK = threading.Lock() +_COMPILED: Dict[tuple, object] = {} +_WORKSPACES: Dict[int, "_Workspace"] = {} + + +def _sm_count(device: torch.device) -> int: + return torch.cuda.get_device_properties(device).multi_processor_count + + +_MAX_CLUSTERS: Dict[tuple, int] = {} + + +def _max_active_clusters(device: torch.device, cluster: int) -> int: + """Clusters of ``cluster`` CTAs that are co-resident at one CTA per SM + (a cluster must fit in one GPC, so this can be below sm_count / cluster).""" + idx = device.index if device.index is not None else torch.cuda.current_device() + key = (idx, cluster) + if key not in _MAX_CLUSTERS: + import cutlass.utils + + _MAX_CLUSTERS[key] = int( + cutlass.utils.HardwareInfo(device_id=idx).get_max_active_clusters(cluster) + ) + return _MAX_CLUSTERS[key] + + +class _Workspace: + """Graph-stable per-device scratch shared by every routed_moe call of the process. + + The scratch is private to routed_moe. Calls that use it must be ordered on the + device: one workspace serves all layers, both output modes and every + captured CUDA graph. ``ws`` holds one launch counter per CTA; flags and LL + words carry the epoch of the launch that wrote them, so nothing needs + clearing between calls or graph replays. + + Ordering is enforced for eager calls: a call issued from another stream + than the previous eager call first makes its stream wait for that + stream's submitted work (``order_on``). Captured calls are ordered by the + stream the graph is replayed on, which must be the stream of the eager + calls (or synchronized with it), as in the model executor. + + It is created once per device, outside any dispatch mode (a model built + under the loader's meta-device mode still gets device memory), and never + replaced, because captured graphs keep its addresses. + """ + + def __init__(self, device: torch.device, num_ctas: int): + from .kernel import FC1_MT, HBUF_BYTES, LL_PART_BYTES, MAX_SLOTS, WS_WORDS + + self.num_ctas = num_ctas + self.ws = torch.zeros(WS_WORDS, dtype=torch.int32, device=device) + # FC1 and FC2 split partials, one LL slot per CTA each + self.partials = torch.zeros(2 * num_ctas * LL_PART_BYTES, dtype=torch.uint8, device=device) + # per-expert FC2 operand hand-off buffers and per-tile epoch flags + self.himg = torch.zeros(MAX_SLOTS * HBUF_BYTES, dtype=torch.uint8, device=device) + self.hflag = torch.zeros(MAX_SLOTS * FC1_MT, dtype=torch.int32, device=device) + self.dummy = torch.zeros(64, dtype=torch.int32, device=device) + # expert-cluster schedule, finalized mode: raw rows and routing + # weights consumed by the finalize kernel + self.rows = torch.empty(MAX_ROWS * TOP_K, HIDDEN, dtype=torch.bfloat16, device=device) + self.scales = torch.empty(MAX_ROWS, TOP_K, dtype=torch.bfloat16, device=device) + # per-output-tile arrival counters of the in-kernel finalize; the + # closing CTA resets its tile's counter + self.tile_count = torch.zeros(64, dtype=torch.int32, device=device) + # stream of the last eager call (see order_on) + self.stream: Optional[torch.cuda.Stream] = None + + def tensors(self) -> Dict[str, torch.Tensor]: + return {name: t for name, t in vars(self).items() if isinstance(t, torch.Tensor)} + + def nbytes(self) -> int: + return sum(t.numel() * t.element_size() for t in self.tensors().values()) + + def order_on(self, stream: torch.cuda.Stream) -> None: + """Order an eager call on ``stream`` after the previous eager call. + + A no-op while ``stream`` is being captured: graph nodes run when the + graph is replayed, in the order of the replay stream. + """ + if torch.cuda.is_current_stream_capturing(): + return + prev = self.stream + if prev is not None and prev != stream: + stream.wait_stream(prev) + self.stream = stream + + +def _workspace(device: torch.device, num_ctas: int) -> _Workspace: + """The device's workspace, sized for every SM; created on first use.""" + key = device.index if device.index is not None else torch.cuda.current_device() + ws = _WORKSPACES.get(key) + if ws is None: + try: + from torch.utils._python_dispatch import _disable_current_modes + except ImportError: # the buffers are still checked below + _disable_current_modes = contextlib.nullcontext + dev = torch.device("cuda", key) + with _disable_current_modes(): + ws = _Workspace(dev, max(num_ctas, _sm_count(dev))) + bad = [n for n, t in ws.tensors().items() if t.is_meta or t.device != dev] + if bad: + raise RuntimeError(f"routed_moe workspace buffers {bad} are not device memory on {dev}") + _WORKSPACES[key] = ws + elif ws.num_ctas < num_ctas: + raise RuntimeError( + f"routed_moe workspace of cuda:{key} holds {ws.num_ctas} CTA slots, a launch needs " + f"{num_ctas}; it cannot be replaced once CUDA graphs may reference it" + ) + return ws + + +def resolve_expert_owner(module) -> Tuple[Optional[torch.nn.Module], str]: + """The leaf that owns the NVFP4 expert parameters of ``module``. + + The served model passes a ``ConfigurableMoE`` wrapper, whose expert + tensors live on ``wrapper.backend`` (the wrapper only has delegating + properties); a bare TRT-LLM-Gen leaf owns them itself. Ownership is + decided by registered parameters, not by attribute probes. + """ + backend = getattr(module, "backend", None) + owner = backend if isinstance(backend, torch.nn.Module) else module + if not isinstance(owner, torch.nn.Module): + return None, f"{type(module).__name__} is not a module" + if "w3_w1_weight" not in owner._parameters or "w2_weight" not in owner._parameters: + return None, f"{type(owner).__name__} owns no NVFP4 expert parameters" + return owner, "" + + +def _param(owner, name): + return owner._parameters.get(name) if isinstance(owner, torch.nn.Module) else None + + +def _check_vector(t, numel: int) -> bool: + return t is not None and t.dtype == torch.float32 and t.numel() == numel and t.is_contiguous() + + +def _wrapper_reason(wrapper) -> str: + """Wrapper-level behaviour the routed_moe call would skip; empty when none.""" + if getattr(wrapper, "comm", None) is not None: + return "MoE communication strategy" + if getattr(wrapper, "layer_load_balancer", None) is not None: + return "EPLB load balancer" + if getattr(wrapper, "enable_dwdp", False): + return "DWDP" + if getattr(wrapper, "unfinalized_combine_fn", None) is not None: + return "unfinalized_combine_fn" + if getattr(wrapper, "apply_router_weight_on_input", False): + return "router weight on input" + if getattr(wrapper, "_enable_perfect_router", False): + return "perfect router" + if getattr(wrapper, "reduce_results", False) and getattr(wrapper, "parallel_size", 1) > 1: + return "in-op all-reduce" + return "" + + +def _routing_reason(rm) -> str: + from tensorrt_llm._torch.moe.fused_moe.routing import DeepSeekV3MoeRoutingMethod + + if not isinstance(rm, DeepSeekV3MoeRoutingMethod): + return f"routing method {type(rm).__name__}" + impl = rm.routing_impl + groups = (impl.top_k, impl.n_group, impl.topk_group) + if groups != (TOP_K, 1, 1) or float(impl.routed_scaling_factor) != ROUTED_SCALING: + return f"routing {groups} scale {impl.routed_scaling_factor}" + return "" + + +def _owner_reason(owner, device: torch.device) -> str: + """Leaf-level eligibility: stock TRT-LLM-Gen NVFP4 TP8/EP1 shard.""" + from tensorrt_llm._torch.moe.fused_moe.trtllm_gen.fp4_block_scale import TRTLLMGenNvfp4Base + from tensorrt_llm._torch.moe.fused_moe.trtllm_gen.identity import PROVIDER_TRTLLM + from tensorrt_llm._torch.utils import ActivationType + + if not isinstance(owner, TRTLLMGenNvfp4Base): + return f"{type(owner).__name__} is not a TRT-LLM-Gen NVFP4 leaf" + if getattr(type(owner), "provider", None) != PROVIDER_TRTLLM: + return f"provider {getattr(type(owner), 'provider', None)}" + if getattr(owner, "use_dp", False): + return "attention DP" + routes_outside = getattr(owner, "_routes_outside_the_kernel", None) + if routes_outside is None or routes_outside(): + return "separated routing" + if getattr(owner, "activation_type", None) != ActivationType.Swiglu: + return f"activation {getattr(owner, 'activation_type', None)}" + if getattr(owner, "bias", False): + return "expert bias" + for name in ("act_alpha", "act_beta", "act_clamp"): + if getattr(owner, name, None) is not None: + return f"{name} set" + if ( + getattr(owner, "num_slots", None) != NUM_EXPERTS + or getattr(owner, "slot_start", None) != 0 + or getattr(owner, "expert_size_per_partition", None) != NUM_EXPERTS + ): + return "not all 256 experts local (EP or EPLB slots)" + if getattr(owner, "hidden_size", None) != HIDDEN: + return f"hidden {getattr(owner, 'hidden_size', None)}" + quant_method = getattr(owner, "quant_method", None) + lean = getattr(quant_method, "intermediate_size_per_partition_lean", None) + if lean not in (None, INTER): + return f"lean intermediate {lean}" + w13 = _param(owner, "w3_w1_weight") + w2 = _param(owner, "w2_weight") + w13_sf = _param(owner, "w3_w1_weight_scale") + w2_sf = _param(owner, "w2_weight_scale") + if tuple(w13.shape) != (NUM_EXPERTS, 2 * INTER, HIDDEN // 2) or w13.dtype != torch.uint8: + return f"w3_w1 {tuple(w13.shape)} {w13.dtype}" + if tuple(w2.shape) != (NUM_EXPERTS, HIDDEN, INTER // 2) or w2.dtype != torch.uint8: + return f"w2 {tuple(w2.shape)} {w2.dtype}" + if ( + w13_sf is None + or w2_sf is None + or tuple(w13_sf.shape) != (NUM_EXPERTS, 2 * INTER, HIDDEN // 16) + or tuple(w2_sf.shape) != (NUM_EXPERTS, HIDDEN, INTER // 16) + ): + return "block-scale shapes" + for t in (w13, w2, w13_sf, w2_sf): + if not t.is_contiguous() or t.device != device: + return "expert buffers not contiguous on the activation's device" + for name in ("fc31_alpha", "fc31_scale_c", "fc2_alpha"): + t = _param(owner, name) + if not _check_vector(t, NUM_EXPERTS) or t.device != device: + return f"{name} is not FP32 [256] on the device" + t = _param(owner, "fc31_input_scale") + if not _check_vector(t, 1) or t.device != device: + return "fc31_input_scale is not one FP32 scalar on the device" + return _routing_reason(getattr(owner, "routing_method", None)) + + +# Both schedules copy each activation row into shared memory with +# ``cp.async.bulk``, whose global source must be 16-byte aligned. +_BULK_ALIGN = 16 +_REALIGNED_LOGGED = [] + + +def _bulk_copy_ready(x: torch.Tensor) -> torch.Tensor: + """``x`` itself, or an aligned copy when its base is not 16-byte aligned. + + The base address comes from this rank's allocator, so it never decides + between routed_moe and the native path (with routed_moe_into_hop the choice also selects the + post_moe_hop hop variant): a misaligned activation costs one device copy instead. + """ + if x.data_ptr() % _BULK_ALIGN == 0: + return x + if not _REALIGNED_LOGGED: + _REALIGNED_LOGGED.append(True) + from tensorrt_llm.logger import logger + + logger.warning( + "[glm_moe_dsa] routed_moe: activation base is not 16-byte aligned " + f"(data_ptr % 16 = {x.data_ptr() % _BULK_ALIGN}); copying it before every such call" + ) + return x.clone(memory_format=torch.contiguous_format) + + +def eligibility(module, x: torch.Tensor, router_logits: torch.Tensor) -> Tuple[bool, str]: + """Decide routed_moe support for this call from host-side state only. + + ``module`` is the served ``ConfigurableMoE`` wrapper or a bare + TRT-LLM-Gen NVFP4 leaf. + """ + if not torch.cuda.is_available() or x.device.type != "cuda": + return False, "not on CUDA" + major, _ = torch.cuda.get_device_capability(x.device) + if major != 10: + return False, f"SM{major}x is not the SM100 family" + if x.dtype != torch.bfloat16 or x.dim() != 2 or x.shape[1] != HIDDEN: + return False, f"activation {tuple(x.shape)} {x.dtype}" + rows = x.shape[0] + if rows < 1 or rows > MAX_ROWS: + return False, f"{rows} rows outside [1, {MAX_ROWS}]" + if not x.is_contiguous() or not router_logits.is_contiguous(): + return False, "non-contiguous input" + if ( + router_logits.dtype != torch.float32 + or tuple(router_logits.shape) != (rows, NUM_EXPERTS) + or router_logits.device != x.device + ): + return False, f"logits {tuple(router_logits.shape)} {router_logits.dtype}" + if torch.compiler.is_compiling(): + return False, "torch.compile tracing" + owner, reason = resolve_expert_owner(module) + if owner is None: + return False, reason + if owner is not module: + reason = _wrapper_reason(module) or _routing_reason(getattr(module, "routing_method", None)) + if reason: + return False, reason + reason = _owner_reason(owner, x.device) + if reason: + return False, reason + return True, "" + + +class RoutedMoe: + """Callable bound to one native NVFP4 MoE leaf and its router bias. + + ``module`` may be the ``ConfigurableMoE`` wrapper or the leaf itself; the + kernel always reads the leaf's parameters. + """ + + def __init__(self, module, routing_bias: torch.Tensor): + owner, reason = resolve_expert_owner(module) + if owner is None: + raise ValueError(f"routed_moe needs an NVFP4 expert leaf: {reason}") + self.module = owner + self.bias = routing_bias + self.device = owner.w3_w1_weight.device + self.num_ctas = _sm_count(self.device) + + @classmethod + def from_native(cls, module, routing_bias: torch.Tensor) -> "RoutedMoe": + return cls(module, routing_bias) + + def _spec(self): + """Speculative prefetch of this layer's experts (cross_layer_expert_prefetch): + ``(ids, k, ctas, self_store)`` or ``None``.""" + hook = getattr(self.module, "_expert_prefetch_spec", None) + return hook() if hook is not None else None + + def _key(self, finalize: bool, debug: int, rows: int = MAX_ROWS, red: bool = False): + """``(num_ctas, finalize, debug, schedule, grid, red, prefetch spec, delay injection)`` of a + launch.""" + sched = self.schedule(rows, finalize, red) + grid = self._xc_ctas(rows, finalize, red) if sched == "xc" else self.num_ctas + spec = self._spec() if sched == "xc" else None + spec_key = spec[1:] if spec is not None else None + return (self.num_ctas, finalize, debug, sched, grid, red, spec_key, DELAY_INJECTION) + + def schedule(self, rows: int = MAX_ROWS, finalize: bool = True, red: bool = False) -> str: + """``xc`` (expert clusters) or ``v6`` (grid-wide hand-off). + + ``xc`` needs enough co-resident clusters for two rounds to cover the + largest distinct-expert count of a ``rows``-row call (8 per row); + otherwise the grid-wide schedule runs. + """ + from .kernel_xc import CLUSTER, MAX_ROUNDS + + if (self._xc_ctas(rows, finalize, red) // CLUSTER) * MAX_ROUNDS < rows * TOP_K: + return "v6" + return "xc" + + @staticmethod + def inkernel_finalize() -> bool: + """Whether ``xc`` finalizes inside the routed kernel: no, a second, PDL-launched kernel + finalizes.""" + return False + + def _xc_ctas(self, rows: int = MAX_ROWS, finalize: bool = True, red: bool = False) -> int: + """Grid of the expert-cluster schedule for a ``rows``-row call. + + Every cluster must be co-resident, and in the served graph the BF16 + shared expert runs concurrently on the aux stream, so the grid leaves + SMs free: ``XC_MAX_CLUSTERS`` clusters (32 on sm_103 = 128 of 152 SMs), + or, when a pre-MoE hop supplies the router logits, ``XC_RAW_CLUSTERS`` + (26 = 104 SMs) for the raw tuple and ``XC_RED_CLUSTERS`` (24 = 96 SMs) + for the FP32 accumulator output (``red``), raised to + ``ceil(rows * 8 / 2)`` so that two rounds cover every distinct expert + of the call. + """ + from .kernel_xc import CLUSTER, MAX_ROUNDS + + co_resident = min(XC_MAX_CLUSTERS, _max_active_clusters(self.device, CLUSTER)) + clusters = co_resident + if not finalize and _logits_from_hop(): + need = -(-rows * TOP_K // MAX_ROUNDS) + base = XC_RED_CLUSTERS if red else XC_RAW_CLUSTERS + clusters = min(co_resident, max(base, need)) + n = CLUSTER * clusters + n = min(n, self.num_ctas) + return n - n % CLUSTER + + def ready_for_capture( + self, finalize: bool, debug: int = 0, rows: int = MAX_ROWS, red: bool = False + ) -> bool: + """True when a launch needs no compilation and no workspace + allocation, i.e. it can be recorded into a CUDA graph.""" + key = self._key(finalize, debug, rows, red) + dev = self.device.index if self.device.index is not None else torch.cuda.current_device() + ws = _WORKSPACES.get(dev) + return key in _COMPILED and ws is not None and ws.num_ctas >= self.num_ctas + + def _compiled(self, finalize: bool, debug: int, rows: int = MAX_ROWS, red: bool = False): + """Compiled launcher(s): ``(routed, finalize_or_None)``. + + ``red``: the expert-cluster kernel adds the weighted FC2 outputs into + an FP32 accumulator instead of writing the raw rows (routed_moe_into_hop "fp32"). + """ + key = self._key(finalize, debug, rows, red) + with _LOCK: + fns = _COMPILED.get(key) + if fns is not None: + return fns + import cutlass + import cutlass.cute as cute + from cuda.bindings import driver as cuda + from cutlass.cute.runtime import make_ptr + + ptrs = self._pointers(make_ptr, cute, cutlass) + stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream) + if key[3] == "xc": + from .kernel_xc import RoutedMoeExpertClusterKernel, RoutedMoeFinalizeKernel + + inkernel = finalize and self.inkernel_finalize() + spec = key[6] or (0, 0, False) + kern = RoutedMoeExpertClusterKernel( + self._xc_ctas(rows, finalize, red), + debug=debug, + finalize=inkernel, + red_out=red, + **xc_kernel_args(), + spec_k=spec[0], + spec_ctas=spec[1], + spec_self=spec[2], + ) + ints = [cutlass.Int64(0)] * _NUM_XC_ADDRS + routed = cute.compile(kern.run, *ptrs, *ints, cutlass.Int32(1), stream) + if spec[0]: + from ....shared import dispatch + + dispatch.log_capability( + "routed_moe", + f"expert_prefetch[k={spec[0]},ctas={spec[1]},self={int(spec[2])}]", + True, + ) + fin = None + if finalize and not inkernel and not red: + from .kernel_xc import DELAY_FINALIZE + + delay, delay_ns = DELAY_INJECTION + fin = cute.compile( + RoutedMoeFinalizeKernel( + delay_ns=delay_ns if delay & DELAY_FINALIZE else 0 + ).run, + cutlass.Int64(0), + cutlass.Int64(0), + cutlass.Int64(0), + cutlass.Int32(1), + stream, + ) + fns = (routed, fin) + else: + from .kernel import RoutedMoeKernel + + delay, delay_ns = DELAY_INJECTION + kern = RoutedMoeKernel( + self.num_ctas, finalize=finalize, debug=debug, delay=delay, delay_ns=delay_ns + ) + ints = [cutlass.Int64(0)] * _NUM_ADDRS + fns = (cute.compile(kern.run, *ptrs, *ints, cutlass.Int32(1), stream), None) + _COMPILED[key] = fns + return fns + + def _pointers(self, make_ptr, cute, cutlass): + m = self.module + return [ + make_ptr( + cutlass.Float4E2M1FN, + m.w3_w1_weight.data_ptr(), + cute.AddressSpace.gmem, + assumed_align=32, + ), + make_ptr( + cutlass.Float8E4M3FN, + m.w3_w1_weight_scale.data_ptr(), + cute.AddressSpace.gmem, + assumed_align=16, + ), + make_ptr( + cutlass.Float4E2M1FN, + m.w2_weight.data_ptr(), + cute.AddressSpace.gmem, + assumed_align=32, + ), + make_ptr( + cutlass.Float8E4M3FN, + m.w2_weight_scale.data_ptr(), + cute.AddressSpace.gmem, + assumed_align=16, + ), + ] + + def __call__( + self, + x: torch.Tensor, + router_logits: torch.Tensor, + do_finalize: bool = True, + debug: int = 0, + debug_buffer: Optional[torch.Tensor] = None, + out: Optional[torch.Tensor] = None, + raw_out: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, + red_out: Optional[torch.Tensor] = None, + ): + """Run the routed MoE. + + ``out``: finalized output buffer. ``raw_out``: ``(rows [R*8, H] bf16, + scales [R, 8] bf16)`` buffers for the raw tuple (identity layout; the + permutation is not written). ``red_out``: FP32 ``[R, H]`` accumulator + that receives ``sum_k w[t, k] * fc2[t, k]`` (zero on entry; expert + cluster schedule only). + """ + import cutlass + import cutlass.cute as cute + from cuda.bindings import driver as cuda + from cutlass.cute.runtime import make_ptr + + m = self.module + rows = x.shape[0] + x = _bulk_copy_ready(x) + red = red_out is not None + if red: + do_finalize = False + if self.schedule(rows, False, True) != "xc": + raise ValueError("FP32 accumulator output needs the expert-cluster schedule") + routed_fn, fin_fn = self._compiled(do_finalize, debug, rows, red) + ws = _workspace(self.device, self.num_ctas) + dbg = debug_buffer if debug_buffer is not None else ws.dummy + current = torch.cuda.current_stream() + ws.order_on(current) + stream = cuda.CUstream(current.cuda_stream) + ptrs = self._pointers(make_ptr, cute, cutlass) + params = [ + x.data_ptr(), + router_logits.data_ptr(), + self.bias.data_ptr(), + m.fc31_alpha.data_ptr(), + m.fc31_scale_c.data_ptr(), + m.fc2_alpha.data_ptr(), + m.fc31_input_scale.data_ptr(), + ] + if self.schedule(rows, do_finalize, red) == "xc": + if red: + out = red_out + raw_rows, raw_scale, raw_idx = ws.rows, ws.scales, ws.dummy + elif do_finalize: + raw_rows, raw_scale, raw_idx = ws.rows, ws.scales, ws.dummy + if out is None: + out = torch.empty(rows, HIDDEN, dtype=torch.bfloat16, device=x.device) + elif raw_out is not None: + out = ws.dummy + raw_rows, raw_scale = raw_out + raw_idx = ws.dummy + else: + out = ws.dummy + raw_rows = torch.empty(rows * TOP_K, HIDDEN, dtype=torch.bfloat16, device=x.device) + raw_scale = torch.empty(rows, TOP_K, dtype=torch.bfloat16, device=x.device) + raw_idx = torch.empty(rows, TOP_K, dtype=torch.int32, device=x.device) + addrs = params + [ + raw_rows.data_ptr(), + raw_scale.data_ptr(), + raw_idx.data_ptr(), + out.data_ptr(), + ws.tile_count.data_ptr(), + dbg.data_ptr(), + m.w3_w1_weight.data_ptr(), + m.w3_w1_weight_scale.data_ptr(), + m.w2_weight.data_ptr(), + m.w2_weight_scale.data_ptr(), + ] + spec = self._spec() + addrs.append(spec[0].data_ptr() if spec is not None else ws.dummy.data_ptr()) + assert len(addrs) == _NUM_XC_ADDRS + routed_fn(*ptrs, *[cutlass.Int64(a) for a in addrs], cutlass.Int32(rows), stream) + if red: + return [red_out] + if not do_finalize: + return [raw_rows, raw_scale, raw_idx] + if fin_fn is None: + return out + fin_fn( + cutlass.Int64(raw_rows.data_ptr()), + cutlass.Int64(raw_scale.data_ptr()), + cutlass.Int64(out.data_ptr()), + cutlass.Int32(rows), + stream, + ) + return out + if do_finalize: + if out is None: + out = torch.empty(rows, HIDDEN, dtype=torch.bfloat16, device=x.device) + raw_scale = ws.dummy + raw_idx = ws.dummy + else: + out = torch.empty(rows * TOP_K, HIDDEN, dtype=torch.bfloat16, device=x.device) + raw_scale = torch.empty(rows, TOP_K, dtype=torch.bfloat16, device=x.device) + raw_idx = torch.empty(rows, TOP_K, dtype=torch.int32, device=x.device) + addrs = params + [ + out.data_ptr(), + raw_scale.data_ptr(), + raw_idx.data_ptr(), + ws.ws.data_ptr(), + ws.partials.data_ptr(), + ws.himg.data_ptr(), + ws.hflag.data_ptr(), + dbg.data_ptr(), + m.w3_w1_weight.data_ptr(), + m.w3_w1_weight_scale.data_ptr(), + m.w2_weight.data_ptr(), + m.w2_weight_scale.data_ptr(), + ] + assert len(addrs) == _NUM_ADDRS + routed_fn(*ptrs, *[cutlass.Int64(a) for a in addrs], cutlass.Int32(rows), stream) + if do_finalize: + return out + return [out, raw_scale, raw_idx] + + +def _engaged_runner( + module, + routing_bias: torch.Tensor, + x: torch.Tensor, + router_logits: torch.Tensor, + do_finalize: bool, + feature: str = "routed_moe", + red: bool = False, +) -> Optional["RoutedMoe"]: + """The runner for this call, or ``None`` (decided before any device work).""" + from ....shared import dispatch + + ok, reason = eligibility(module, x, router_logits) + owner = resolve_expert_owner(module)[0] if ok else None + if ok: + rm = getattr(owner, "routing_method", None) + native_bias = getattr(rm, "e_score_correction_bias", None) + if ( + routing_bias is None + or routing_bias.dtype != torch.bfloat16 + or tuple(routing_bias.shape) != (NUM_EXPERTS,) + or not routing_bias.is_contiguous() + or routing_bias.device != x.device + ): + ok, reason = False, "router bias is not a contiguous BF16 [256] on the device" + elif native_bias is not None and native_bias.data_ptr() != routing_bias.data_ptr(): + ok, reason = False, "router bias is not the routing method's bias" + runner = getattr(owner, "_routed_moe_runner", None) if ok else None + if ok and runner is not None and runner.bias.data_ptr() != routing_bias.data_ptr(): + runner = None + if ok and torch.cuda.is_current_stream_capturing(): + # Compiling or allocating the workspace inside a capture would record + # the workspace initialization into the graph; the model's eager + # warmup forwards make both ready before capture. + if runner is None or not runner.ready_for_capture(do_finalize, rows=x.shape[0], red=red): + ok, reason = False, "not warmed up before CUDA graph capture" + dispatch.log_capability(feature, f"rows={x.shape[0]} {reason or 'engaged'}", ok, reason) + if not ok: + return None + if runner is None: + runner = RoutedMoe(owner, routing_bias) + owner._routed_moe_runner = runner + return runner + + +def routed_output( + module, + routing_bias: torch.Tensor, + x: torch.Tensor, + router_logits: torch.Tensor, + do_finalize: bool, +) -> Optional[List[torch.Tensor]]: + """Entry point for the routed-MoE call site; ``None`` means "use the native path". + + The decision is made before any device work, so returning ``None`` leaves + no side effect behind. + """ + runner = _engaged_runner(module, routing_bias, x, router_logits, do_finalize) + if runner is None: + return None + return runner(x, router_logits, do_finalize=do_finalize) + + +def routed_output_into_hop( + module, + routing_bias: torch.Tensor, + x: torch.Tensor, + router_logits: torch.Tensor, + hop, +) -> Optional[List[torch.Tensor]]: + """routed_moe_into_hop: routed_moe writes its per-rank routed result into the post_moe_hop + post-MoE hop's routed-input buffer; post_moe_hop then adds the shared output, + reduces across ranks and applies residual and RMSNorm (``hop.hop_routed``). + + The hop's routed-input mode (``hop.routed_mode``) decides what is written: + + * "row": the finalized routed output ``[R, H]`` bf16 (finalize kernel); + * "identity": the raw FC2 rows ``[R * 8, H]`` in the identity layout plus + the routing weights in the hop's scale buffer; post_moe_hop finalizes without the + permutation round trip (bit-identical to the raw tuple); + * "fp32": an FP32 ``[R, H]`` accumulator of ``w[t, k] * fc2[t, k]``, added + by the routed kernel itself (no finalize kernel, no raw rows); the sum + order differs from the native finalize (FP32 reorder class). + + Returns ``[buffer]`` (a view of the hop's buffer) or ``None`` when routed_moe or + the hop's routed input is not eligible; the decision is taken before any + device work and depends only on rank-identical state. + """ + if hop is None or not getattr(hop, "_routed_enabled", False): + return None + rows = x.shape[0] + if not hop.can_run(rows): + return None + mode = getattr(hop, "routed_mode", "row") + if mode == "row": + runner = _engaged_runner( + module, routing_bias, x, router_logits, True, feature="routed_moe_into_hop" + ) + if runner is None: + return None + target = hop.routed_input_buffer(rows) + return [runner(x, router_logits, do_finalize=True, out=target)] + red = mode == "fp32" + runner = _engaged_runner( + module, routing_bias, x, router_logits, False, "routed_moe_into_hop", red + ) + if runner is None or runner.schedule(rows, False, red) != "xc": + return None + if red: + return runner(x, router_logits, red_out=hop.routed_acc_buffer(rows)) + rows_buf, scales_buf = hop.routed_raw_buffers(rows) + runner(x, router_logits, do_finalize=False, raw_out=(rows_buf, scales_buf)) + return [rows_buf] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/__init__.py new file mode 100644 index 000000000000..bbdb33993a2c --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/__init__.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Package shared_expert_prefetch: the MoE region END (routed END and shared END). + +Entry point registered with ``dispatch``: + +* ``routed_output_into_hop(module, bias, x, logits, routing, hop, shared)``: + hop_routing's routed_moe fork as ``SharedPrefetchExpertClusterKernel`` (``routed_moe.py``, + ``kernel_xc.py``). Before its dependency wait, while the pre-MoE hop runs, it prefetches the + shared expert's weights into L2, so the native shared expert on the auxiliary stream + reads them from L2 instead of competing with the routed kernel for DRAM. + ``None`` means "run hop_routing's fork" (decided before any device work). + +The shared expert itself stays native (``GatedMLP``); shared_expert_prefetch has no numerical +variant: given the same routing its expert rows are routed_moe's and its FP32 +accumulator is in routed_moe_into_hop's ``fp32`` class. +""" + +from ....shared.dispatch import register +from .routed_moe import routed_output_into_hop + +register("shared_expert_prefetch", "routed_output_into_hop", routed_output_into_hop) + +__all__ = ["routed_output_into_hop"] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/kernel_xc.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/kernel_xc.py new file mode 100644 index 000000000000..cb6e29f6ef7c --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/kernel_xc.py @@ -0,0 +1,745 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Routed-MoE kernel of package shared_expert_prefetch: hop_routing's expert-cluster fork that +stages the shared expert's weights in L2. + +``SharedPrefetchExpertClusterKernel`` is ``HopRoutedExpertClusterKernel`` (routed_moe's +expert-cluster kernel reading the pre-MoE hop's routing, ``hop_routing/routed_moe.py``) with one +addition. Before its ``griddepcontrol.wait``, each CTA issues ``cp.async.bulk.prefetch.L2`` for +its ``1 / grid`` slice of the layer's BF16 shared-expert weights +(``gate_up`` ``[512, 6144]`` and ``down`` ``[6144, 256]``, one TP8 rank shard, +9,437,184 bytes). The lanes of the TMA warp each prefetch one chunk. + +The routed kernel is launched with programmatic dependent launch during the +pre-MoE hop and waits there while DRAM is nearly idle. The prefetch uses +that window, so the shared expert, which runs beside the routed kernel on +the auxiliary stream right after the hop, reads its weights from L2 instead +of competing with the routed weight stream for DRAM. + +``run`` and ``kernel`` are routed_moe's (``routed_moe/kernel_xc.py``, including package +cross_layer_expert_prefetch's speculative expert prefetch) with the two shared-expert addresses +added to their parameters (after cross_layer_expert_prefetch's ``spec_ids_addr``) and the +shared-expert prefetch added before the wait. Every other line, every helper method and +``_route_tokens`` (hop_routing's) are unchanged, so the schedule, the arithmetic and +the outputs are those of hop_routing's fork. + +With package cross_layer_expert_prefetch (``spec_k`` > 0), the TMA warp runs one pre-wait prologue: +each lane issues its shared-expert chunks first (the shared expert reads all +of them right after the hop), then its chunks of cross_layer_expert_prefetch's predicted experts in +rank order (``_xc_spec_prefetch``). When cross_layer_expert_prefetch restricts its issuers to the +first ``spec_ctas`` CTAs (the CTAs resident before the dependency release), +the shared-expert chunks are spread over the same CTAs (``se_ctas``); +otherwise over the whole grid, as without cross_layer_expert_prefetch. With ``spec_self``, CTA 0's +MMA warp stores the call's distinct experts, ranked by row count, into cross_layer_expert_prefetch's +row after routing (``_xc_spec_store``), as in routed_moe's kernel. + +Memory order and progress: + +* Before ``griddepcontrol.wait`` the addition reads only the shared-expert + weights, through the L2 prefetch. They are model parameters that nothing + writes while serving. It writes nothing and touches no flag, buffer or + counter. +* Everything a producer owns (activation, routing, the FP32 accumulator) is + read and written only after the wait, exactly as in hop_routing's fork. +* The prefetch is an async-proxy read of memory that the generic proxy never + writes while serving, so it needs no fence. Whether the lines are in L2 or + not, the shared expert's own loads (ordered by its own graph edges) read the + same bytes. +* No wait, spin or cross-CTA dependency is added. +* cross_layer_expert_prefetch's prologue reads its row of predicted expert IDs with relaxed loads + before the wait. The row is written only by cross_layer_expert_prefetch (on another stream) or by + CTA 0 of the same layer's previous call, after that call's wait; any value + read only selects which immutable weight bytes are prefetched (each ID is + masked into [0, 256)), never what the kernel reads or computes (the memory + order of ``hop_routing/routed_moe.py`` and ``routed_moe/routed_moe.py``). +""" + +import cutlass +import cutlass.cute as cute +import cutlass.pipeline as pipeline +import cutlass.utils as utils +import cutlass.utils.blackwell_helpers as sm100_utils +import cutlass.utils.blockscaled_layout as blockscaled_utils +from cutlass.cutlass_dsl import Float32, Int32, Int64, Uint32 + +from ..hop_routing import routed_moe as hop_routed_mod +from ..routed_moe import ptx +from ..routed_moe.kernel_xc import ( + A_STAGE_BYTES, + B2_BAR, + BAR_EPI, + CLUSTER, + DEBUG_ROUTING, + DEBUG_TIMELINE, + DELAY_ENTRY, + EPI_WARPS, + FC1_KB, + FC2_MT, + HIDDEN, + HSTAGE_BYTES, + KB_PER_CTA, + KB_TX_BYTES, + MAX_ROUNDS, + MAX_SLOTS, + MMA_WARP, + NTOK, + NUM_BAR, + NUM_EXPERTS, + NUM_THREADS, + SFA_STAGE_BYTES, + SFB2_BYTES, + SFB_BYTES, + TL_BYTE_BASE, + TL_SLOTS, + TMA_WARP, + TMEM_COLS, + TOP_K, + X_BAR, + X_ROW_BYTES, + XS_ROW_BYTES, + XT_ENTRY, + XT_EXIT, + XT_GT0, + XT_PDL, + XT_ROUTED, + _stamp, +) + +SE_INTER = 256 # per-rank shared intermediate size (2048 / TP8) +SE_W1_BYTES = 2 * SE_INTER * HIDDEN * 2 # gate_up [512, 6144] BF16 +SE_W2_BYTES = HIDDEN * SE_INTER * 2 # down [6144, 256] BF16 +PF_LANES = 32 +# Debug-timeline slots of the pre-wait prologue (routed_moe's ``XT_*`` slots end at +# 30; ``XT_GT0`` is 47): SM clock at its start and after its prefetches are +# issued, the global timer at its start, and the global timer when the +# dependency wait returns (the release, next to ``XT_PDL``). +XZ_PRO_CLK0 = 40 +XZ_PRO_CLK1 = 41 +XZ_PRO_GT0 = 42 +XZ_PDL_GT = 43 + + +def _chunk(total: int, ctas: int) -> int: + """Bytes per prefetching lane (a multiple of 16) so that ``ctas x 32`` lanes cover ``total``.""" + per = -(-total // (ctas * PF_LANES)) + return -(-per // 16) * 16 + + +class SharedPrefetchExpertClusterKernel(hop_routed_mod._kernel_class()): + """hop_routing's expert-cluster kernel plus the pre-wait L2 prefetch of the shared expert's weights.""" + + def __init__(self, num_ctas: int, *args, se_pf: bool = True, **kwargs): + super().__init__(num_ctas, *args, **kwargs) + self.se_pf = se_pf + # CTAs that issue the shared-expert prefetch: every CTA, or, when + # cross_layer_expert_prefetch's prefetch is compiled in for its first ``spec_ctas`` CTAs + # (the CTAs resident before the dependency release), the same CTAs. + self.se_ctas = num_ctas + if self.spec_k > 0 and self.spec_ctas > 0: + self.se_ctas = min(num_ctas, self.spec_ctas) + self.se_chunk1 = _chunk(SE_W1_BYTES, self.se_ctas) + self.se_chunk2 = _chunk(SE_W2_BYTES, self.se_ctas) + + @cute.jit + def _se_prefetch(self, bidx, lane, se_w1_addr: Int64, se_w2_addr: Int64): + """Lane ``lane`` of CTA ``bidx`` prefetches chunk ``bidx * 32 + lane`` of ``gate_up`` and of ``down``. + + The chunks are sized for the first ``se_ctas`` CTAs; CTAs past them own no chunk. + """ + i = Int64(bidx * PF_LANES + lane) + s1 = i * Int64(self.se_chunk1) + if s1 < Int64(SE_W1_BYTES): + n1 = cutlass.min(Int64(self.se_chunk1), Int64(SE_W1_BYTES) - s1) + ptx.prefetch_l2_bulk(se_w1_addr + s1, Int32(n1)) + s2 = i * Int64(self.se_chunk2) + if s2 < Int64(SE_W2_BYTES): + n2 = cutlass.min(Int64(self.se_chunk2), Int64(SE_W2_BYTES) - s2) + ptx.prefetch_l2_bulk(se_w2_addr + s2, Int32(n2)) + + @cute.jit + def run( + self, + w13_ptr: cute.Pointer, + w13_sf_ptr: cute.Pointer, + w2_ptr: cute.Pointer, + w2_sf_ptr: cute.Pointer, + x_addr: Int64, + logits_addr: Int64, + bias_addr: Int64, + alpha1_addr: Int64, + scale_c1_addr: Int64, + alpha2_addr: Int64, + xgs_addr: Int64, + rows_addr: Int64, + raw_scale_addr: Int64, + raw_idx_addr: Int64, + out_addr: Int64, + counter_addr: Int64, + dbg_addr: Int64, + w13_addr: Int64, + w13_sf_addr: Int64, + w2_addr: Int64, + w2_sf_addr: Int64, + spec_ids_addr: Int64, + se_w1_addr: Int64, + se_w2_addr: Int64, + num_tokens: Int32, + stream, + ): + self._setup() + w13 = cute.make_tensor( + w13_ptr, cute.make_ordered_layout((2 * 256, HIDDEN, NUM_EXPERTS), order=(1, 0, 2)) + ) + w2 = cute.make_tensor( + w2_ptr, cute.make_ordered_layout((HIDDEN, 256, NUM_EXPERTS), order=(1, 0, 2)) + ) + w13_sf = cute.make_tensor( + w13_sf_ptr, blockscaled_utils.tile_atom_to_shape_SF(w13.shape, self.sf_vec_size) + ) + w2_sf = cute.make_tensor( + w2_sf_ptr, blockscaled_utils.tile_atom_to_shape_SF(w2.shape, self.sf_vec_size) + ) + a_op = sm100_utils.cluster_shape_to_tma_atom_A((1, 1), self.tiled_mma.thr_id) + a_smem_layout = cute.slice_(self.a_smem_layout_staged, (None, None, None, 0)) + sfa_smem_layout = cute.slice_(self.sfa_smem_layout_staged, (None, None, None, 0)) + tma_atom_a1, tma_a1 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, w13, a_smem_layout, self.mma_tiler, self.tiled_mma, self.cluster_layout_vmnk.shape + ) + tma_atom_sfa1, tma_sfa1 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, + w13_sf, + sfa_smem_layout, + self.mma_tiler, + self.tiled_mma, + self.cluster_layout_vmnk.shape, + internal_type=cutlass.Int16, + ) + tma_atom_a2, tma_a2 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, w2, a_smem_layout, self.mma_tiler, self.tiled_mma, self.cluster_layout_vmnk.shape + ) + tma_atom_sfa2, tma_sfa2 = cute.nvgpu.make_tiled_tma_atom_A( + a_op, + w2_sf, + sfa_smem_layout, + self.mma_tiler, + self.tiled_mma, + self.cluster_layout_vmnk.shape, + internal_type=cutlass.Int16, + ) + + @cute.struct + class SharedStorage: + ring_mbar: cute.struct.MemRange[cutlass.Int64, 2 * self.num_stages] + acc_mbar: cute.struct.MemRange[cutlass.Int64, 2 * self.acc_stages] + b_mbar: cute.struct.MemRange[cutlass.Int64, NUM_BAR] + tmem_holding: cutlass.Int32 + tl_buf: cute.struct.MemRange[cutlass.Int64, TL_SLOTS] + hstage: cute.struct.Align[cute.struct.MemRange[cutlass.Int64, HSTAGE_BYTES // 8], 16] + misc: cute.struct.MemRange[cutlass.Int32, 16] + closed: cute.struct.MemRange[cutlass.Int32, MAX_ROUNDS * FC2_MT] + bitmap: cute.struct.MemRange[cutlass.Uint32, 8] + topk_id: cute.struct.MemRange[cutlass.Int32, NTOK * TOP_K] + topk_w: cute.struct.MemRange[cutlass.Float32, NTOK * TOP_K] + slot_expert: cute.struct.MemRange[cutlass.Int32, MAX_SLOTS] + slot_mask: cute.struct.MemRange[cutlass.Uint32, MAX_SLOTS] + slot_w: cute.struct.MemRange[cutlass.Float32, MAX_SLOTS * NTOK] + slot_k: cute.struct.MemRange[cutlass.Int32, MAX_SLOTS * NTOK] + expert_slot: cute.struct.MemRange[cutlass.Int32, NUM_EXPERTS] + sA: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float4E2M1FN, cute.cosize(self.a_smem_layout_staged.outer) + ], + 1024, + ] + sSFA: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float8E4M3FN, cute.cosize(self.sfa_smem_layout_staged) + ], + 1024, + ] + # this CTA's share of the activation rows (NTOK x 6 k-blocks) + sX: cute.struct.Align[cute.struct.MemRange[cutlass.Uint8, NTOK * XS_ROW_BYTES], 1024] + sB: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float4E2M1FN, cute.cosize(self.b_smem_layout_staged.outer) + ], + 1024, + ] + sSFB: cute.struct.Align[cute.struct.MemRange[cutlass.Float8E4M3FN, SFB_BYTES], 1024] + sB2: cute.struct.Align[ + cute.struct.MemRange[ + cutlass.Float4E2M1FN, cute.cosize(self.b2_smem_layout_staged.outer) + ], + 1024, + ] + sSFB2: cute.struct.Align[cute.struct.MemRange[cutlass.Float8E4M3FN, SFB2_BYTES], 1024] + + self.shared_storage = SharedStorage + + self.kernel( + self.tiled_mma, + tma_atom_a1, + tma_a1, + tma_atom_sfa1, + tma_sfa1, + tma_atom_a2, + tma_a2, + tma_atom_sfa2, + tma_sfa2, + self.cluster_layout_vmnk, + self.a_smem_layout_staged, + self.sfa_smem_layout_staged, + self.b_smem_layout_staged, + self.sfb_smem_layout_staged, + self.sfb_smem_layout_std, + self.b2_smem_layout_staged, + self.sfb2_smem_layout_staged, + x_addr, + logits_addr, + bias_addr, + alpha1_addr, + scale_c1_addr, + alpha2_addr, + xgs_addr, + rows_addr, + raw_scale_addr, + raw_idx_addr, + out_addr, + counter_addr, + dbg_addr, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + spec_ids_addr, + se_w1_addr, + se_w2_addr, + num_tokens, + ).launch( + grid=[self.num_ctas, 1, 1], + block=[NUM_THREADS, 1, 1], + cluster=[CLUSTER, 1, 1], + smem=self.shared_storage.size_in_bytes(), + min_blocks_per_mp=1, + stream=stream, + use_pdl=True, + ) + + # ------------------------------------------------------------------ + @cute.kernel + def kernel( + self, + tiled_mma: cute.TiledMma, + tma_atom_a1: cute.CopyAtom, + mA1: cute.Tensor, + tma_atom_sfa1: cute.CopyAtom, + mSFA1: cute.Tensor, + tma_atom_a2: cute.CopyAtom, + mA2: cute.Tensor, + tma_atom_sfa2: cute.CopyAtom, + mSFA2: cute.Tensor, + cluster_layout_vmnk: cute.Layout, + a_smem_layout_staged: cute.ComposedLayout, + sfa_smem_layout_staged: cute.Layout, + b_smem_layout_staged: cute.ComposedLayout, + sfb_smem_layout_staged: cute.Layout, + sfb_smem_layout_std: cute.Layout, + b2_smem_layout_staged: cute.ComposedLayout, + sfb2_smem_layout_staged: cute.Layout, + x_addr: Int64, + logits_addr: Int64, + bias_addr: Int64, + alpha1_addr: Int64, + scale_c1_addr: Int64, + alpha2_addr: Int64, + xgs_addr: Int64, + rows_addr: Int64, + raw_scale_addr: Int64, + raw_idx_addr: Int64, + out_addr: Int64, + counter_addr: Int64, + dbg_addr: Int64, + w13_addr: Int64, + w13_sf_addr: Int64, + w2_addr: Int64, + w2_sf_addr: Int64, + spec_ids_addr: Int64, + se_w1_addr: Int64, + se_w2_addr: Int64, + num_tokens: Int32, + ): + warp_idx = cute.arch.make_warp_uniform(cute.arch.warp_idx()) + tidx, _, _ = cute.arch.thread_idx() + lane = tidx % 32 + bidx, _, _ = cute.arch.block_idx() + grid, _, _ = cute.arch.grid_dim() + bidx = Int32(bidx) + rk = ptx.cluster_ctarank() + cl = bidx // CLUSTER + ncl = Int32(grid) // CLUSTER + + # shared_expert_prefetch (+ cross_layer_expert_prefetch): one pre-wait prologue in the TMA + # warp. Its lanes first start the L2 prefetch of this CTA's slice of the layer's + # shared-expert weights, then (cross_layer_expert_prefetch) of their chunks of the + # predicted routed experts in rank order; both are immutable model + # parameters (see the module docstring). With cross_layer_expert_prefetch's issuing CTAs + # set, both lists are spread over those CTAs only. + # Debug timeline only: when the prologue starts (SM clock and global + # timer) and when its prefetches are issued (SM clock), recorded by the + # TMA warp's lane 0 and stored into slots XZ_PRO_* after the shared + # memory set-up below. + pro_on = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + if cutlass.const_expr(pro_on): + pro_c0 = Int64(0) + pro_g0 = Int64(0) + pro_c1 = Int64(0) + if warp_idx == TMA_WARP: + if cutlass.const_expr(pro_on): + pro_c0 = ptx.clock64() + pro_g0 = ptx.globaltimer_u64() + if cutlass.const_expr(self.se_pf): + self._se_prefetch(bidx, lane, se_w1_addr, se_w2_addr) + if cutlass.const_expr(self.spec_k > 0): + self._xc_spec_prefetch( + bidx, + Int32(grid), + lane, + spec_ids_addr, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ) + if cutlass.const_expr(pro_on): + pro_c1 = ptx.clock64() + + if warp_idx == TMA_WARP: + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_a1) + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_sfa1) + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_a2) + cute.nvgpu.cpasync.prefetch_descriptor(tma_atom_sfa2) + + smem = utils.SmemAllocator() + storage = smem.allocate(self.shared_storage) + ring = pipeline.PipelineTmaUmma.create( + barrier_storage=storage.ring_mbar.data_ptr(), + num_stages=self.num_stages, + producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread), + consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, 1), + tx_count=A_STAGE_BYTES + SFA_STAGE_BYTES, + cta_layout_vmnk=cluster_layout_vmnk, + ) + acc_pipe = pipeline.PipelineUmmaAsync.create( + barrier_storage=storage.acc_mbar.data_ptr(), + num_stages=self.acc_stages, + producer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread), + consumer_group=pipeline.CooperativeGroup(pipeline.Agent.Thread, EPI_WARPS), + cta_layout_vmnk=cluster_layout_vmnk, + ) + b_mbar = storage.b_mbar.data_ptr() + if warp_idx == MMA_WARP: + with cute.arch.elect_one(): + for kb in cutlass.range_constexpr(FC1_KB): + cute.arch.mbarrier_init(b_mbar + kb, 1) + cute.arch.mbarrier_init(b_mbar + X_BAR, 1) + for rho in cutlass.range_constexpr(MAX_ROUNDS): + cute.arch.mbarrier_init(b_mbar + B2_BAR + rho, CLUSTER) + # k-blocks quantized by a peer complete on its bulk copies + for kb in cutlass.range_constexpr(FC1_KB): + if Int32(kb // KB_PER_CTA) != rk: + ptx.mbar_arrive_expect_tx(Int32((b_mbar + kb).toint()), Int32(KB_TX_BYTES)) + cute.arch.mbarrier_init_fence() + # Peers arrive on this CTA's operand barriers: their initialization + # must be visible cluster-wide first (waited before the first remote + # arrive, below). + cute.arch.cluster_arrive_relaxed() + + if warp_idx == 0: + cute.arch.alloc_tmem(TMEM_COLS, storage.tmem_holding) + + s_misc = storage.misc.get_tensor(cute.make_layout(16)) + s_bitmap = storage.bitmap.get_tensor(cute.make_layout(8)) + s_topk_id = storage.topk_id.get_tensor(cute.make_layout(NTOK * TOP_K)) + s_topk_w = storage.topk_w.get_tensor(cute.make_layout(NTOK * TOP_K)) + s_slot_expert = storage.slot_expert.get_tensor(cute.make_layout(MAX_SLOTS)) + s_slot_mask = storage.slot_mask.get_tensor(cute.make_layout(MAX_SLOTS)) + s_slot_w = storage.slot_w.get_tensor(cute.make_layout(MAX_SLOTS * NTOK)) + s_slot_k = storage.slot_k.get_tensor(cute.make_layout(MAX_SLOTS * NTOK)) + s_expert_slot = storage.expert_slot.get_tensor(cute.make_layout(NUM_EXPERTS)) + + s_expert_slot[tidx] = Int32(-1) + s_slot_w[tidx] = Float32(0.0) + s_slot_w[tidx + NUM_THREADS] = Float32(0.0) + if tidx < MAX_SLOTS: + s_slot_mask[tidx] = Uint32(0) + if tidx < 16: + s_misc[tidx] = Int32(0) + if cutlass.const_expr(self.debug & DEBUG_TIMELINE): + if tidx < TL_SLOTS: + ptx.sts_u64(Int32(storage.tl_buf.data_ptr().toint()) + tidx * 8, Int64(0)) + if tidx < 8: + s_bitmap[tidx] = Uint32(0) + bias_r = cute.make_rmem_tensor((8,), Float32) + for j in cutlass.range_constexpr(8): + bias_r[j] = ptx.bf16x2_to_f32(ptx.ld_nc_u16(bias_addr + (lane + 32 * j) * 2), False) + cute.arch.sync_threads() + + tl = cutlass.const_expr(self.debug & DEBUG_TIMELINE) + tl_s = Int32(storage.tl_buf.data_ptr().toint()) + if tidx == 0: + _stamp(tl, tl_s, XT_ENTRY) + if cutlass.const_expr(tl): + ptx.sts_u64(tl_s + XT_GT0 * 8, ptx.globaltimer_u64()) + if cutlass.const_expr(pro_on): + if warp_idx == TMA_WARP: + if lane == 0: + ptx.sts_u64(tl_s + XZ_PRO_CLK0 * 8, pro_c0) + ptx.sts_u64(tl_s + XZ_PRO_GT0 * 8, pro_g0) + ptx.sts_u64(tl_s + XZ_PRO_CLK1 * 8, pro_c1) + cute.arch.griddepcontrol_wait() + if tidx == 0: + _stamp(tl, tl_s, XT_PDL) + if cutlass.const_expr(pro_on): + ptx.sts_u64(tl_s + XZ_PDL_GT * 8, ptx.globaltimer_u64()) + if cutlass.const_expr(self.delay & DELAY_ENTRY): + self._xc_delay(DELAY_ENTRY * 53, bidx) + if warp_idx == TMA_WARP: + with cute.arch.elect_one(): + xbar = Int32((b_mbar + X_BAR).toint()) + sx = Int32(storage.sX.data_ptr().toint()) + ptx.mbar_arrive_expect_tx(xbar, num_tokens * XS_ROW_BYTES) + r = Int32(0) + while r < num_tokens: + ptx.bulk_g2s( + sx + r * XS_ROW_BYTES, + x_addr + r * X_ROW_BYTES + rk * XS_ROW_BYTES, + Int32(XS_ROW_BYTES), + xbar, + ) + r = r + 1 + # Every CTA of the cluster has initialized its barriers: remote + # copies and arrivals may start. + cute.arch.cluster_wait() + if cutlass.const_expr(self.trigger == "early"): + cute.arch.griddepcontrol_launch_dependents() + + # -------- routing (warp t handles token t) -> slot tables -------- + self._route_tokens( + warp_idx, lane, num_tokens, logits_addr, bias_r, s_topk_id, s_topk_w, storage + ) + cute.arch.sync_threads() + e = tidx + bw = s_bitmap[e // 32] + prefix = Int32(0) + for w in cutlass.range_constexpr(8): + word = s_bitmap[w] + if w < e // 32: + prefix = prefix + ptx.popc_u32(word) + below = bw & ((Uint32(1) << Uint32(e % 32)) - Uint32(1)) + prefix = prefix + ptx.popc_u32(below) + if ((bw >> Uint32(e % 32)) & Uint32(1)) != Uint32(0): + s_slot_expert[prefix] = e + s_expert_slot[e] = prefix + if tidx == 0: + total = Int32(0) + for w in cutlass.range_constexpr(8): + total = total + ptx.popc_u32(s_bitmap[w]) + s_misc[0] = total + cute.arch.sync_threads() + if tidx < NTOK * TOP_K: + t = tidx // TOP_K + k = tidx % TOP_K + if t < num_tokens: + e = s_topk_id[tidx] + sl = s_expert_slot[e] + s_slot_w[sl * NTOK + t] = s_topk_w[tidx] + s_slot_k[sl * NTOK + t] = k + ptx.atom_or_shared_u32( + Int32(storage.slot_mask.data_ptr().toint()) + sl * 4, Uint32(1) << Uint32(t) + ) + cute.arch.sync_threads() + num_slots = s_misc[0] + if tidx == 0: + _stamp(tl, tl_s, XT_ROUTED) + + # -------- expert rounds of this cluster -------- + # Round 0 spreads min(D, clusters) experts over all clusters (an + # expert served by r clusters splits its FC2 tiles r ways); when + # D > clusters, round 1 spreads the remaining D - clusters experts + # over all clusters the same way. Every cluster then streams two FC1 + # tiles per CTA and an even share of the FC2 tiles. + nact = ncl + if cutlass.const_expr(self.min_clusters > 0): + if num_slots <= ncl: + nact = cutlass.max(num_slots, Int32(self.min_clusters)) + nact = cutlass.min(nact, ncl) + if cutlass.const_expr(self.two_round_clusters > 0): + if num_slots > ncl: + if num_slots <= Int32(MAX_ROUNDS * self.two_round_clusters): + nact = cutlass.min(Int32(self.two_round_clusters), ncl) + rounds = (num_slots + nact - 1) // nact + if cl >= nact: + rounds = Int32(0) + slots = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + shares = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + firsts = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + t_lo = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + t_hi = cute.make_rmem_tensor((MAX_ROUNDS,), Int32) + for rho in cutlass.range_constexpr(MAX_ROUNDS): + base = Int32(0) + count = cutlass.min(num_slots, nact) + if cutlass.const_expr(rho == 1): + base = nact + count = cutlass.max(num_slots - nact, Int32(1)) + i = cl % count + j = cl // count + nshare = (nact - 1 - i) // count + 1 + a = (j * FC2_MT) // nshare + n = ((j + 1) * FC2_MT) // nshare - a + slots[rho] = base + i + shares[rho] = nshare + firsts[rho] = Int32(0) + if j == 0: + firsts[rho] = Int32(1) + t_lo[rho] = a + (rk * n) // CLUSTER + t_hi[rho] = a + ((rk + 1) * n) // CLUSTER + + if warp_idx == TMA_WARP: + self._xc_tma_producer( + tma_atom_a1, + mA1, + tma_atom_sfa1, + mSFA1, + tma_atom_a2, + mA2, + tma_atom_sfa2, + mSFA2, + a_smem_layout_staged, + sfa_smem_layout_staged, + tiled_mma, + cluster_layout_vmnk, + storage, + ring, + b_mbar, + s_slot_expert, + rk, + rounds, + slots, + shares, + firsts, + t_lo, + t_hi, + w13_addr, + w13_sf_addr, + w2_addr, + w2_sf_addr, + ) + if cutlass.const_expr(self.spec_self): + if bidx == 0: + if warp_idx == MMA_WARP: + self._xc_spec_store(lane, num_slots, spec_ids_addr, storage, s_slot_expert) + if warp_idx == MMA_WARP: + self._xc_mma_issuer( + tiled_mma, + storage, + ring, + acc_pipe, + b_mbar, + a_smem_layout_staged, + sfa_smem_layout_staged, + b_smem_layout_staged, + sfb_smem_layout_staged, + sfb_smem_layout_std, + b2_smem_layout_staged, + sfb2_smem_layout_staged, + rounds, + t_lo, + t_hi, + ) + if (warp_idx != TMA_WARP) & (warp_idx != MMA_WARP): + if rounds > 0: + self._xc_quantize_x( + warp_idx, lane, xgs_addr, num_tokens, storage, b_mbar, dbg_addr, rk + ) + else: + # an idle cluster still lets its activation copy land + cute.arch.mbarrier_wait(b_mbar + X_BAR, 0) + + if bidx == 0: + if tidx < NTOK * TOP_K: + t = tidx // TOP_K + if t < num_tokens: + ptx.st_u16(raw_scale_addr + tidx * 2, ptx.bf16_bits(s_topk_w[tidx])) + ptx.st_u32(raw_idx_addr + tidx * 4, Uint32(tidx)) + if cutlass.const_expr(self.debug & DEBUG_ROUTING): + if bidx == 0: + if tidx < NTOK * TOP_K: + ptx.st_u32(dbg_addr + tidx * 4, Uint32(s_topk_id[tidx])) + ptx.st_f32(dbg_addr + 256 + tidx * 4, s_topk_w[tidx]) + if tidx < MAX_SLOTS: + ptx.st_u32(dbg_addr + 512 + tidx * 4, Uint32(s_slot_expert[tidx])) + ptx.st_u32(dbg_addr + 768 + tidx * 4, s_slot_mask[tidx]) + if tidx == 0: + ptx.st_u32(dbg_addr + 1024, Uint32(num_slots)) + + if warp_idx < EPI_WARPS: + self._xc_epilogue( + tiled_mma, + tidx, + warp_idx, + lane, + storage, + acc_pipe, + b_mbar, + s_slot_expert, + s_slot_mask, + s_slot_k, + rk, + rounds, + slots, + firsts, + t_lo, + t_hi, + num_tokens, + alpha1_addr, + scale_c1_addr, + alpha2_addr, + rows_addr, + out_addr, + counter_addr, + s_topk_w, + num_slots, + bidx, + dbg_addr, + ) + # No CTA leaves while a peer may still store into its shared memory. + cute.arch.cluster_arrive() + cute.arch.cluster_wait() + if warp_idx < EPI_WARPS: + if cutlass.const_expr(self.debug & DEBUG_TIMELINE): + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + if tidx == 0: + _stamp(tl, tl_s, XT_EXIT) + ptx.bar_sync(BAR_EPI, 32 * EPI_WARPS) + if tidx < TL_SLOTS: + ptx.st_u64( + dbg_addr + TL_BYTE_BASE + (bidx * TL_SLOTS + tidx) * 8, + ptx.lds_u64(tl_s + tidx * 8), + ) + if warp_idx == 0: + tmem_ptr = cute.arch.retrieve_tmem_ptr( + Float32, alignment=16, ptr_to_buffer_holding_addr=storage.tmem_holding + ) + cute.arch.dealloc_tmem(tmem_ptr, TMEM_COLS) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/routed_moe.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/routed_moe.py new file mode 100644 index 000000000000..ba398a5031cf --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/kernels/shared_expert_prefetch/routed_moe.py @@ -0,0 +1,308 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Routed side of package shared_expert_prefetch: hop_routing's routed_moe fork that stages the +shared expert in L2, on a chosen grid. + +The MoE region ends with the later of two branches. On the +main stream, hop_routing's fork of routed_moe's expert-cluster kernel adds the routed experts +into routed_moe_into_hop's FP32 accumulator. On the auxiliary stream, the native BF16 shared +expert runs. Both stream their weights from DRAM at the same time, and the +routed grid is kept at 24 clusters so that the shared expert has SMs. + +``SharedPrefetchRoutedMoe`` runs ``SharedPrefetchExpertClusterKernel`` (``kernel_xc.py``). That is +hop_routing's kernel plus an L2 prefetch of the layer's shared-expert weights, issued before its +dependency wait while the pre-MoE hop is still running. Its grid in the FP32 +accumulator mode is ``min(co-resident clusters, DEFAULT_CLUSTERS)`` clusters, at +least ``ceil(R * 8 / 2)``. The schedule, its rules (routed_moe's +``xc_kernel_args``), the memory order after the wait and the arithmetic are those +of hop_routing's fork. The grid changes only the FP32 order in which top-k +contributions are added into the accumulator (routed_moe_into_hop's ``fp32`` +class). + +With package cross_layer_expert_prefetch the runner key carries cross_layer_expert_prefetch's +prefetch spec (``key[6]``, ``RoutedMoe._key``), which ``_compiled`` passes to +``SharedPrefetchExpertClusterKernel`` exactly as hop_routing's and routed_moe's runners do +(capability ``shared_expert_prefetch/expert_prefetch[k=..,ctas=.., self=..]``), and ``__call__`` +passes cross_layer_expert_prefetch's row of predicted experts as the 18th routed_moe address, before +the two shared-expert addresses. The kernel then issues the shared-expert chunks and +cross_layer_expert_prefetch's chunks from one pre-wait prologue (``kernel_xc.py``). + +``routed_output_into_hop(module, bias, x, logits, routing, hop, shared)`` +returns ``None`` before any device work unless the hop takes the FP32 +accumulator, hop_routing's routing is present, and ``shared`` is a BF16 shared expert +whose weights the kernel can prefetch. The caller then runs hop_routing's fork. +""" + +import threading +from typing import Dict, List, Optional, Tuple + +import torch + +from ..hop_routing import routed_moe as hop_routed_mod +from ..routed_moe import routed_moe as routed_moe_mod + +FEATURE = "shared_expert_prefetch" +DEFAULT_CLUSTERS = 24 +SE_HIDDEN = 6144 +SE_INTER = 256 + +_LOCK = threading.Lock() +_COMPILED: Dict[tuple, object] = {} + + +def shared_weights(shared) -> Tuple[Optional[Tuple[torch.Tensor, torch.Tensor]], str]: + """``(gate_up, down)`` weights of a served BF16 shared expert, or ``(None, reason)`` (host-only).""" + if shared is None or type(shared).__name__ != "GatedMLP": + return None, "no BF16 GatedMLP shared expert" + w1 = getattr(getattr(shared, "gate_up_proj", None), "weight", None) + w2 = getattr(getattr(shared, "down_proj", None), "weight", None) + for name, w, shape in ( + ("gate_up", w1, (2 * SE_INTER, SE_HIDDEN)), + ("down", w2, (SE_HIDDEN, SE_INTER)), + ): + if ( + not isinstance(w, torch.Tensor) + or w.dtype != torch.bfloat16 + or tuple(w.shape) != shape + or not w.is_contiguous() + or not w.is_cuda + or w.data_ptr() % 16 + ): + return ( + None, + f"shared {name} weight is not a contiguous 16-byte aligned BF16 {shape} device tensor", + ) + return (w1, w2), "" + + +class SharedPrefetchRoutedMoe(hop_routed_mod.HopRoutedMoe): + """hop_routing's fork running ``SharedPrefetchExpertClusterKernel`` on the + shared_expert_prefetch grid (FP32 accumulator output only).""" + + def _xc_ctas( + self, rows: int = routed_moe_mod.MAX_ROWS, finalize: bool = True, red: bool = False + ) -> int: + from ..routed_moe.kernel_xc import CLUSTER, MAX_ROUNDS + + if not red or finalize: + return super()._xc_ctas(rows, finalize, red) + co_resident = routed_moe_mod._max_active_clusters(self.device, CLUSTER) + want = DEFAULT_CLUSTERS + need = -(-rows * routed_moe_mod.TOP_K // MAX_ROUNDS) + n = CLUSTER * min(co_resident, max(want, need)) + n = min(n, self.num_ctas) + return n - n % CLUSTER + + def _key( + self, finalize: bool, debug: int, rows: int = routed_moe_mod.MAX_ROWS, red: bool = False + ): + return super()._key(finalize, debug, rows, red) + (("shared_expert_prefetch", True),) + + def ready_for_capture( + self, + finalize: bool = False, + debug: int = 0, + rows: int = routed_moe_mod.MAX_ROWS, + red: bool = True, + ) -> bool: + key = self._key(finalize, debug, rows, red) + dev = self.device.index if self.device.index is not None else torch.cuda.current_device() + ws = routed_moe_mod._WORKSPACES.get(dev) + return key in _COMPILED and ws is not None and ws.num_ctas >= self.num_ctas + + def _compiled( + self, finalize: bool, debug: int, rows: int = routed_moe_mod.MAX_ROWS, red: bool = False + ): + """The compiled ``SharedPrefetchExpertClusterKernel`` launcher (expert clusters, FP32 accumulator only).""" + key = self._key(finalize, debug, rows, red) + if key[3] != "xc" or finalize or not red: + raise ValueError( + "the shared_expert_prefetch fork runs the expert-cluster schedule into the FP32 accumulator only" + ) + with _LOCK: + fn = _COMPILED.get(key) + if fn is not None: + return fn + import cutlass + import cutlass.cute as cute + from cuda.bindings import driver as cuda + from cutlass.cute.runtime import make_ptr + + from .kernel_xc import SharedPrefetchExpertClusterKernel + + ptrs = self._pointers(make_ptr, cute, cutlass) + stream = cuda.CUstream(torch.cuda.current_stream().cuda_stream) + # Package cross_layer_expert_prefetch's speculative expert prefetch + # (``RoutedMoe._key``). + spec = key[6] or (0, 0, False) + kern = SharedPrefetchExpertClusterKernel( + self._xc_ctas(rows, finalize, red), + debug=debug, + finalize=False, + red_out=True, + **routed_moe_mod.xc_kernel_args(), + spec_k=spec[0], + spec_ctas=spec[1], + spec_self=spec[2], + se_pf=True, + ) + ints = [cutlass.Int64(0)] * (routed_moe_mod._NUM_XC_ADDRS + 2) + fn = cute.compile(kern.run, *ptrs, *ints, cutlass.Int32(1), stream) + if spec[0]: + from ....shared import dispatch + + dispatch.log_capability( + FEATURE, + f"expert_prefetch[k={spec[0]},ctas={spec[1]},self={int(spec[2])}]", + True, + ) + _COMPILED[key] = fn + return fn + + def __call__( + self, + x: torch.Tensor, + routing: torch.Tensor, + red_out: torch.Tensor, + se_weights: Tuple[torch.Tensor, torch.Tensor], + debug: int = 0, + debug_buffer: Optional[torch.Tensor] = None, + ) -> List[torch.Tensor]: + """Add the routed experts of ``x`` (routing from the hop) into ``red_out`` (FP32 ``[R, H]``, zero on + entry); ``se_weights`` = the shared expert's ``(gate_up, down)`` weights to prefetch.""" + import cutlass + import cutlass.cute as cute + from cuda.bindings import driver as cuda + from cutlass.cute.runtime import make_ptr + + m = self.module + rows = x.shape[0] + x = routed_moe_mod._bulk_copy_ready(x) + if self.schedule(rows, False, True) != "xc": + raise ValueError("FP32 accumulator output needs the expert-cluster schedule") + routed_fn = self._compiled(False, debug, rows, True) + ws = routed_moe_mod._workspace(self.device, self.num_ctas) + dbg = debug_buffer if debug_buffer is not None else ws.dummy + current = torch.cuda.current_stream() + ws.order_on(current) + stream = cuda.CUstream(current.cuda_stream) + ptrs = self._pointers(make_ptr, cute, cutlass) + se_w1, se_w2 = se_weights + addrs = [ + x.data_ptr(), + routing.data_ptr(), + self.bias.data_ptr(), + m.fc31_alpha.data_ptr(), + m.fc31_scale_c.data_ptr(), + m.fc2_alpha.data_ptr(), + m.fc31_input_scale.data_ptr(), + ws.rows.data_ptr(), + ws.scales.data_ptr(), + ws.dummy.data_ptr(), + red_out.data_ptr(), + ws.tile_count.data_ptr(), + dbg.data_ptr(), + m.w3_w1_weight.data_ptr(), + m.w3_w1_weight_scale.data_ptr(), + m.w2_weight.data_ptr(), + m.w2_weight_scale.data_ptr(), + ] + # cross_layer_expert_prefetch's row of predicted experts (the kernel ignores it unless + # cross_layer_expert_prefetch is compiled in). + spec = self._spec() + addrs.append(spec[0].data_ptr() if spec is not None else ws.dummy.data_ptr()) + addrs += [se_w1.data_ptr(), se_w2.data_ptr()] + assert len(addrs) == routed_moe_mod._NUM_XC_ADDRS + 2 + routed_fn(*ptrs, *[cutlass.Int64(a) for a in addrs], cutlass.Int32(rows), stream) + return [red_out] + + +def _engaged_runner(module, routing_bias, x, router_logits, routing, shared): + """``(runner, se_weights)`` for an FP32-accumulator call, or ``(None, None)`` (host-only decision).""" + from ....shared import dispatch + + reason = hop_routed_mod.routing_reason(routing, x) + ok = not reason + se = None + if ok: + se, reason = shared_weights(shared) + ok = se is not None + if ok: + ok, reason = routed_moe_mod.eligibility(module, x, router_logits) + owner = routed_moe_mod.resolve_expert_owner(module)[0] if ok else None + if ok: + rm = getattr(owner, "routing_method", None) + native_bias = getattr(rm, "e_score_correction_bias", None) + if ( + routing_bias is None + or routing_bias.dtype != torch.bfloat16 + or tuple(routing_bias.shape) != (routed_moe_mod.NUM_EXPERTS,) + or not routing_bias.is_contiguous() + or routing_bias.device != x.device + ): + ok, reason = False, "router bias is not a contiguous BF16 [256] on the device" + elif native_bias is not None and native_bias.data_ptr() != routing_bias.data_ptr(): + ok, reason = False, "router bias is not the routing method's bias" + runner = getattr(owner, "_shared_prefetch_runner", None) if ok else None + if ok and runner is not None and runner.bias.data_ptr() != routing_bias.data_ptr(): + runner = None + if ok and runner is None: + runner = SharedPrefetchRoutedMoe(owner, routing_bias) + rows = x.shape[0] + if ok and runner.schedule(rows, False, True) != "xc": + ok, reason = False, "routed_moe would run its grid-wide schedule" + if ok and torch.cuda.is_current_stream_capturing(): + if getattr(owner, "_shared_prefetch_runner", None) is None or not runner.ready_for_capture( + False, rows=rows, red=True + ): + ok, reason = False, "not warmed up before CUDA graph capture" + grid = runner._xc_ctas(rows, False, True) if ok else 0 + dispatch.log_capability( + FEATURE, + f"routed rows={rows} grid={grid} sepf=1 {reason or 'engaged'}", + ok, + reason, + ) + if not ok: + return None, None + owner._shared_prefetch_runner = runner + return runner, se + + +def routed_output_into_hop( + module, + routing_bias: torch.Tensor, + x: torch.Tensor, + router_logits: torch.Tensor, + routing: Optional[torch.Tensor], + hop, + shared=None, +) -> Optional[List[torch.Tensor]]: + """hop_routing's ``routed_output_into_hop`` (FP32 accumulator only) through + ``SharedPrefetchExpertClusterKernel``, which prefetches ``shared``'s weights into L2; ``None``: + use hop_routing's fork.""" + if ( + hop is None + or not getattr(hop, "_routed_enabled", False) + or getattr(hop, "routed_mode", "") != "fp32" + ): + return None + rows = x.shape[0] + if not hop.can_run(rows): + return None + runner, se = _engaged_runner(module, routing_bias, x, router_logits, routing, shared) + if runner is None: + return None + return runner(x, routing, hop.routed_acc_buffer(rows), se) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/modeling.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/modeling.py new file mode 100644 index 000000000000..7abbfb3c7d0d --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/modeling.py @@ -0,0 +1,318 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""ModelingV2 target: glm-5.2-nvfp4 / sm_103 / tp8. + +GLM-5.2, modelopt NVFP4 export: 78 layers, hidden 6144, 64 query heads, MLA with +DSA sparse attention (index top-k 2048), layers 0-2 dense and 3-77 MoE (256 +routed experts at top-8 plus a shared expert). Layer 78 is the checkpoint's bf16 +MTP module, driven by the one-model MTP-Eagle loop at five draft tokens. `tp8` +is the attention heads and the experts tensor-parallel over eight ranks. + +The shell is the built-in ``GlmMoeDsaForCausalLM``: the same module tree, the +same weights and the same engine preferences (KV cache manager, KV transceiver, +sparse-attention defaults), so a built-in context server and this target's +generation server exchange the same KV and indexer-K layout. Construction runs, +in order: + +1. the rank-uniform check of the knobs (``dispatch.check_rank_uniform``), first, + so it runs before every collective the construction below makes; +2. the dispatch plan (``dispatch.resolve``), before the built-in model is built, + so a knob value no rank can parse fails early; +3. the plan is installed (``dispatch.install``): from here on the fast paths read + it; +4. the built-in model, then the core: its text model adopted as + ``GlmMoeDsaTp8ModelingV2``, whose forward hands each step to + ``PrefillTarget`` or ``DecodeTarget``; +5. ``adopt_model`` (``shared.layers``), which re-classes the decoder layers, + the draft layer, their attention modules (``shared.attention``) and their MoE + modules as the family's layer classes and attaches the fast-path objects the + plan enables to them, then re-classes the speculative worker + (``shared.spec_worker``). The attention backends build their FMHA libraries + after construction, in ``create_weights``, each with the libraries adoption + declared on it first; +6. the rank-uniform check of that state (``dispatch.check_rank_uniform_adopted``). + +The step-loop hooks the target declares to the engine (``runtime_hooks``, +``shared.runtime.make_runtime_hooks``) are built later, on the engine's first +read, after the model is loaded. + +The rank-uniform check of step 1 also covers the shared switches the kernels +read and the modeling_v2 mode (``dispatch.rank_uniform_inputs``). + +Every step runs under the phase of the target that takes it +(``dispatch.step``): the shell's forward enters it around the built-in forward, +so the model, the speculative worker and its draft loop all run in it. A +prefill step runs the built-in decoder forward, the built-in layers and the +built-in draft loop; a decode step runs ``DecodeTarget``'s loop, the layers' +decode composition and the worker's draft loop. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + +import torch + +from tensorrt_llm._torch._experimental.modeling_v2._router_index import resolved_mode +from tensorrt_llm._torch.attention.attention import maybe_allgather_for_helix_cp +from tensorrt_llm._torch.attention.backends import AttentionMetadata +from tensorrt_llm._torch.model_config import ModelConfig +from tensorrt_llm._torch.models.modeling_deepseekv3 import DeepseekV3Model, GlmMoeDsaForCausalLM +from tensorrt_llm._torch.pyexecutor.runtime_hooks import RuntimeHooks +from tensorrt_llm._torch.speculative import SpecMetadata + +from ..shared import dispatch, runtime +from ..shared.layers import adopt_model, adopted_state +from . import weights as _weights + +if TYPE_CHECKING: + from ..shared.kernels.decode_prologue import DecodePrologueHook + + +# Every fast path of the family, with the family's tables; the routed-MoE paths run from this +# target's own kernel packages. +TARGET_PATHS = dispatch.TargetPaths( + kernel_modules={ + path: f"{__package__}.kernels.{package}" + for path, package in ( + ("routed_moe", "routed_moe"), + ("routed_moe_into_hop", "routed_moe"), + ("hop_routing", "hop_routing"), + ("shared_expert_prefetch", "shared_expert_prefetch"), + ("draft_routed_moe", "draft_routed_moe"), + ("cross_layer_expert_prefetch", "cross_layer_expert_prefetch"), + ) + } +) + + +class GlmMoeDsaTp8ModelingV2(DeepseekV3Model): + """The text model: the built-in ``DeepseekV3Model``, whose forward picks a target per step. + + Never constructed directly: ``adopt`` re-classes the built-in model the shell + has already built, so the module tree, the parameters and their names stay + the built-in model's. The class adds methods and plain attributes only. + """ + + _prefill: "PrefillTarget" + _decode: "DecodeTarget" + # The decode prologue the step loop runs, set when the target's runtime hooks are built; + # the decode target takes the embedding rows of its launch. + decode_prologue: Optional["DecodePrologueHook"] = None + + @classmethod + def adopt(cls, model: DeepseekV3Model) -> "GlmMoeDsaTp8ModelingV2": + """Re-class the built-in text model as this core and build its two targets. + + Raises: + TypeError: if ``model`` is not exactly the built-in ``DeepseekV3Model``. + """ + if type(model) is not DeepseekV3Model: + raise TypeError( + f"{cls.__name__} adopts the built-in DeepseekV3Model, got {type(model).__name__}" + ) + model.__class__ = cls + model._prefill = PrefillTarget(model) + model._decode = DecodeTarget(model) + return model + + def _select_target(self, attn_metadata) -> "PrefillTarget | DecodeTarget": + """Which target runs this step. + + One host-side integer read and nothing else: a decode-only CUDA graph is a + per-capture constant environment. A mixed batch -- context and generation + rows in one step -- goes to prefill, so prefill is the general case and + decode the specialization that may assume it has no context rows. + """ + return self._decode if attn_metadata.num_contexts == 0 else self._prefill + + def forward( + self, + attn_metadata, + input_ids: torch.IntTensor | None = None, + position_ids: torch.IntTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + spec_metadata=None, + **kwargs, + ) -> torch.Tensor: + return self._select_target(attn_metadata).forward( + attn_metadata, + input_ids=input_ids, + position_ids=position_ids, + inputs_embeds=inputs_embeds, + spec_metadata=spec_metadata, + **kwargs, + ) + + +class PrefillTarget: + """The general case: context rows, and possibly generation rows beside them. + + Runs the built-in decoder forward. + """ + + phase = dispatch.Phase.PREFILL + + def __init__(self, core: GlmMoeDsaTp8ModelingV2) -> None: + self.core = core + + def forward( + self, + attn_metadata, + input_ids: torch.IntTensor | None = None, + position_ids: torch.IntTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + spec_metadata=None, + **kwargs, + ) -> torch.Tensor: + return DeepseekV3Model.forward( + self.core, + attn_metadata, + input_ids=input_ids, + position_ids=position_ids, + inputs_embeds=inputs_embeds, + spec_metadata=spec_metadata, + **kwargs, + ) + + +class DecodeTarget: + """The specialization: routed to only when there are no context rows. + + Runs this target's decoder loop. + """ + + phase = dispatch.Phase.DECODE + + def __init__(self, core: GlmMoeDsaTp8ModelingV2) -> None: + self.core = core + + def forward( + self, + attn_metadata: AttentionMetadata, + input_ids: Optional[torch.IntTensor] = None, + position_ids: Optional[torch.IntTensor] = None, + inputs_embeds: Optional[torch.FloatTensor] = None, + spec_metadata: Optional[SpecMetadata] = None, + **kwargs, + ) -> torch.Tensor: + """The decoder layers of the text model on a decode step. + + The built-in loop with the decode prologue's embedding and the expert-prefetch join. + Each layer is called as a module, so its forward hooks run, and runs its decode + composition under the step's phase. The draft layer is not run here; the last decoder + layer applies the final norm. + """ + if (input_ids is None) ^ (inputs_embeds is not None): + raise ValueError( + "You cannot specify both input_ids and inputs_embeds at the same time, and must specify either one" + ) + + prologue = self.core.decode_prologue + if inputs_embeds is None and prologue is not None: + # The rows the decode prologue launch embedded (None unless that launch ran for + # exactly these ids and this table). + inputs_embeds = prologue.take_embedding( + attn_metadata, input_ids, self.core.embed_tokens + ) + if inputs_embeds is None: + inputs_embeds = self.core.embed_tokens(input_ids) + + hidden_states = inputs_embeds + residual = None + + for idx, decoder_layer in enumerate(self.core.layers[: self.core.num_hidden_layers]): + hidden_states, residual = decoder_layer( + position_ids=position_ids, + hidden_states=hidden_states, + attn_metadata=attn_metadata, + residual=residual, + spec_metadata=spec_metadata, + ) + if dispatch.is_enabled("cross_layer_expert_prefetch"): + # The expert-prefetch stream rejoins before the drafts. + dispatch.get_impl("cross_layer_expert_prefetch", "join")() + + hidden_states = maybe_allgather_for_helix_cp( + hidden_states, attn_metadata, self.core.mapping_with_cp + ) + return hidden_states + + +class ModelingV2Glm52Nvfp4Sm103Tp8(GlmMoeDsaForCausalLM): + """GLM-5.2-NVFP4 on sm_103 at tp8: the built-in causal LM with this target's core and dispatch.""" + + def __init__(self, model_config: ModelConfig): + world_size = model_config.mapping.world_size + knobs = dispatch.read_knobs() + mode = resolved_mode(type(self)) + dispatch.check_rank_uniform(dispatch.rank_uniform_inputs(knobs, mode), world_size) + plan = dispatch.resolve(mode, dict(knobs), TARGET_PATHS) + dispatch.install(plan) + super().__init__(model_config) + GlmMoeDsaTp8ModelingV2.adopt(self.model) + self.plan = plan + adopt_model(self, self.plan) + dispatch.check_rank_uniform_adopted(adopted_state(self), world_size) + + @property + def runtime_hooks(self) -> RuntimeHooks: + """The step-loop hooks this target declares, built on the engine's first read. + + Built after the model is loaded: a hook may hold pinned host memory, which model + construction (under meta-device initialization) must not allocate. + + Raises: + RuntimeError: if building the hooks raises ``AttributeError``, which + ``runtime_hooks_of`` would otherwise read as "no hooks declared". + """ + hooks = self.__dict__.get("_runtime_hooks") + if hooks is None: + try: + hooks = runtime.make_runtime_hooks(self) + except AttributeError as exc: + raise RuntimeError( + f"building the step-loop hooks of {type(self).__name__} failed: {exc}" + ) from exc + self._runtime_hooks = hooks + return hooks + + def forward( + self, + attn_metadata, + input_ids: torch.IntTensor | None = None, + position_ids: torch.IntTensor | None = None, + inputs_embeds: torch.FloatTensor | None = None, + return_context_logits: bool = False, + spec_metadata=None, + resource_manager=None, + **kwargs, + ): + """The built-in forward under the phase of the target that takes this step. + + The scope covers the text model, the speculative worker and its draft loop. + """ + with dispatch.step(self.model._select_target(attn_metadata).phase): + return super().forward( + attn_metadata, + input_ids=input_ids, + position_ids=position_ids, + inputs_embeds=inputs_embeds, + return_context_logits=return_context_logits, + spec_metadata=spec_metadata, + resource_manager=resource_manager, + **kwargs, + ) + + def setup_aliases(self) -> None: + """The built-in aliases, then the cross-layer expert prefetch links.""" + super().setup_aliases() + # Bind each decoder MoE layer to the next MoE layer's gate and experts (structural + # references only). + if dispatch.is_enabled("cross_layer_expert_prefetch"): + dispatch.get_impl("cross_layer_expert_prefetch", "link_layers")( + self.model.layers, self.config.num_hidden_layers + ) + + def load_weights(self, weights): + _weights.load(self, weights) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/weights.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/weights.py new file mode 100644 index 000000000000..b77cd3c04a62 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/glm_5_2_nvfp4__sm_103__tp8/weights.py @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""The weights this target expects: the GLM-5.2 NVFP4 checkpoint as the built-in model loads it. + +The target's module tree is the built-in model's, so the built-in loader maps +every checkpoint tensor, including the bf16 MTP layer and the NVFP4 scales. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from tensorrt_llm._torch.models.modeling_deepseekv3 import GlmMoeDsaForCausalLM + +if TYPE_CHECKING: + from .modeling import ModelingV2Glm52Nvfp4Sm103Tp8 + + +def load(model: "ModelingV2Glm52Nvfp4Sm103Tp8", weights: Any) -> None: + """Load the checkpoint ``weights`` into ``model`` with the built-in loader.""" + GlmMoeDsaForCausalLM.load_weights(model, weights) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/routing.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/routing.py new file mode 100644 index 000000000000..5faf1a030aa2 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/routing.py @@ -0,0 +1,290 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Where a GlmMoeDsaForCausalLM config lands. Read this file and you know. + +``route`` is one forward-reading decision: the facts that fix which code runs, +compared against the deployment the target below was written for. When it does +not fit, ``route`` raises ``NoTarget`` stating those facts. In ``auto`` the +engine then uses the built-in GlmMoeDsa implementation; in ``require`` it raises +that error. + +The three targets are decode specializations of one checkpoint on one GPU +architecture, and differ in how the attention and the experts are split. Their +fast paths are built for the checkpoint's operand shapes and layer pattern, their +ranks and the rows per request (the target token and its draft tokens), so each of +those facts is a routing criterion rather than a runtime branch: + +* ``glm_5_2_nvfp4__sm_103__tp8``: attention and experts tensor-parallel over + eight ranks, five draft tokens; +* ``glm_5_2_nvfp4__sm_103__tep``: attention tensor-parallel over four or eight + ranks and the experts expert-parallel over the same ranks, five draft tokens; +* ``glm_5_2_nvfp4__sm_103__dep``: attention data parallelism and the experts + expert-parallel over the same ranks, at the rank counts, LM-head splits and + draft lengths of ``_DEP``. Two more facts fix what its fast paths serve, the + KV-cache dtype and the MoE backend; ``route`` does not read them, so the + target checks them when it is built. +""" + +from __future__ import annotations + +from typing import Any, Optional, Tuple + +from tensorrt_llm.llmapi.llm_args import MTPDecodingConfig +from tensorrt_llm.quantization.mode import QuantAlgo +from tensorrt_llm.quantization.modelopt_config import ( + canonicalize_quant_algo, + is_modelopt_quant_config, + read_modelopt_quant_config, +) + +from ..._router_index import NoTarget + +# The one GPU architecture the fast paths are built and validated for. +_SM = (10, 3) + +# The checkpoint's config shape; see the gpt_oss routing module for what +# shape-sniffing does and does not pin. The head count fixes the per-rank +# attention operands at tp8, and index_topk fixes the sparse-attention selection +# width the decode kernels are built for. The last four fields fix the layer +# pattern: the dense layers before the first MoE layer, and which layers run +# their own indexer and which reuse an earlier layer's selection (an explicit +# index_topk_pattern overrides the frequency and offset, so it must be absent). +_SHAPE_FIELDS = ( + "num_hidden_layers", + "hidden_size", + "n_routed_experts", + "q_lora_rank", + "num_attention_heads", + "index_topk", + "first_k_dense_replace", + "index_topk_freq", + "index_skip_topk_offset", + "index_topk_pattern", +) +_SHAPE = (78, 6144, 256, 2048, 64, 2048, 3, 4, 3, None) + +# Ranks of tensor-parallel attention and tensor-parallel experts. +_TP = 8 + +# Ranks of tensor-parallel attention with the experts expert-parallel over the same ranks. +_TEP = (4, 8) + +# Draft tokens per request. The decode kernels take the target token plus this +# many draft tokens per request in one step. +_DRAFT_LEN = 5 + +# Attention data parallelism with the experts expert-parallel over the same ranks: per +# (ranks, LM head tensor-parallel in attention DP), the draft tokens per request. +_DEP = {(16, False): 3, (8, False): 3, (8, True): 5} + + +def _nvfp4_with_bf16_mtp_layer(pretrained_config: Any) -> Tuple[bool, str]: + """Whether the checkpoint is NVFP4 with a bf16 MTP layer, and its quantization in words. + + Reads the inline ``quantization_config`` of config.json, and parses only the + modelopt shape, with the modelopt normalizer (which raises only + ``ValueError``). The engine itself prefers ``hf_quant_config.json`` when the + checkpoint has one, which ``route`` does not see, and warns when the inline + copy diverges from it. Any other declaration is named and declined, never + parsed, so a config this target does not serve cannot fail here. The export + keeps the whole MTP layer (layer ``num_hidden_layers``) in bf16; the draft + fast paths read bf16 weights there. + """ + from tensorrt_llm.models.modeling_utils import QuantConfig + + declared = getattr(pretrained_config, "quantization_config", None) + if declared is None: + return False, "none" + if not isinstance(declared, dict) and callable(getattr(declared, "to_dict", None)): + declared = declared.to_dict() + if not isinstance(declared, dict): + return False, f"declared as a {type(declared).__name__}, not a dict" + if not is_modelopt_quant_config(declared): + return False, f"quant_method {declared.get('quant_method')!r} (not a modelopt export)" + try: + normalized = read_modelopt_quant_config(declared) + quant_algo = QuantAlgo(canonicalize_quant_algo(normalized.get("quant_algo"))) + except ValueError as why: + return False, f"a modelopt config without a readable global algorithm ({why})" + excluded = normalized.get("exclude_modules") + if excluded is not None and not ( + isinstance(excluded, (list, tuple)) and all(isinstance(m, str) for m in excluded) + ): + return False, f"modelopt {quant_algo.name} with unreadable excluded modules" + mtp_layer = f"model.layers.{pretrained_config.num_hidden_layers}" + unquantized = QuantConfig(exclude_modules=excluded).is_module_excluded_from_quantization( + mtp_layer + ) + words = f"{quant_algo.name} with the MTP layer {'unquantized' if unquantized else 'quantized'}" + return quant_algo == QuantAlgo.NVFP4 and unquantized, words + + +def _tp8(m: Any) -> bool: + """Attention heads and experts both tensor-parallel over eight ranks, with every + expert on every rank and no attention data parallelism. + + The split decides which weights each rank loads and into which shapes, so it + selects a target rather than a runtime branch. + """ + return ( + m.world_size == _TP + and m.tp_size == _TP + and m.pp_size == 1 + and m.cp_size == 1 + and m.moe_tp_size == _TP + and m.moe_ep_size == 1 + and not m.enable_attention_dp + ) + + +def _tep(m: Any) -> bool: + """Attention heads tensor-parallel over four or eight ranks and the experts expert-parallel + over the same ranks (each rank holds whole experts), with no attention data parallelism. + + The split decides which weights each rank loads and into which shapes, so it + selects a target rather than a runtime branch. + """ + return ( + m.world_size == m.tp_size + and m.tp_size in _TEP + and m.pp_size == 1 + and m.cp_size == 1 + and m.moe_tp_size == 1 + and m.moe_ep_size == m.tp_size + and not m.enable_attention_dp + ) + + +def _dep(m: Any) -> bool: + """Attention data parallelism (each rank runs the whole attention on its own requests) + and the experts expert-parallel over the same ranks, at a rank count and LM-head split + of ``_DEP``. + + The split decides which weights each rank loads and into which shapes, so it + selects a target rather than a runtime branch. + """ + return ( + m.world_size == m.tp_size + and m.pp_size == 1 + and m.cp_size == 1 + and m.moe_tp_size == 1 + and m.moe_ep_size == m.world_size + and m.enable_attention_dp + and (m.world_size, bool(m.enable_lm_head_tp_in_adp)) in _DEP + ) + + +def _mtp_eagle_linear(spec_config: Optional[Any], draft_len: int) -> bool: + """The checkpoint's MTP layer driven by the one-model MTP-Eagle loop, as a + linear chain of exactly ``draft_len`` draft tokens. + + No speculation and other draft lengths do not route here: the decode + kernels are built for the target's rows per request. Nor do the + suffix-automaton draft enhancer and relaxed acceptance: both are fixed for + the engine's lifetime, and the draft fast paths implement neither. + """ + return ( + isinstance(spec_config, MTPDecodingConfig) + and spec_config.spec_dec_mode.is_mtp_eagle_one_model() + and spec_config.max_draft_len == draft_len + and spec_config.max_total_draft_tokens == draft_len + and not spec_config.use_dynamic_tree + and spec_config.sa_config is None + and not spec_config.use_relaxed_acceptance_for_thinking + ) + + +def _speculation(spec_config: Optional[Any]) -> str: + if spec_config is None: + return "off" + words = ( + f"{spec_config.spec_dec_mode.name} max_draft_len={spec_config.max_draft_len} " + f"max_total_draft_tokens={spec_config.max_total_draft_tokens}" + ) + if getattr(spec_config, "use_dynamic_tree", False): + words += " dynamic tree" + if getattr(spec_config, "sa_config", None) is not None: + words += " with the suffix-automaton enhancer" + if getattr(spec_config, "use_relaxed_acceptance_for_thinking", False): + words += " with relaxed acceptance" + return words + + +def route( + pretrained_config: Any, + mapping: Any, + compute_capability: Tuple[int, int], + spec_config: Optional[Any], +) -> type: + """Return the target class for this deployment, or raise ``NoTarget`` saying why none fits. + + The target is imported where it is returned, so declining costs no import + of modeling code. + """ + # A GlmMoeDsa config without one of these fields is another assembly: it reads as + # None and the shape does not match. + shape = tuple(getattr(pretrained_config, name, None) for name in _SHAPE_FIELDS) + + # GLM-5.2-NVFP4 (bf16 MTP layer) on sm103, 8 ranks of tensor-parallel attention + # and experts, one-model MTP-Eagle at five draft tokens. The quantization is + # read last, and only for this deployment. + quantization = "not read" + if ( + compute_capability == _SM + and shape == _SHAPE + and _tp8(mapping) + and _mtp_eagle_linear(spec_config, _DRAFT_LEN) + ): + served, quantization = _nvfp4_with_bf16_mtp_layer(pretrained_config) + if served: + from .glm_5_2_nvfp4__sm_103__tp8.modeling import ModelingV2Glm52Nvfp4Sm103Tp8 + + return ModelingV2Glm52Nvfp4Sm103Tp8 + + # The same checkpoint, architecture and speculation, with 4 or 8 ranks of + # tensor-parallel attention and the experts expert-parallel over them. + if ( + compute_capability == _SM + and shape == _SHAPE + and _tep(mapping) + and _mtp_eagle_linear(spec_config, _DRAFT_LEN) + ): + served, quantization = _nvfp4_with_bf16_mtp_layer(pretrained_config) + if served: + from .glm_5_2_nvfp4__sm_103__tep.modeling import ModelingV2Glm52Nvfp4Sm103Tep + + return ModelingV2Glm52Nvfp4Sm103Tep + + # The same checkpoint and architecture with attention data parallelism and the experts + # expert-parallel over the same ranks, at the draft length of the rank count and LM-head + # split. + if ( + compute_capability == _SM + and shape == _SHAPE + and _dep(mapping) + and _mtp_eagle_linear( + spec_config, _DEP[(mapping.world_size, bool(mapping.enable_lm_head_tp_in_adp))] + ) + ): + served, quantization = _nvfp4_with_bf16_mtp_layer(pretrained_config) + if served: + from .glm_5_2_nvfp4__sm_103__dep.modeling import ModelingV2Glm52Nvfp4Sm103Dep + + return ModelingV2Glm52Nvfp4Sm103Dep + + raise NoTarget( + f"compute capability {compute_capability}, config shape {shape}, quantization " + f"{quantization}, parallel ws={mapping.world_size} tp={mapping.tp_size} " + f"pp={mapping.pp_size} cp={mapping.cp_size} moe_tp={mapping.moe_tp_size} " + f"moe_ep={mapping.moe_ep_size} attention_dp={mapping.enable_attention_dp} " + f"lm_head_tp_in_adp={mapping.enable_lm_head_tp_in_adp}, " + f"speculative decoding {_speculation(spec_config)}; the glm_moe_dsa targets are " + "the GLM-5.2 modelopt NVFP4 checkpoint (bf16 MTP layer) on sm103, with one-model " + "MTP-Eagle as a linear chain, without the suffix-automaton enhancer or relaxed " + "acceptance, at tp8 (8 ranks of tensor-parallel attention and experts) or tep (4 or 8 " + "ranks of tensor-parallel attention with the experts expert-parallel over the same " + "ranks), both without attention DP and at max_draft_len 5, or at dep (attention DP " + "with the experts expert-parallel over the same ranks: 16 ranks at max_draft_len 3, " + "8 ranks at max_draft_len 3, or 8 ranks with the LM head tensor-parallel in attention " + "DP at max_draft_len 5)" + ) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/__init__.py new file mode 100644 index 000000000000..506daf40bc38 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/__init__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Code two or more glm_moe_dsa targets call: the fast-path plan, the attention adoption, the +speculative worker, the step-loop hooks and the kernel packages they share.""" diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/attention.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/attention.py new file mode 100644 index 000000000000..a61b18ea447a --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/attention.py @@ -0,0 +1,569 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""The attention modules of the glm_moe_dsa targets: the MLA, its DSA hooks, its DSA backend and +its indexer. + +``GlmMLA.adopt`` re-classes a layer's built-in attention modules in place -- the +``DeepseekV32Attention`` as ``GlmMLA``, its ``DSAHooks`` as ``GlmDsaHooks``, its +``DSATrtllmAttention`` as ``GlmDsaBackend`` and its ``Indexer`` as ``GlmIndexer`` -- and attaches +the attention fast paths the installed plan enables: the attention output tail and, with +``sparse_mla_decode``, that FMHA library ahead of the registry's on the layer's attention +backends. The backends build their FMHA libraries later, in ``create_weights``, so the library is +created when and where the built-in model creates its own. The classes add methods and +class-level defaults only, so the module tree and the weights stay the built-in model's. + +Each forward override runs the family's decode composition (``attention_forward``) when the +step is a decode step (``dispatch.Phase.DECODE``) and the built-in method otherwise. +""" + +from typing import Optional, Tuple + +import torch + +import tensorrt_llm +import tensorrt_llm.bindings +from tensorrt_llm._torch.attention.attention import _helix_cp_allgather_input +from tensorrt_llm._torch.attention.backends.interface import ( + AttentionForwardArgs, + AttentionInputType, + AttentionMetadata, +) +from tensorrt_llm._torch.attention.backends.sparse.dsa.backend import DSATrtllmAttention +from tensorrt_llm._torch.attention.backends.sparse.dsa.indexer import Indexer +from tensorrt_llm._torch.attention.backends.sparse.dsa.metadata import DSAtrtllmAttentionMetadata +from tensorrt_llm._torch.attention.backends.sparse.dsa.module import DSAHooks +from tensorrt_llm._torch.distributed import AllReduceParams +from tensorrt_llm._torch.models.modeling_deepseekv3 import DeepseekV32Attention +from tensorrt_llm._torch.utils import Fp4QuantizedTensor + +from . import attention_forward, dispatch +from .kernels.indexer_topk import region + + +def reclass(module, builtin: type, cls: type) -> None: + """Re-class ``module``, which must be exactly the built-in class ``builtin``, as ``cls``. + + Raises: + TypeError: if ``module`` is not exactly ``builtin`` (a subclass of it is refused too). + """ + if type(module) is not builtin: + raise TypeError( + f"{cls.__name__} adopts the built-in {builtin.__name__}, got {type(module).__name__}" + ) + module.__class__ = cls + + +def _deferred_norm_view(hidden_states) -> Optional[torch.Tensor]: + """The tensor view of a deferred-norm packet, else None.""" + if dispatch.is_enabled("deferred_norm") and dispatch.get_impl("deferred_norm", "is_deferred")( + hidden_states + ): + return hidden_states.residual + return None + + +class GlmMLA(DeepseekV32Attention): + """A layer's MLA: the built-in attention, with the family's decode composition.""" + + # The attention output tail (``attention_output_tail``) and whether it also prefetches its + # weights into L2 (``attention_output_tail_prefetch``); None keeps the built-in output. + _attn_tail = None + _tail_prefetch = False + + @classmethod + def adopt(cls, mla: DeepseekV32Attention) -> "GlmMLA": + """Re-class a layer's built-in attention modules and attach its attention fast paths. + + Raises: + TypeError: if ``mla``, its DSA hooks, its DSA backend or its indexer is not exactly + the built-in class. + """ + reclass(mla, DeepseekV32Attention, cls) + mla._adopt_attention_tail() + GlmDsaHooks.adopt(mla.sparse_attn_hooks) + GlmDsaBackend.adopt(mla.mqa) + if mla.mqa.indexer is not None: + GlmIndexer.adopt(mla.mqa.indexer) + # The sparse-MLA decode library goes ahead of the registry's libraries on both attention + # backends, which build their FMHA libraries in ``create_weights``. + if dispatch.is_enabled("sparse_mla_decode"): + libraries = (dispatch.get_impl("sparse_mla_decode", "fmha_lib"),) + for backend in (mla.mha, mla.mqa): + if backend is not None: + backend.prepended_fmha_lib_classes = libraries + return mla + + def _adopt_attention_tail(self) -> None: + """The attention output tail: the fused generation W_UV and the local o_proj.""" + # attention_output_tail_prefetch uses the same tail object to stage the tail weights + # into L2 off the critical path while the attention runs. + self._tail_prefetch = dispatch.is_enabled("attention_output_tail_prefetch") + self._attn_tail = None + # attention_output_into_hop publishes the tail's partial into the post-attention hop's + # transport. + if ( + dispatch.is_enabled("attention_output_tail") + or self._tail_prefetch + or dispatch.is_enabled("attention_output_into_hop") + ): + self._attn_tail = dispatch.get_impl("attention_output_tail", "make_attn_tail")(self) + # oproj_weight_staging: the tail's o_proj stages the W_o rows it streams after its + # programmatic-dependent-launch wait before that wait. + if dispatch.is_enabled("oproj_weight_staging"): + self._attn_tail = dispatch.get_impl("oproj_weight_staging", "wrap_attn_tail")( + self, self._attn_tail + ) + + def forward_absorption_generation( + self, + q: torch.Tensor, + compressed_kv: torch.Tensor, + k_pe: torch.Tensor, + attn_metadata: AttentionMetadata, + output: torch.Tensor, + position_ids: Optional[torch.Tensor] = None, + latent_cache: Optional[torch.Tensor] = None, + absorbed_fused_q: Optional[torch.Tensor] = None, + deferred_indexer_k=None, + **kwargs, + ) -> torch.Tensor: + if dispatch.phase() is not dispatch.Phase.DECODE: + return super().forward_absorption_generation( + q=q, + compressed_kv=compressed_kv, + k_pe=k_pe, + attn_metadata=attn_metadata, + output=output, + position_ids=position_ids, + latent_cache=latent_cache, + **kwargs, + ) + return attention_forward.forward_absorption_generation( + self, + q, + compressed_kv, + k_pe, + attn_metadata, + output, + position_ids=position_ids, + latent_cache=latent_cache, + absorbed_fused_q=absorbed_fused_q, + deferred_indexer_k=deferred_indexer_k, + **kwargs, + ) + + def forward( + self, + position_ids: Optional[torch.Tensor], + hidden_states: torch.Tensor, + attn_metadata: AttentionMetadata, + all_reduce_params: Optional[AllReduceParams] = None, + latent_cache_gen: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + if dispatch.phase() is not dispatch.Phase.DECODE: + return super().forward( + position_ids=position_ids, + hidden_states=hidden_states, + attn_metadata=attn_metadata, + all_reduce_params=all_reduce_params, + latent_cache_gen=latent_cache_gen, + ) + hidden_states = _helix_cp_allgather_input( + hidden_states, attn_metadata, self.mapping, self.layer_idx + ) + + # Unquantized view of the module input, used by create_mla_outputs and + # by _apply_output_gate. + output_hidden_states = hidden_states + deferred_view = _deferred_norm_view(hidden_states) + if deferred_view is not None: + # A deferred-norm packet: only the DSA query path consumes it; the outputs are + # allocated on its tensor view. + output_hidden_states = deferred_view + elif isinstance(hidden_states, Fp4QuantizedTensor): + assert hidden_states.unquantized_hidden_states is not None, ( + "MLA.forward received an Fp4QuantizedTensor without a " + "unquantized_hidden_states view" + ) + output_hidden_states = hidden_states.unquantized_hidden_states + + # The attention tail decides here, before the attention op, so the W_UV step inside the + # op and the projection below see one decision. arm() returns False under + # torch.compile and inside breakable CUDA graphs, where the op runs apart from the + # traced or captured projection. + attn_tail = getattr(self, "_attn_tail", None) + tail_armed = attn_tail is not None and attn_tail.arm(self, attn_metadata) + + if self.register_to_config: + attn_output = [ + torch.ops.trtllm.create_mla_outputs(output_hidden_states, self.layer_idx_str) + ] + self._forward_custom_op( + hidden_states, + position_ids, + attn_output, + latent_cache_gen, + ) + else: + attn_output = self._create_outputs( + hidden_states if deferred_view is None else deferred_view, attn_metadata + ) + self.forward_impl( + position_ids, + hidden_states, + attn_metadata, + attn_output=attn_output, + latent_cache_gen=latent_cache_gen, + ) + + attn_output[0] = self._apply_output_gate(output_hidden_states, attn_output[0]) + if tail_armed: + partial = attn_tail.project(self, attn_output[0]) + if partial is not None: + # The tail returns o_proj's rank-local GEMM output; reduce it as the + # ROW-parallel Linear.forward does for this o_proj (the tail takes only an + # o_proj without bias, LoRA or fused GEMM+all-reduce). + if self.o_proj.reduce_output: + partial = self.o_proj.all_reduce(partial, all_reduce_params=all_reduce_params) + return partial + return self._project_output(attn_output, position_ids, attn_metadata, all_reduce_params) + + +class GlmDsaHooks(DSAHooks): + """A layer's DSA hooks: the built-in sparse attention, with the family's decode forward.""" + + @classmethod + def adopt(cls, hooks: DSAHooks) -> "GlmDsaHooks": + """Re-class a layer's built-in DSA hooks. + + Raises: + TypeError: if ``hooks`` is not exactly the built-in ``DSAHooks``. + """ + reclass(hooks, DSAHooks, cls) + return hooks + + def forward( + self, + mla, + position_ids: Optional[torch.Tensor], + hidden_states: torch.Tensor, + attn_metadata: AttentionMetadata, + attn_output: list[torch.Tensor], + ) -> None: + if dispatch.phase() is not dispatch.Phase.DECODE: + return super().forward( + mla=mla, + position_ids=position_ids, + hidden_states=hidden_states, + attn_metadata=attn_metadata, + attn_output=attn_output, + ) + attention_forward.forward_sparse_attn( + mla, position_ids, hidden_states, attn_metadata, attn_output + ) + + def forward_custom_op( + self, + mla, + hidden_states: torch.Tensor, + position_ids: Optional[torch.Tensor], + attn_output: list[torch.Tensor], + latent_cache_gen: Optional[torch.Tensor], + ) -> bool: + if dispatch.phase() is not dispatch.Phase.DECODE: + return super().forward_custom_op( + mla=mla, + hidden_states=hidden_states, + position_ids=position_ids, + attn_output=attn_output, + latent_cache_gen=latent_cache_gen, + ) + attention_forward.forward_sparse_attn_custom_op( + mla, + hidden_states, + position_ids, + attn_output, + latent_cache_gen, + ) + return True + + +class GlmDsaBackend(DSATrtllmAttention): + """A layer's DSA attention backend, with the family's top-k bridge on decode steps.""" + + @classmethod + def adopt(cls, backend: DSATrtllmAttention) -> "GlmDsaBackend": + """Re-class a layer's built-in DSA attention backend. + + Raises: + TypeError: if ``backend`` is not exactly the built-in ``DSATrtllmAttention``. + """ + reclass(backend, DSATrtllmAttention, cls) + return backend + + def sparse_attn_predict( + self, + q: torch.Tensor, + k: Optional[torch.Tensor], + metadata: DSAtrtllmAttentionMetadata, + forward_args: AttentionForwardArgs, + ) -> Tuple[Optional[torch.Tensor], Optional[torch.Tensor]]: + """Run the DSA indexer and convert its local TopK to paged indices.""" + if dispatch.phase() is not dispatch.Phase.DECODE: + return super().sparse_attn_predict( + q=q, k=k, metadata=metadata, forward_args=forward_args + ) + is_generation = forward_args.attention_input_type == AttentionInputType.generation_only + sparse_backend_args = forward_args.sparse_backend_args + phase_start = metadata.num_ctx_tokens if is_generation else 0 + phase_end = metadata.num_tokens if is_generation else metadata.num_ctx_tokens + + shared_topk_indices = metadata.shared_topk_indices + # rope_kv_append: one fused bridge kernel performs the native top-k copy of this call + # and the local -> global remap. With cross_rank_topk_global_emit the cross-rank top-k + # writes them and records it on its packet. + bridge = None + bridge_path = ( + "cross_rank_topk_global_emit" + if dispatch.is_enabled("cross_rank_topk_global_emit") + else "rope_kv_append" + ) + if dispatch.is_enabled("rope_kv_append"): + bridge = dispatch.get_impl(bridge_path, "new_bridge_packet")(metadata, is_generation) + if self.indexer is None: + topk_indices = shared_topk_indices[phase_start:phase_end] + else: + indexer_intermediates = sparse_backend_args.indexer_intermediates + # The indexer's top-k region reads the bridge of this call only. + self.indexer._topk_bridge = bridge + try: + topk_indices = self.indexer.forward_from_projected( + metadata, + q, + indexer_intermediates, + is_generation=is_generation, + ) + finally: + self.indexer._topk_bridge = None + preserve_mtp_topk = metadata.in_mtp_draft_loop and self.indexer.mtp_index_share + if shared_topk_indices is not None and not preserve_mtp_topk: + if bridge is not None: + dispatch.get_impl("rope_kv_append", "defer_shared_copy")( + bridge, shared_topk_indices, phase_start, topk_indices + ) + else: + shared_topk_indices[ + phase_start : phase_start + topk_indices.shape[0], + : topk_indices.shape[1], + ].copy_(topk_indices) + + local_layer_idx = self.get_local_layer_idx(metadata) + kv_cache_dtype = getattr(metadata.kv_cache_manager, "dtype", None) + if kv_cache_dtype == tensorrt_llm.bindings.DataType.NVFP4 and not is_generation: + metadata._ensure_pool_view_cached() + page_index_scale, layer_offset = ( + metadata.kv_cache_manager.get_primary_pool_page_index_params(local_layer_idx) + ) + total_kv_tokens = metadata.num_ctx_mla_kv_tokens + if total_kv_tokens <= 0: + raise RuntimeError("NVFP4 DSA context has no active KV tokens") + head_dim = metadata.kv_cache_manager.head_dim + # Static sparse FMHA may prefetch up to TopK rows even when a + # short context has fewer valid KV tokens. The compaction itself + # can select at most min(total KV tokens, query/TopK pairs), so a + # late chunk with a long cached prefix does not need one fp8 row + # for every token in that prefix. + max_selected_tokens = min(total_kv_tokens, topk_indices.numel()) + scratch_capacity = max(max_selected_tokens, topk_indices.shape[1]) + # The previous layer's attention has already been enqueued on this + # stream. Drop its Python reference before allocating the next + # layer's scratch so the caching allocator can reuse that storage + # in stream order instead of transiently keeping two full buffers. + metadata.nvfp4_mla_context_fp8_scratch = None + scratch = torch.empty( + (scratch_capacity, 1, head_dim), + dtype=torch.float8_e4m3fn, + device=topk_indices.device, + ) + compact_indices = torch.empty_like(topk_indices) + torch.ops.trtllm.nvfp4_mla_context_kv_cache_gather( + metadata.host_kv_cache_pool_pointers, + metadata.host_kv_cache_pool_mapping, + topk_indices, + metadata._cached_req_idx_ctx, + metadata._cached_block_table_ctx, + metadata.ctx_kv_indptr[: metadata.num_contexts + 1], + scratch, + compact_indices, + self._nvfp4_mla_kv_scale_quant_orig, + local_layer_idx, + total_kv_tokens, + metadata._cached_tokens_per_block, + page_index_scale * metadata._cached_tokens_per_block, + layer_offset, + metadata.kv_cache_manager.mla_kv_cache_residual_dim, + metadata._cached_num_pool_tokens, + ) + metadata.nvfp4_mla_context_fp8_scratch = scratch + forward_args.sparse_runtime_params.aux_kv_cache_pool_ptr = scratch.data_ptr() + return compact_indices, None + + if bridge is not None: + topk_indices_global = dispatch.get_impl(bridge_path, "remap_topk_to_global")( + self, topk_indices, metadata, bridge + ) + else: + topk_indices_global = self._remap_topk_to_global(topk_indices, metadata, is_generation) + + if kv_cache_dtype == tensorrt_llm.bindings.DataType.NVFP4: + scratch = metadata.nvfp4_mla_fp8_scratch + compact_indices = metadata.nvfp4_mla_compact_indices + if scratch is None or compact_indices is None: + raise RuntimeError("NVFP4 MLA scratch buffers were not allocated") + num_rows = topk_indices_global.shape[0] + if num_rows > scratch.shape[0] or num_rows > compact_indices.shape[0]: + raise RuntimeError( + "NVFP4 MLA generation rows exceed the allocated MTP " + f"workspace: requested {num_rows}, capacity {scratch.shape[0]}" + ) + scratch = scratch[:num_rows] + compact_indices = compact_indices[:num_rows] + torch.ops.trtllm.nvfp4_mla_kv_cache_gather( + metadata.host_kv_cache_pool_pointers, + metadata.host_kv_cache_pool_mapping, + topk_indices_global, + scratch, + compact_indices, + self._nvfp4_mla_kv_scale_quant_orig, + local_layer_idx, + metadata.kv_cache_manager.mla_kv_cache_residual_dim, + metadata._cached_num_pool_tokens, + ) + # Static sparse MLA treats indices as offsets from kvPtr. Feed it + # compact offsets and the FP8 scratch base; TRTLLMGen is unchanged. + forward_args.sparse_runtime_params.aux_kv_cache_pool_ptr = scratch.data_ptr() + return compact_indices, None + + return topk_indices_global, None + + +class GlmIndexer(Indexer): + """A full-indexer layer's DSA indexer, with the family's decode top-k region.""" + + # The top-k bridge of the current ``GlmDsaBackend.sparse_attn_predict`` call (set only for + # that call, on a decode step); None keeps the built-in copies. + _topk_bridge = None + + @classmethod + def adopt(cls, indexer: Indexer) -> "GlmIndexer": + """Re-class a layer's built-in indexer. + + Raises: + TypeError: if ``indexer`` is not exactly the built-in ``Indexer``. + """ + reclass(indexer, Indexer, cls) + return indexer + + def _copy_reused_topk( + self, + metadata: DSAtrtllmAttentionMetadata, + topk_indices_buffer: torch.Tensor, + token_offset: int, + num_gen_tokens: int, + num_generations: int, + has_prefill: bool, + gvr_prior_indices: Optional[torch.Tensor], + ) -> None: + # rope_kv_append: the fused bridge reads the shared rows and writes this copy itself. + bridge = self._topk_bridge + if ( + bridge is not None + and not has_prefill + and num_gen_tokens == num_generations + and dispatch.get_impl("rope_kv_append", "defer_reuse_copy")( + bridge, + topk_indices_buffer, + token_offset, + metadata.shared_topk_indices[:num_generations, :], + gvr_prior_indices is not None, + ) + ): + return + super()._copy_reused_topk( + metadata, + topk_indices_buffer, + token_offset, + num_gen_tokens, + num_generations, + has_prefill, + gvr_prior_indices, + ) + + def _fill_decode_topk( + self, + metadata: DSAtrtllmAttentionMetadata, + q_fp8: torch.Tensor, + weights: torch.Tensor, + topk_indices_buffer: torch.Tensor, + token_offset: int, + num_gen_tokens: int, + ) -> bool: + if dispatch.phase() is not dispatch.Phase.DECODE: + return super()._fill_decode_topk( + metadata=metadata, + q_fp8=q_fp8, + weights=weights, + topk_indices_buffer=topk_indices_buffer, + token_offset=token_offset, + num_gen_tokens=num_gen_tokens, + ) + out = topk_indices_buffer[token_offset : token_offset + num_gen_tokens, :] + # The context-partitioned top-k reproduces the default selection only, so a + # deterministic top-k goes to the indexer_topk entry. + if not self.top_k.gvr_deterministic and region.cross_rank_topk_decode( + self, metadata, q_fp8, weights, token_offset, num_gen_tokens, out, self._topk_bridge + ): + return True + return region.indexer_topk_decode( + self, metadata, q_fp8, weights, token_offset, num_gen_tokens, out + ) + + def _store_mtp_shared_rows( + self, + topk_indices_buffer: torch.Tensor, + metadata: DSAtrtllmAttentionMetadata, + num_contexts: int, + num_generations: int, + num_gen_tokens: int, + token_offset: int, + has_prefill: bool, + has_decode: bool, + is_generation: Optional[bool], + ) -> None: + # rope_kv_append: the fused bridge stashes each request's last accepted row (the same + # selection as ``_mtp_last_accepted_rows``). + bridge = self._topk_bridge + if bridge is not None and has_decode and not has_prefill and is_generation: + num_accepted = metadata.mtp_num_accepted + if num_accepted is not None: + num_accepted = num_accepted[num_contexts : num_contexts + num_generations] + if dispatch.get_impl("rope_kv_append", "defer_mtp_row_stash")( + bridge, + metadata.shared_topk_indices, + num_contexts, + num_generations, + num_gen_tokens // num_generations, + num_accepted, + ): + return + super()._store_mtp_shared_rows( + topk_indices_buffer, + metadata, + num_contexts, + num_generations, + num_gen_tokens, + token_offset, + has_prefill, + has_decode, + is_generation, + ) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/attention_forward.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/attention_forward.py new file mode 100644 index 000000000000..91133f458fca --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/attention_forward.py @@ -0,0 +1,744 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""The MLA generation and DSA sparse-attention forward of the glm_moe_dsa targets on a decode step. + +These are the built-in functions of ``attention/mla.py`` and ``attention/backends/sparse/dsa`` +with the family's fast paths composed in: + +- the absorbed-query packet: one producer runs the DSA projections and the absorbed query + (``q_nope @ W_UK``) for a layer, so the generation skips its absorption BMM; +- the deferred-norm packet: the hidden state a post-MoE hop hands over before its RMSNorm, + normalized inside the next layer's projection (or materialized when no producer serves); +- the fused generation rope, which also performs the deferred indexer K-cache scatter; +- the attention output tail, which takes over the W_UV BMM when ``GlmMLA.forward`` armed it. + +``attention.GlmMLA`` and ``attention.GlmDsaHooks`` call them only on a decode step; every other +step runs the built-in functions. ``trtllm::glm_moe_dsa_mla_dsa_attn_inplace`` is the family's +copy of the built-in DSA op 2 (``trtllm::mla_dsa_attn_inplace``): the same op on this module's +``_forward_dsa_attn``. +""" + +from typing import List, Optional + +import torch + +from tensorrt_llm._torch.attention.attention import extract_extra_attrs +from tensorrt_llm._torch.attention.backends.interface import AttentionInputType, AttentionMetadata +from tensorrt_llm._torch.attention.backends.sparse.dsa.module import ( + forward_context_sparse_attn, + forward_dsa_proj, + forward_sparse_mla_kvcache_bf16, + should_use_short_mha, +) +from tensorrt_llm._torch.attention.backends.sparse.dsa.params import DSABackendForwardArgs +from tensorrt_llm._torch.attention.mla import fp8_block_scaling_bmm_out +from tensorrt_llm._torch.modules.multi_stream_utils import maybe_execute_in_parallel +from tensorrt_llm._torch.pyexecutor.breakable_cuda_graph import eager_on_graph +from tensorrt_llm._torch.utils import Fp4QuantizedTensor +from tensorrt_llm._utils import get_sm_version + +from . import dispatch + + +def _absorbed_query_packet(self, position_ids, hidden_states, attn_metadata): + """Return the absorbed-query packet of this layer's DSA projections, or None. + + The producers are tried in order; each decides its eligibility before any device work and + returns None outside its domain, in which case the next one, and finally the built-in + projection, runs: + + - a deferred-norm packet goes to the deferred-norm projection (the evict-hints variant when + selected), which normalizes it inside the projection; + - ``fused_kv_indexer_projection``: one projection launch for the fused [q_a | kv_a] and the + indexer's wk | weights_proj outputs; + - ``indexer_layer_front``: the query kernel of the full-indexer target layers and the draft + layer (the same packet bytes); + - ``absorbed_query`` (with ``shared_indexer_absorbed_query`` first on the shared-indexer + layers); + - ``draft_absorbed_query`` on the draft layer. + """ + if dispatch.is_enabled("deferred_norm") and dispatch.get_impl("deferred_norm", "is_deferred")( + hidden_states + ): + # The deferred-norm projection feeds the absorbed-query kernels; None makes the caller + # materialize the packet. + entry = ( + "deferred_norm_evict_hints" + if dispatch.is_enabled("deferred_norm_evict_hints") + else "deferred_norm" + ) + return dispatch.get_impl(entry, "dsa_proj")( + self, position_ids, hidden_states, attn_metadata + ) + if dispatch.is_enabled("fused_kv_indexer_projection"): + packet = dispatch.get_impl("fused_kv_indexer_projection", "dsa_proj")( + self, position_ids, hidden_states, attn_metadata + ) + if packet is not None: + return packet + if dispatch.is_enabled("indexer_layer_front"): + # The indexer-front query kernel on the full-indexer target layers and the draft layer + # (the same packet bytes); None runs the absorbed-query entries below. + packet = dispatch.get_impl("indexer_layer_front", "dsa_proj")( + self, position_ids, hidden_states, attn_metadata + ) + if packet is not None: + return packet + if dispatch.is_enabled("absorbed_query"): + if dispatch.is_enabled("shared_indexer_absorbed_query"): + packet = dispatch.get_impl("absorbed_query", "shared_dsa_proj")( + self, position_ids, hidden_states, attn_metadata + ) + if packet is not None: + return packet + packet = dispatch.get_impl("absorbed_query", "dsa_proj")( + self, position_ids, hidden_states, attn_metadata + ) + if packet is not None: + return packet + if dispatch.is_enabled("draft_absorbed_query"): + return dispatch.get_impl("absorbed_query", "mtp_dsa_proj")( + self, position_ids, hidden_states, attn_metadata + ) + return None + + +def _deferred_norm_materialize(hidden_states): + """The normalized hidden state of a deferred-norm packet (bit-identical to the post-MoE + hop's normalized output); any other input passes through.""" + if dispatch.is_enabled("deferred_norm") and dispatch.get_impl("deferred_norm", "is_deferred")( + hidden_states + ): + return hidden_states.materialize() + return hidden_states + + +def forward_absorption_generation( + self, + q: torch.Tensor, + compressed_kv: torch.Tensor, + k_pe: torch.Tensor, + attn_metadata: AttentionMetadata, + output: torch.Tensor, + position_ids: Optional[torch.Tensor] = None, + latent_cache: Optional[torch.Tensor] = None, + absorbed_fused_q: Optional[torch.Tensor] = None, + deferred_indexer_k=None, + **kwargs, +) -> torch.Tensor: + """Absorbed-MLA generation (``MLA.forward_absorption_generation``) on a decode step. + + ``absorbed_fused_q`` is the absorbed-query packet: a + ``[num_tokens, num_heads_tp, kv_lora_rank + qk_rope_head_dim]`` buffer whose + ``[..., :kv_lora_rank]`` already holds ``q_nope @ W_UK``. When given, only the absorption + BMM is skipped; RoPE, cache append, FP8 query quantization and scheduler buffers run as + usual. ``deferred_indexer_k`` is the indexer K-cache scatter ``_forward_dsa_attn`` deferred + into this call's rope. + """ + num_tokens = q.shape[0] + q_nope, q_pe = q.view([-1, self.num_heads_tp, self.qk_head_dim]).split( + [self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1 + ) + + # fused_q contains 1) the result of the following bmm with shape [num_tokens, num_heads, kv_lora_rank] + # 2) rope(q_pe) with shape [num_tokens, num_heads, qk_rope_head_dim]. rope is applied inside AttentionOp + num_seqs = attn_metadata.num_seqs + + # The rope kernel writes seqQOffset[batch_idx + 1] for every + # batch_idx < batch_size, and CUB-scans total_s_len / seq_len entries + # into seqKVOffsets (mlaKernels.cu:579, :650). `batch_size` there is the + # count of presented rows, which equals num_seqs only while every + # request contributes the same number of query tokens. Under the + # token-major presentation a row is a token, so size these by the token + # count. Uniform verification keeps the established request-major + # allocation rather than scaling these temporary buffers by K. + cu_seqlens_rows = ( + max(num_seqs, num_tokens) + 1 + if getattr(attn_metadata, "is_ragged_verify", False) + else num_seqs + 1 + ) + cu_q_seqlens = torch.empty(cu_seqlens_rows, dtype=torch.int32, device=q.device) + cu_kv_seqlens = torch.empty(cu_seqlens_rows, dtype=torch.int32, device=q.device) + fmha_scheduler_counter = torch.empty(1, dtype=torch.uint32, device=q.device) + use_fp8_mla = getattr(self.mqa, "has_fp8_kv_cache", False) or getattr( + self.mqa, "has_fp4_kv_cache", False + ) + + mla_bmm1_scale = None + mla_bmm2_scale = None + quant_q_buffer = None + if use_fp8_mla: + mla_bmm1_scale = torch.empty(2, dtype=torch.float32, device=q.device) + mla_bmm2_scale = torch.empty(1, dtype=torch.float32, device=q.device) + quant_q_buffer = torch.empty( + num_tokens, + self.num_heads_tp, + (self.kv_lora_rank + self.qk_rope_head_dim), + dtype=torch.uint8, + device=q.device, + ) + + if hasattr(self, "k_b_proj_trans"): + if absorbed_fused_q is not None: + fused_q = absorbed_fused_q + else: + fused_q = torch.empty( + [ + num_tokens, + self.num_heads_tp, + (self.kv_lora_rank + self.qk_rope_head_dim), + ], + dtype=q.dtype, + device=q.device, + ) + + fp4_mla = ( + self.sparse_params is None + and self.mqa.support_fp4_kv_cache() + and self.quant_config is not None + and self.quant_config.layer_quant_mode.has_fp4_kv_cache() + ) + if fp4_mla and latent_cache is None: + raise RuntimeError( + "FP4 MLA generation requires a latent cache for fused " + "Q quantization, RoPE, and cache update." + ) + fuse_fp4_q_quant = fp4_mla + fp4_rope_kwargs = {"fuse_fp4_q_quant": True} if fuse_fp4_q_quant else {} + + def _mla_gen_rope(): + # rope_kv_append: one fused launch writes the bytes of mla_rope_generation plus the + # deferred indexer K-cache scatter. Every check runs before any device work; + # otherwise the deferred scatter runs natively first and the native path follows. + if deferred_indexer_k is not None or dispatch.is_enabled("rope_kv_append"): + rope_fused = ( + not self.apply_rotary_emb + and self.kv_cache_dtype != "fp8_ds_mla" + and not fp4_rope_kwargs + and dispatch.get_impl("rope_kv_append", "rope_eligible")( + self.mqa, + attn_metadata, + fused_q, + q_pe, + latent_cache, + quant_q_buffer, + mla_bmm1_scale, + mla_bmm2_scale, + ) + ) + if rope_fused: + # indexer_layer_front: the same fused rope, forked to trigger its + # dependents after its last read (the same bytes). + rope_owner = "rope_kv_append" + if dispatch.is_enabled("indexer_layer_front"): + front_rope = dispatch.get_impl("indexer_layer_front", "rope_available")() + dispatch.log_capability( + "indexer_layer_front", + "rope", + front_rope, + "" if front_rope else "indexer_layer_front rope op not loaded", + ) + if front_rope: + rope_owner = "indexer_layer_front" + dispatch.get_impl(rope_owner, "mla_rope_generation")( + self.mqa, + fused_q, + q_pe, + latent_cache, + attn_metadata, + cu_q_seqlens, + cu_kv_seqlens, + fmha_scheduler_counter, + mla_bmm1_scale, + mla_bmm2_scale, + quant_q_buffer, + indexer_k=deferred_indexer_k, + ) + return + if deferred_indexer_k is not None: + dispatch.get_impl("rope_kv_append", "run_indexer_k_scatter_native")( + deferred_indexer_k + ) + if self.apply_rotary_emb: + # Non-fused backends (Vanilla / FlashInfer) do not fuse RoPE + # in the attention kernel. Reuse apply_rope, which rotates + # q's q_pe slice in place, then copy rotated k_pe into the + # latent cache that the backend appends. + assert position_ids is not None + assert latent_cache is not None + gen_position_ids = position_ids[..., attn_metadata.num_ctx_tokens :] + k_pe_rope = self.apply_rope(q, k_pe, gen_position_ids) + fused_q[..., self.kv_lora_rank :] = q_pe + latent_cache[..., self.kv_lora_rank :] = k_pe_rope + else: + # Fused backend (TRTLLM): RoPE, latent-cache append and the + # trtllm-gen scheduler buffers are all produced in-kernel. + if self.kv_cache_dtype == "fp8_ds_mla": + fused_q[..., self.kv_lora_rank :] = q_pe + else: + self.mqa.mla_rope_generation( + fused_q, + q_pe, + latent_cache, + attn_metadata, + cu_q_seqlens, + cu_kv_seqlens, + fmha_scheduler_counter, + mla_bmm1_scale, + mla_bmm2_scale, + quant_q_buffer, + **fp4_rope_kwargs, + ) + + if fuse_fp4_q_quant or use_fp8_mla: + rope_stream = None + else: + rope_stream = self.aux_stream + if absorbed_fused_q is not None: + _mla_gen_rope() + elif self.k_b_proj_trans.dtype == torch.bfloat16: + # [num_heads, num_tokens, self.qk_nope_head_dim] + q_nope_t = q_nope.transpose(0, 1) + # [num_heads, num_tokens, self.kv_lora_rank] + q_nope_out = fused_q[..., : self.kv_lora_rank].transpose(0, 1) + + # [num_heads, num_tokens, self.qk_nope_head_dim] x [num_heads, kv_lora_rank, qk_nope_head_dim] + # -> [num_heads, num_tokens, kv_lora_rank] -> [num_tokens, num_heads, kv_lora_rank] + # The output of bmm is written directly into fused_q + maybe_execute_in_parallel( + lambda: self._bmm_bf16_out( + q_nope_t, + self.k_b_proj_trans, + self.k_b_proj_trans.transpose(1, 2), + q_nope_out, + self._locality_domain_k_b_proj_trans_shards, + ), + _mla_gen_rope, + self.ln_events[0], + self.ln_events[1], + rope_stream, + ) + + elif self.k_b_proj_trans.dtype == torch.float8_e4m3fn: + # [num_heads, num_tokens, self.kv_lora_rank] + q_nope_out = fused_q[..., : self.kv_lora_rank].transpose(0, 1) + + maybe_execute_in_parallel( + lambda: fp8_block_scaling_bmm_out( + q_nope, + self.k_b_proj_trans, + self.k_b_proj_trans_scale, + q_nope_out, + self.k_b_proj_trans_dequant, + self.use_cute_dsl_blockscaling_bmm, + ), + _mla_gen_rope, + self.ln_events[0], + self.ln_events[1], + rope_stream, + ) + else: + raise NotImplementedError(f"Missing bmm impl for dtype: {self.k_b_proj_trans.dtype}.") + + fused_q = fused_q.view( + [ + num_tokens, + self.num_heads_tp * (self.kv_lora_rank + self.qk_rope_head_dim), + ] + ) + else: + raise RuntimeError("MLA absorption requires k_b_proj_trans") + + # attention_output_tail_prefetch: on a call that GlmMLA.forward armed, fork the L2 + # prefetch of the tail weights onto a private stream while the attention runs; the + # projection step joins it. No-op otherwise (decided before any device work). + attn_tail = getattr(self, "_attn_tail", None) + if getattr(self, "_tail_prefetch", False) and attn_tail is not None: + attn_tail.before_attention(self, point="attn") + + attn_out_latent = self._attn_forward_gen( + self.mqa, + fused_q, + None, + None, + position_ids, + attn_metadata, + attention_input_type=AttentionInputType.generation_only, + out_scale=self.out_scale, + latent_cache=latent_cache, # kvcache and k_pe + q_pe=q_pe, # used by `invokeMLARopeGeneration` + cu_q_seqlens=cu_q_seqlens, # used by `mlaGeneration` + cu_kv_seqlens=cu_kv_seqlens, # used by `mlaGeneration` + fmha_scheduler_counter=fmha_scheduler_counter, # used by `mlaGeneration` + mla_bmm1_scale=mla_bmm1_scale, # used by `mlaGeneration` + mla_bmm2_scale=mla_bmm2_scale, # used by `mlaGeneration` + quant_q_buffer=quant_q_buffer, # used by `mlaGeneration` + **kwargs, + ) + fused_q = None + + # note: if we do not have CP, then num_heads_tp_cp == num_heads_tp + assert ( + attn_out_latent.shape[0] == q.shape[0] + and attn_out_latent.shape[1] == self.num_heads_tp_cp * self.kv_lora_rank + ) + + # [seq, num_heads, kv_lora_rank] + attn_out_latent = attn_out_latent.view([-1, self.num_heads_tp_cp, self.kv_lora_rank]) + + # Acts only on a call that GlmMLA.forward armed; when the attention tail takes the call, + # its projection step consumes the latent and the native W_UV BMM is skipped. The latent + # shape is checked before any device work. + attn_tail = getattr(self, "_attn_tail", None) + if attn_tail is not None and attn_tail.defer_w_uv(self, attn_metadata, attn_out_latent, output): + return output + + attn_output = output.view([num_tokens, self.num_heads_tp_cp, self.v_head_dim]) + + if self.v_b_proj.dtype == torch.bfloat16: + # [num_heads, seq, kv_lora_rank] x [num_heads, kv_lora_rank, v_head_dim] + # -> [num_heads, seq, v_head_dim] + self._bmm_bf16_out( + attn_out_latent.transpose(0, 1), + self.v_b_proj, + self.v_b_proj.transpose(1, 2), + attn_output.transpose(0, 1), + self._locality_domain_v_b_proj_shards, + ) + elif self.v_b_proj.dtype == torch.float8_e4m3fn: + fp8_block_scaling_bmm_out( + attn_out_latent, + self.v_b_proj, + self.v_b_proj_scale, + attn_output.transpose(0, 1), + self.v_b_proj_dequant, + self.use_cute_dsl_blockscaling_bmm, + ) + else: + raise NotImplementedError(f"Missing bmm impl for dtype: {self.v_b_proj.dtype}.") + + return output + + +def forward_generation_sparse_attn( + self, + q: torch.Tensor, + compressed_kv: torch.Tensor, + k_pe: torch.Tensor, + attn_metadata: AttentionMetadata, + output: torch.Tensor, + position_ids: Optional[torch.Tensor] = None, + latent_cache: Optional[torch.Tensor] = None, + indexer_intermediates: Optional[List[torch.Tensor]] = None, + absorbed_fused_q: Optional[torch.Tensor] = None, + deferred_indexer_k=None, +) -> torch.Tensor: + if get_sm_version() >= 100: + return self.forward_absorption_generation( + q, + compressed_kv, + k_pe, + attn_metadata, + output, + position_ids=position_ids, + latent_cache=latent_cache, + absorbed_fused_q=absorbed_fused_q, + deferred_indexer_k=deferred_indexer_k, + sparse_backend_args=DSABackendForwardArgs(indexer_intermediates=indexer_intermediates), + ) + assert absorbed_fused_q is None, "absorbed_query preabsorbed queries require SM100+" + assert deferred_indexer_k is None + return forward_sparse_mla_kvcache_bf16( + self, + q, + latent_cache, + attn_metadata, + output, + indexer_intermediates, + is_generation=True, + ) + + +def forward_sparse_attn( + self, + position_ids: Optional[torch.Tensor], + hidden_states: torch.Tensor, + attn_metadata: AttentionMetadata, + attn_output: list[torch.Tensor], +) -> None: + """ + Forward pass for the MLA module with DSA (always in MQA mode). + Writes result into output tensor in-place. + + Delegates to forward_dsa_proj (token-wise projections) followed by + _forward_dsa_attn (batch-dependent attention dispatch). An absorbed-query packet replaces + forward_dsa_proj; a deferred-norm packet that no producer takes is materialized first. + + Args: + position_ids (Optional[torch.IntTensor]): The position IDs. + hidden_states (torch.Tensor): The hidden states. + attn_metadata (AttentionMetadata): The attention metadata. + attn_output: Algorithm-defined output tensors. DSA writes to the + standard output at index zero. + """ + output = attn_output[0] + query_packet = _absorbed_query_packet(self, position_ids, hidden_states, attn_metadata) + if query_packet is None: + hidden_states = _deferred_norm_materialize(hidden_states) + proj_outputs = forward_dsa_proj(self, position_ids, hidden_states, attn_metadata) + absorbed_fused_q = None + else: + proj_outputs, absorbed_fused_q = query_packet.proj_outputs, query_packet.fused_q + q, compressed_kv, k_pe, latent_cache = proj_outputs[:4] + indexer_intermediates = proj_outputs[4:] + _forward_dsa_attn( + self, + q, + compressed_kv, + k_pe, + latent_cache, + indexer_intermediates, + position_ids, + attn_metadata, + output, + absorbed_fused_q=absorbed_fused_q, + ) + + +def forward_sparse_attn_custom_op( + self, + hidden_states: torch.Tensor, + position_ids: Optional[torch.Tensor], + attn_output: list[torch.Tensor], + latent_cache_gen: Optional[torch.Tensor], +) -> None: + """Run DSA's two-stage custom ops for piecewise CUDA graph capture. + + The second op is the family's ``trtllm::glm_moe_dsa_mla_dsa_attn_inplace``. With an + absorbed-query packet the attention runs directly, without the ops. + """ + del latent_cache_gen + from tensorrt_llm._torch.attention.backends.sparse.dsa import custom_ops # noqa: F401 + + if ( + dispatch.is_enabled("absorbed_query") or dispatch.is_enabled("draft_absorbed_query") + ) and not isinstance(hidden_states, Fp4QuantizedTensor): + metadata, _ = extract_extra_attrs(self.layer_idx_str, "mla") + query_packet = _absorbed_query_packet(self, position_ids, hidden_states, metadata) + if query_packet is not None: + # The packet producers decline under torch.compile and breakable CUDA graphs, so + # the two-op split is not needed on this path. + proj_outputs = query_packet.proj_outputs + _forward_dsa_attn( + self, + *proj_outputs[:4], + proj_outputs[4:], + position_ids, + metadata, + attn_output[0], + absorbed_fused_q=query_packet.fused_q, + ) + return + hidden_states = _deferred_norm_materialize(hidden_states) + + if isinstance(hidden_states, Fp4QuantizedTensor): + proj_outputs = torch.ops.trtllm.mla_dsa_proj( + hidden_states.unquantized_hidden_states, + position_ids, + self.layer_idx_str, + hidden_states.fp4_tensor, + hidden_states.scaling_factor, + ) + else: + proj_outputs = torch.ops.trtllm.mla_dsa_proj( + hidden_states, position_ids, self.layer_idx_str + ) + q, compressed_kv, k_pe, latent_cache = proj_outputs[:4] + indexer_intermediates = proj_outputs[4:] + maybe_bcg_glm_moe_dsa_mla_dsa_attn_inplace( + q, + compressed_kv, + k_pe, + latent_cache, + indexer_intermediates, + position_ids, + self.layer_idx_str, + attn_output[0], + ) + + +def _forward_dsa_attn( + self, + q: torch.Tensor, + compressed_kv: torch.Tensor, + k_pe: torch.Tensor, + latent_cache: torch.Tensor, + indexer_intermediates: List[torch.Tensor], + position_ids: Optional[torch.Tensor], + attn_metadata: AttentionMetadata, + output: torch.Tensor, + absorbed_fused_q: Optional[torch.Tensor] = None, +) -> None: + """Batch-structure-dependent attention for DSA MLA (Op 2, not graph-captured). + + indexer_intermediates is [q_fp8, k_fp8, k_scale, weights, q_scale] + when the indexer ran in Op 1, or [] when short-MHA handled all tokens. + + All num_tokens slicing happens here (not in Op 1) because + num_tokens comes from batch-specific metadata and must not be + baked into CUDA graph capture. + """ + num_contexts = attn_metadata.num_contexts + num_generations = attn_metadata.num_generations + num_ctx_tokens = attn_metadata.num_ctx_tokens + num_tokens = attn_metadata.num_tokens + + # Slice Op 1 outputs to actual num_tokens (Op 1 operates on the + # full padded tensor for CUDA graph compatibility). + q = q[:num_tokens, ...] + compressed_kv = compressed_kv[:num_tokens, ...] + k_pe = k_pe[:num_tokens, ...] + latent_cache = latent_cache[:num_tokens, ...] + if position_ids is not None: + position_ids = position_ids[..., :num_tokens] + + use_short_mha_for_ctx = num_contexts > 0 and should_use_short_mha( + self, attn_metadata, position_ids + ) + + deferred_indexer_k = None + if not (use_short_mha_for_ctx and num_generations == 0) and self.mqa.indexer is not None: + q_fp8, k_fp8, k_scale, weights, q_scale = indexer_intermediates + # Slice indexer intermediates to actual num_tokens (they were + # computed on the full padded tensor in Op 1). + q_fp8 = q_fp8[:num_tokens, ...] + k_fp8 = k_fp8[:num_tokens, ...] + k_scale = k_scale[:num_tokens, ...] + weights = weights[:num_tokens, ...] + q_scale = q_scale[:num_tokens, ...] + indexer_intermediates = [q_fp8, k_fp8, k_scale, weights, q_scale] + # Update the full mixed batch once before context/generation prediction + # splits. Context chunk gathers must observe the newly appended keys. + # rope_kv_append: a pure-generation batch folds this scatter into the same layer's + # fused generation rope, which still runs before the indexer reads the K cache. The + # deferred scatter must reach the generation call below, which is checked right after + # that call. + if dispatch.is_enabled("rope_kv_append"): + deferred_indexer_k = dispatch.get_impl("rope_kv_append", "defer_indexer_k_scatter")( + self, k_fp8, k_scale, attn_metadata, forwards_packet=True + ) + if deferred_indexer_k is None: + self.mqa.indexer._update_k_cache(k_fp8, k_scale, attn_metadata) + else: + indexer_intermediates = None + + assert output is not None, "output must be provided" + + if num_contexts > 0: + q_ctx = q[:num_ctx_tokens, ...] + compressed_kv_ctx = compressed_kv[:num_ctx_tokens, ...] + k_pe_ctx = k_pe[:num_ctx_tokens, ...] + latent_cache_ctx = latent_cache[:num_ctx_tokens, ...] + ctx_position_ids = position_ids[..., :num_ctx_tokens] if position_ids is not None else None + if self.apply_rotary_emb: + assert ctx_position_ids is not None + k_pe_ctx = self.apply_rope(q_ctx, k_pe_ctx, ctx_position_ids) + + forward_context_sparse_attn( + self, + q_ctx, + compressed_kv_ctx, + k_pe_ctx, + attn_metadata, + output[:num_ctx_tokens, :], + latent_cache_ctx, + position_ids=ctx_position_ids, + indexer_intermediates=indexer_intermediates, + ) + + if num_generations > 0: + q_gen = q[num_ctx_tokens:, ...] + compressed_kv_gen = compressed_kv[num_ctx_tokens:, ...] + k_pe_gen = k_pe[num_ctx_tokens:, ...] + latent_cache_gen = latent_cache[num_ctx_tokens:, ...] + gen_position_ids = ( + position_ids[..., num_ctx_tokens:num_tokens] if position_ids is not None else None + ) + if self.apply_rotary_emb: + assert gen_position_ids is not None + k_pe_gen = self.apply_rope(q_gen, k_pe_gen, gen_position_ids) + + forward_generation_sparse_attn( + self, + q_gen, + compressed_kv_gen, + k_pe_gen, + attn_metadata, + output[num_ctx_tokens:num_tokens, :], + latent_cache=latent_cache_gen, + position_ids=gen_position_ids, + indexer_intermediates=indexer_intermediates, + absorbed_fused_q=( + absorbed_fused_q[num_ctx_tokens:num_tokens] + if absorbed_fused_q is not None + else None + ), + deferred_indexer_k=deferred_indexer_k, + ) + if deferred_indexer_k is not None: + # Raises unless the generation rope (fused or the native fallback) executed the + # deferred indexer K-cache scatter. + dispatch.get_impl("rope_kv_append", "check_indexer_k_scatter_consumed")(deferred_indexer_k) + + # Join the prev_topk copy forked in sparse_attn_indexer; CUDA graph + # capture requires the join within the same layer's forward. + if self.mqa.indexer is not None: + self.mqa.indexer.maybe_join_prev_topk_copy() + + +@torch.library.custom_op("trtllm::glm_moe_dsa_mla_dsa_attn_inplace", mutates_args=("output",)) +def glm_moe_dsa_mla_dsa_attn_inplace( + q: torch.Tensor, + compressed_kv: torch.Tensor, + k_pe: torch.Tensor, + latent_cache: torch.Tensor, + indexer_intermediates: List[torch.Tensor], + position_ids: Optional[torch.Tensor], + layer_idx: str, + output: torch.Tensor, +) -> None: + """Batch-structure-dependent attention dispatch for DSA MLA, on ``_forward_dsa_attn`` above. + + The built-in ``trtllm::mla_dsa_attn_inplace`` with the family's op-2 function: + indexer_intermediates is [q_fp8, k_fp8, k_scale, weights, q_scale] when the indexer ran in + Op 1, or [] when short-MHA handled all tokens. This op is excluded from CUDA graph capture. + """ + metadata, mla_layer = extract_extra_attrs(layer_idx, "mla") + _forward_dsa_attn( + mla_layer, + q, + compressed_kv, + k_pe, + latent_cache, + indexer_intermediates, + position_ids, + metadata, + output, + ) + + +@glm_moe_dsa_mla_dsa_attn_inplace.register_fake +def _glm_moe_dsa_mla_dsa_attn_inplace_fake( + q: torch.Tensor, + compressed_kv: torch.Tensor, + k_pe: torch.Tensor, + latent_cache: torch.Tensor, + indexer_intermediates: List[torch.Tensor], + position_ids: Optional[torch.Tensor], + layer_idx: str, + output: torch.Tensor, +) -> None: + """Model the in-place output mutation during fake-tensor propagation.""" + + +maybe_bcg_glm_moe_dsa_mla_dsa_attn_inplace = eager_on_graph(glm_moe_dsa_mla_dsa_attn_inplace) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/dispatch.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/dispatch.py new file mode 100644 index 000000000000..1cf9a9abb921 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/dispatch.py @@ -0,0 +1,1209 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""The decode fast paths of the glm_moe_dsa targets, which of them a process runs, and what happens +when one cannot. + +Composition. ``FAST_PATHS`` names every fast path of the family once, with the +paths it extends (``depends_on``): a path that extends another has no effect +without it. Each target serves some of them (``TargetPaths``), and every path +it serves is on by default. ``TRTLLM_STAIRCASE_GLM_LL_DISABLE`` takes a +comma-separated list of path names and marks those off in the plan together +with every path that depends on them, transitively; a name that is not a path +the target serves raises. ``resolve`` reads it once, at construction, into a +``Plan``. The fast paths read the installed plan through ``is_enabled``; +``DISABLE`` turns a path and its dependents off. + +Rank uniformity. Several fast paths are collectives across the model's ranks: a +rank that runs one while a peer runs the generic path waits forever. So every +rank must read the same knob values. ``check_rank_uniform`` allgathers them, +with the shared switches the kernels read and the modeling_v2 mode +(``rank_uniform_inputs``), once, at construction, before they are parsed and +before any fast path builds a collective workspace, and raises on every rank +with the per-rank table on a mismatch. The plan is a function of the knobs +alone, so equal knobs give every rank the same plan, and a value no rank can +parse fails on every rank alike. Once the model is built, +``check_rank_uniform_adopted`` compares the fast-path state every rank +attached to its modules the same way. + +Phases. ``step`` runs a model step under the ``Phase`` of the target that +takes it (``PREFILL`` or ``DECODE``); ``phase`` reads it, and is None outside +a step. + +Two kinds of not taking a fast path, kept apart: + +* **uncovered**: the step is outside the shapes the path takes (more rows than + its kernel holds, a KV length outside its tier, a prefill step). ``uncovered`` + logs the reason once per (path, reason); the caller runs the next tier or the + generic path in every mode. +* **missed**: the step is inside the shapes the path takes, but a precondition + is absent (a workspace, a compiled kernel, multi-node NVLink memory, a device + check). Under ``modeling_v2='require'`` ``missed`` raises + ``FastPathUnavailable`` naming the path and the precondition; otherwise it + logs once per (path, reason), counts the miss, and the caller falls back. A + miss decided per call never raises: it is counted and logged once. + +The kernels report their decisions through ``log_capability``, which also +classifies every decline (``classify_decline``) by where it is decided +(``DECLINE_KINDS``), the layers each path serves (``LAYER_CELLS``) and the +reason: a path turned off is neither kind, a decline decided once inside the +path's layers is a miss, and a decline decided per call is a miss only when +its reason names a missing resource. + +Selection. A kernel or a shared hook asks ``is_enabled(path)`` whether a fast +path runs, looks up its entry points with ``get_impl(path, name)`` (the kernel +package of the path registers them with ``register`` when it is imported) and +logs its per-rank decisions with ``log_capability``. ``is_enabled`` answers +from the installed plan (``install``); without one, which is the case of every +process that does not build a target of the family, no fast path runs. ``install`` logs a +record line (``feature_record``): every enabled path with its numerical variant +(``NUMERICAL_VARIANTS``), every set knob of the family and the shared +TensorRT-LLM switches the kernels read. + +Validation. ``TRTLLM_MODELING_V2_VALIDATE`` (read once, in ``resolve``) turns on +the diagnostics of the fast paths that have some (``validate_mode``): the +step-input stager's guard and the draft loop's in-pipeline native oracle. + +Knobs. The knobs of the family are ``KNOBS``: ``DISABLE``, the numerical variant +of ``routed_moe_into_hop`` and the cross-rank top-k wait timeout (read by the +C++ kernels). A target refuses a knob that only a path it does not serve reads +(``KNOB_PATHS``). The kernels run at fixed launch parameters; ``resolve`` refuses +every other ``TRTLLM_STAIRCASE_GLM_LL_*`` variable that is set. +""" + +from __future__ import annotations + +import enum +import functools +import hashlib +import importlib +import json +import os +import re +import threading +from dataclasses import dataclass, field +from typing import ( + Any, + Callable, + Dict, + FrozenSet, + Iterable, + List, + Mapping, + Optional, + Sequence, + Tuple, +) + +from tensorrt_llm._torch._experimental.modeling_v2._router_index import ( + MODELING_V2_VALIDATE_ENV, + ModelingV2Mode, + step_contract_enabled, +) +from tensorrt_llm.logger import logger + +#: Prefix of every knob of this target; every set variable with it is part of the rank-uniform +#: check. +KNOB_PREFIX = "TRTLLM_STAIRCASE_GLM_LL_" +#: Comma-separated fast-path names to turn off, with every path that depends on them. +DISABLE_ENV = KNOB_PREFIX + "DISABLE" +#: The numerical variant of ``routed_moe_into_hop`` (``NUMERICAL_VARIANTS``). +ACCUMULATION_ENV = KNOB_PREFIX + "ROUTED_MOE_INTO_HOP_ACCUMULATION" +#: How long the cross-rank top-k kernels wait for their peers before they trap, in ms (C++). +CROSS_RANK_TOPK_TIMEOUT_ENV = KNOB_PREFIX + "CROSS_RANK_TOPK_TIMEOUT_MS" +#: The knobs of this target; ``resolve`` refuses any other set ``KNOB_PREFIX`` variable. +KNOBS: FrozenSet[str] = frozenset({DISABLE_ENV, ACCUMULATION_ENV, CROSS_RANK_TOPK_TIMEOUT_ENV}) +#: A fast-path selector this target does not read; ``resolve`` refuses it. +FEATURE_ENV = KNOB_PREFIX + "FEATURES" +#: A narrowing of ``deferred_input_prep`` to some of its parts; ``resolve`` refuses it. +PARTS_ENV = KNOB_PREFIX + "DEFERRED_INPUT_PREP_PARTS" + +_VALIDATE_HINT = f"set {MODELING_V2_VALIDATE_ENV}=1 instead" +#: Knobs this target does not read, each with what to use instead; ``resolve`` refuses them. +REMOVED_KNOBS: Mapping[str, str] = { + FEATURE_ENV: f"every fast path of this target is on; {KNOB_PREFIX}DISABLE turns paths off", + PARTS_ENV: ( + "each part is a fast path of its own (prep = deferred_input_prep, store = " + f"sampler_store, stream = execution_stream_loop); list the parts to drop in " + f"{KNOB_PREFIX}DISABLE" + ), + KNOB_PREFIX + "DEFERRED_INPUT_PREP_DEBUG": _VALIDATE_HINT, + KNOB_PREFIX + "DRAFT_LOOP_VERIFY": _VALIDATE_HINT, + KNOB_PREFIX + "CROSS_LAYER_EXPERT_PREFETCH_STATS": "the hit counters are not reported", + KNOB_PREFIX + "CROSS_LAYER_EXPERT_PREFETCH_REPORT_EVERY": "the hit counters are not reported", + KNOB_PREFIX + "SHARED_INDEXER_ABSORBED_QUERY_FORCE_NATIVE": ( + f"list shared_indexer_absorbed_query in {KNOB_PREFIX}DISABLE to run absorbed_query on the " + "shared-indexer layers" + ), +} +#: What ``resolve`` says about any other set variable with the prefix that is not in ``KNOBS``. +_FIXED_HINT = "the fast paths of this target run at fixed launch parameters" + +_LOG_TAG = "[glm_moe_dsa]" + + +@dataclass(frozen=True) +class FastPath: + """One decode fast path: its name and the paths it extends.""" + + name: str + depends_on: Tuple[str, ...] = () + + +# The attention query front, the indexer and top-k selection, sparse attention, +# the attention output and its cross-rank hop, the MoE region and its hop, the +# dense layers, the draft (MTP) layer and loop, and the engine-side step work. +FAST_PATHS: Tuple[FastPath, ...] = ( + # The target layers' query front: the query projections and the absorbed query. + FastPath("absorbed_query"), + # The same query front on the draft layer. + FastPath("draft_absorbed_query"), + # The query front of layers that reuse an earlier layer's indexer selection. + FastPath("shared_indexer_absorbed_query", depends_on=("absorbed_query",)), + # One projection launch for the latent KV and the indexer key and weights. + FastPath("fused_kv_indexer_projection", depends_on=("absorbed_query",)), + # The query front of layers that run the full indexer, with its rope and + # top-k exchange started early. + FastPath( + "indexer_layer_front", + depends_on=( + "absorbed_query", + "draft_absorbed_query", + "indexer_topk", + "rope_kv_append", + "cross_rank_topk", + ), + ), + # The next layer's RMSNorm deferred across the post-MoE hop into its projection. + FastPath("deferred_norm", depends_on=("absorbed_query", "post_moe_hop")), + # The deferred-norm hop and projection chain with cache-eviction hints. + FastPath("deferred_norm_evict_hints", depends_on=("deferred_norm",)), + # Rope, FP8 query, latent-KV append, indexer-K scatter and the top-k to + # KV-index bridge in one launch per layer. + FastPath("rope_kv_append"), + # Indexer logits and top-k selection on decode steps. + FastPath("indexer_topk"), + # Top-k over the KV pages each rank owns, merged across the ranks. + FastPath("cross_rank_topk"), + # The cross-rank top-k with a cluster merge of the per-rank candidates. + FastPath("cross_rank_topk_cluster_merge", depends_on=("cross_rank_topk",)), + # The cluster merge also emits the top-k to KV-index bridge. + FastPath( + "cross_rank_topk_global_emit", + depends_on=("rope_kv_append", "cross_rank_topk_cluster_merge"), + ), + # Sparse MLA attention for the decode rows. + FastPath("sparse_mla_decode"), + # The attention output projection and its local tail. + FastPath("attention_output_tail"), + # The attention tail's prefetch schedule: the next weights are fetched while the current + # attention runs. + FastPath("attention_output_tail_prefetch", depends_on=("attention_output_tail",)), + # The attention output published straight into the post-attention hop. + FastPath( + "attention_output_into_hop", depends_on=("attention_output_tail", "post_attention_hop") + ), + # Output-projection weight staging inside an already-waiting kernel of the attention window. + FastPath("oproj_weight_staging", depends_on=("attention_output_into_hop", "sparse_mla_decode")), + # Cross-rank sum, residual add, RMSNorm and router after attention, in one hop. + FastPath("post_attention_hop"), + # The MoE routing (top-8) computed inside the post-attention hop. + FastPath("hop_routing", depends_on=("attention_output_into_hop", "routed_moe")), + # The router GEMM at the pre-MoE boundary. + FastPath("router_gemm"), + # The routed NVFP4 experts in one launch. + FastPath("routed_moe"), + # The routed-expert result written straight into the post-MoE hop. + FastPath("routed_moe_into_hop", depends_on=("post_moe_hop", "routed_moe")), + # The shared expert's weights prefetched into L2 by the routed-expert launch. + FastPath("shared_expert_prefetch", depends_on=("routed_moe_into_hop", "hop_routing")), + # Cross-rank finalize, reduction and RMSNorm after the MoE, in one hop. + FastPath("post_moe_hop"), + # The next MoE layer's predicted experts prefetched into L2. + FastPath("cross_layer_expert_prefetch", depends_on=("routed_moe",)), + # The dense layers' small-row MLP. + FastPath("dense_mlp"), + # The draft layer's input normalization and packing. + FastPath("draft_front"), + # The draft layer's bf16 routed experts. + FastPath("draft_routed_moe"), + # Only the accepted rows of the first draft step through the post-attention hop. + FastPath("draft_live_rows", depends_on=("post_attention_hop", "draft_loop")), + # Draft-token selection at the end of each draft step. + FastPath("draft_tail"), + # The draft loop's embedding, bookkeeping and sparse-attention metadata. + FastPath("draft_loop"), + # The decode step's metadata prologue and the target embedding in one launch. + FastPath("decode_prologue", depends_on=("draft_loop",)), + # The decode step's input preparation recorded and issued as one launch. + FastPath("deferred_input_prep"), + # The speculative sampler's store update and host copy in one launch. + FastPath("sampler_store"), + # The overlap executor loop on the execution stream. + FastPath("execution_stream_loop"), + # Freed host pages returned to the operating system lazily on decode ranks. + FastPath("host_page_retention", depends_on=("post_attention_hop",)), +) + +FAST_PATH_NAMES: Tuple[str, ...] = tuple(p.name for p in FAST_PATHS) +_BY_NAME: Mapping[str, FastPath] = {p.name: p for p in FAST_PATHS} + + +def _dependents() -> Dict[str, FrozenSet[str]]: + """Every path that depends on each path, directly or transitively.""" + direct: Dict[str, List[str]] = {name: [] for name in FAST_PATH_NAMES} + for path in FAST_PATHS: + for base in path.depends_on: + direct[base].append(path.name) + closure = {} + for name in FAST_PATH_NAMES: + seen, todo = set(), list(direct[name]) + while todo: + dependent = todo.pop() + if dependent not in seen: + seen.add(dependent) + todo.extend(direct[dependent]) + closure[name] = frozenset(seen) + return closure + + +_DEPENDENTS = _dependents() + + +def parse_disable(raw: Optional[str], served: Iterable[str] = FAST_PATH_NAMES) -> FrozenSet[str]: + """The fast-path names a ``DISABLE`` value lists (case and blanks ignored). + + ``served`` is the fast paths of the target that reads the value (every fast path by default). + + Raises: + ValueError: naming every entry that is not one of ``served``. + """ + if not raw: + return frozenset() + names = {tok.strip().lower() for tok in raw.split(",") if tok.strip()} + served = frozenset(served) + unknown = sorted(names - served) + if unknown: + raise ValueError( + f"{DISABLE_ENV} names {unknown}, which are not fast paths of this target; " + f"the fast paths are {[name for name in FAST_PATH_NAMES if name in served]}" + ) + return frozenset(names) + + +def disable_closure(names: FrozenSet[str]) -> FrozenSet[str]: + """``names`` and every path that depends on one of them.""" + closed = set(names) + for name in names: + closed |= _DEPENDENTS[name] + return frozenset(closed) + + +#: Every knob of this target set in an environment, as (name, value) sorted by name. +Knobs = Tuple[Tuple[str, str], ...] + + +def read_knobs(environ: Optional[Mapping[str, str]] = None) -> Knobs: + """Every knob of the family set in ``environ`` (the process environment by default).""" + env = os.environ if environ is None else environ + return tuple(sorted((k, v) for k, v in env.items() if k.startswith(KNOB_PREFIX))) + + +def knob_digest(knobs: Knobs) -> str: + """A short digest of ``knobs`` for the log line.""" + return hashlib.sha256(repr(knobs).encode()).hexdigest()[:16] + + +@dataclass(frozen=True) +class Plan: + """The composition this process resolved: which fast paths run, and under which mode.""" + + mode: ModelingV2Mode + # Paths the DISABLE knob names. + disabled: FrozenSet[str] + # Paths turned off only because a path they depend on is disabled. + disabled_dependents: FrozenSet[str] + # Every knob of this target set in the environment, as (name, value). + knobs: Knobs + # Whether the fast paths run their diagnostics (TRTLLM_MODELING_V2_VALIDATE). + validate: bool = False + # What the target that resolved the plan serves (``TargetPaths``); None serves every path. + target: Optional["TargetPaths"] = None + + @property + def require(self) -> bool: + return self.mode is ModelingV2Mode.REQUIRE + + @property + def served(self) -> FrozenSet[str]: + """The fast paths the target serves; the others are off in every plan of the target.""" + return frozenset(FAST_PATH_NAMES) if self.target is None else self.target.served + + def enabled(self, name: str) -> bool: + """Whether fast path ``name`` runs in this process. + + Raises: + KeyError: if ``name`` is not a fast path, so a misspelled call site fails. + """ + if name not in _BY_NAME: + raise KeyError(f"{name!r} is not a fast path of this target") + return ( + name in self.served + and name not in self.disabled + and name not in self.disabled_dependents + ) + + def describe(self) -> str: + """One line naming the mode, the number of fast paths that run and what is off.""" + on = sum(self.enabled(name) for name in FAST_PATH_NAMES) + words = f"modeling_v2={self.mode.value}, {on}/{len(self.served)} fast paths" + if self.disabled: + words += f", disabled {sorted(self.disabled)}" + if self.disabled_dependents: + words += f" and their dependents {sorted(self.disabled_dependents)}" + return words + f", knobs {knob_digest(self.knobs)}" + + +#: The knobs that only one fast path reads; a target that does not serve the path refuses them. +KNOB_PATHS: Mapping[str, str] = {ACCUMULATION_ENV: "routed_moe_into_hop"} + + +def _refuse_removed_knobs(knobs: Knobs, served: Iterable[str] = FAST_PATH_NAMES) -> None: + """Raise if a variable with the knob prefix that is not in ``KNOBS`` is set, or a knob of a + fast path outside ``served``, naming each with what to use instead.""" + served = frozenset(served) + refused = [ + f"{name} ({REMOVED_KNOBS.get(name, _FIXED_HINT)})" for name, _ in knobs if name not in KNOBS + ] + refused += [ + f"{name} (this target does not serve {KNOB_PATHS[name]})" + for name, _ in knobs + if name in KNOB_PATHS and KNOB_PATHS[name] not in served + ] + if refused: + raise ValueError(f"this target does not read {'; '.join(refused)}; unset them") + + +def resolve( + mode: ModelingV2Mode, + environ: Optional[Mapping[str, str]] = None, + target: Optional["TargetPaths"] = None, +) -> Plan: + """Read the knobs once and resolve the composition of ``target``; arms ``missed`` for ``mode``. + + ``target`` is what the target serves (every fast path, with the family's tables, when None). + + Raises: + ValueError: if ``DISABLE`` names something that is not a fast path the target serves, or + if a variable with the knob prefix that is not in ``KNOBS`` (or a knob of a path the + target does not serve) is set. + """ + knobs = read_knobs(environ) + served = FAST_PATH_NAMES if target is None else target.served + _refuse_removed_knobs(knobs, served) + disabled = parse_disable(dict(knobs).get(DISABLE_ENV), served) + plan = Plan( + mode=mode, + disabled=disabled, + disabled_dependents=disable_closure(disabled) - disabled, + knobs=knobs, + validate=step_contract_enabled(), + target=target, + ) + set_require(plan.require) + logger.info(f"{_LOG_TAG} dispatch: {plan.describe()}") + return plan + + +# -------------------------------------------------------------------------------------------------- +# Rank uniformity +# -------------------------------------------------------------------------------------------------- + +#: Gathers one rank's value from every rank of a ``world_size``-rank group, or returns None +#: (having logged why) when no such group is available. +Allgather = Callable[[Any, int], Optional[List[Any]]] + +#: The key under which ``rank_uniform_inputs`` records the modeling_v2 mode. +MODE_KEY = "modeling_v2" + + +def rank_uniform_inputs( + knobs: Knobs, mode: ModelingV2Mode, environ: Optional[Mapping[str, str]] = None +) -> Knobs: + """What every rank must agree on before the model is built, as (name, value) sorted by name. + + ``knobs``, the shared switches the kernels read to choose a collective or a protocol + (``SHARED_ENV``) and the modeling_v2 validation switch, where set in ``environ`` (the + process environment by default), and ``mode``. + """ + env = os.environ if environ is None else environ + shared = [(name, env[name]) for name in SHARED_ENV + (MODELING_V2_VALIDATE_ENV,) if name in env] + return tuple(sorted(list(knobs) + shared + [(MODE_KEY, mode.value)])) + + +def _world_allgather(value: Any, world_size: int) -> Optional[List[Any]]: + """``value`` from every rank of the session's MPI communicator. + + Returns None, with a warning, without MPI or when the communicator is not + the ``world_size`` ranks of this model: gathering over any other group + would either prove nothing or wait on ranks that never build this model. + """ + from tensorrt_llm._utils import mpi_comm, mpi_disabled + + if mpi_disabled(): + logger.warning( + f"{_LOG_TAG} dispatch: no MPI communicator, so the ranks are not checked for agreement" + ) + return None + comm = mpi_comm() + if comm.Get_size() != world_size: + logger.warning( + f"{_LOG_TAG} dispatch: the MPI communicator has {comm.Get_size()} ranks, not the " + f"{world_size} of this model, so the ranks are not checked for agreement" + ) + return None + return comm.allgather(value) + + +def _rank_table(gathered: Sequence[Knobs]) -> str: + knob_names = sorted({name for knobs in gathered for name, _ in knobs}) + lines = [] + for rank, knobs in enumerate(gathered): + values = dict(knobs) + knob_words = ", ".join(f"{name}={values.get(name, '')!r}" for name in knob_names) + lines.append(f" rank {rank}: knobs {knob_digest(knobs)} ({knob_words or 'none set'})") + return "\n".join(lines) + + +def check_rank_uniform( + knobs: Knobs, world_size: int, allgather: Optional[Allgather] = None +) -> None: + """Raise on every rank unless every rank read the same knob values. + + Called once, at construction, on every rank, before the knobs are parsed: + the allgather is itself a collective, so a rank must not raise before it. + ``knobs`` is ``rank_uniform_inputs`` of the knobs. ``allgather`` defaults to + the session's MPI communicator. + + Raises: + RuntimeError: with one line per rank, when the ranks disagree. + """ + gathered = (allgather or _world_allgather)(knobs, world_size) + if gathered is None or all(peer == knobs for peer in gathered): + return + raise RuntimeError( + f"{_LOG_TAG} the ranks read different knob values, so they would resolve different " + "fast paths; a collective fast path would wait forever on the ranks that skip it. " + f"Set the {KNOB_PREFIX}* variables, {', '.join(SHARED_ENV)}, " + f"{MODELING_V2_VALIDATE_ENV} and {MODE_KEY} identically on every rank (before the " + f"ranks start):\n{_rank_table(gathered)}" + ) + + +#: The fast-path state of a built model, module by module in construction order: per module, +#: its path and, per fast-path object attached to it, the attribute, the object's class and its +#: rank-independent flags. +AdoptedState = Tuple[Tuple[str, Tuple[Tuple[str, ...], ...]], ...] + + +def check_rank_uniform_adopted( + state: AdoptedState, world_size: int, allgather: Optional[Allgather] = None +) -> None: + """Raise on every rank unless every rank attached the same fast-path state to its modules. + + Called once, on every rank, after the fast paths are attached; a fast path that engaged on + some ranks only would run its collective on those ranks alone. + + Raises: + RuntimeError: naming the first module whose state differs, with one line per rank. + """ + gathered = (allgather or _world_allgather)(state, world_size) + if gathered is None or all(peer == state for peer in gathered): + return + depth = max(len(peer) for peer in gathered) + first = next( + i + for i in range(depth) + if len({peer[i] if i < len(peer) else None for peer in gathered}) > 1 + ) + lines = [] + for rank, peer in enumerate(gathered): + lines.append(f" rank {rank}: {peer[first] if first < len(peer) else ''}") + raise RuntimeError( + f"{_LOG_TAG} the ranks attached different fast-path state to their modules; a " + "collective fast path would wait forever on the ranks that skip it. First module that " + "differs:\n" + "\n".join(lines) + ) + + +# -------------------------------------------------------------------------------------------------- +# Fallback policy +# -------------------------------------------------------------------------------------------------- + + +class FastPathUnavailable(RuntimeError): + """A step inside a fast path's shapes could not take it under ``modeling_v2='require'``.""" + + +_lock = threading.Lock() +_require = [False] +_logged: set = set() +_missed: Dict[str, int] = {} + + +def set_require(require: bool) -> None: + """Whether ``missed`` raises (``modeling_v2='require'``); set by ``resolve``.""" + _require[0] = bool(require) + + +def require_mode() -> bool: + return _require[0] + + +def _log_once(level: Callable[[str], None], path: str, reason: str, message: str) -> None: + key = (path, reason) + with _lock: + if key in _logged: + return + _logged.add(key) + level(message) + + +def missed(path: str, rows: Optional[int], reason: str, *, per_call: bool = False) -> None: + """``path`` takes this step's shape but cannot run: raise under require, else log once and count. + + A miss decided per call (``per_call``) never raises: a step that is already being served + falls back, and the miss is counted and logged once. The caller falls back after it returns. + """ + at = "" if rows is None else f" at {rows} rows" + if _require[0] and not per_call: + raise FastPathUnavailable( + f"modeling_v2='require': the GLM-5.2 decode fast path {path!r} takes this step{at} " + f"but cannot run: {reason}" + ) + with _lock: + _missed[path] = _missed.get(path, 0) + 1 + _log_once( + logger.warning, + path, + reason, + f"{_LOG_TAG} {path}/missed{at}: {reason}; falling back (logged once per reason)", + ) + + +def uncovered(path: str, reason: str) -> None: + """The step is outside ``path``'s shapes: log once per (path, reason); the next tier runs.""" + _log_once(logger.info, path, reason, f"{_LOG_TAG} {path}/uncovered: {reason} (logged once)") + + +def missed_counts() -> Dict[str, int]: + """Misses per fast path in this process: per-call misses in every mode, the others under + ``auto`` (``require`` raises on them instead).""" + with _lock: + return dict(_missed) + + +def reset_fallback_state() -> None: + """Forget the logged reasons, the miss counts and the installed plan.""" + with _lock: + _logged.clear() + _missed.clear() + _INSTALLED[0] = None + _VALIDATE[0] = False + _TARGET[0] = DEFAULT_TARGET + classify_decline.cache_clear() + + +# -------------------------------------------------------------------------------------------------- +# Phases +# -------------------------------------------------------------------------------------------------- + + +class Phase(enum.Enum): + """Which target takes a model step.""" + + PREFILL = "prefill" + DECODE = "decode" + + +_PHASE: List[Optional[Phase]] = [None] + + +class _Step: + """The scope of one model step under a phase; restores the enclosing phase on exit.""" + + __slots__ = ("_phase", "_previous") + + def __init__(self, phase: Phase) -> None: + self._phase = phase + self._previous: Optional[Phase] = None + + def __enter__(self) -> None: + self._previous = _PHASE[0] + _PHASE[0] = self._phase + + def __exit__(self, *exc_info: Any) -> None: + _PHASE[0] = self._previous + + +def step(phase: Phase) -> _Step: + """A context manager that runs the enclosed model step under ``phase``; scopes nest.""" + return _Step(phase) + + +def phase() -> Optional[Phase]: + """The phase of the model step being run, or None outside a step.""" + return _PHASE[0] + + +# -------------------------------------------------------------------------------------------------- +# Decline classes +# -------------------------------------------------------------------------------------------------- + + +class Decline(enum.Enum): + """What a fast path's decline means.""" + + #: The path is turned off: neither a miss nor outside its shapes. + DISABLED = "disabled" + #: Outside the path's shapes (a layer it does not serve, a row count, a step kind). + UNCOVERED = "uncovered" + #: Decided once, at construction or in the warm-up, inside the path's layers, with a + #: precondition absent: ``missed``, which raises under ``require``. + MISSED = "missed" + #: Decided per call, with a resource absent: counted and logged once, never raised. + MISSED_PER_CALL = "missed per call" + + +_TARGET_LAYERS = frozenset(range(78)) +_DRAFT_LAYERS = frozenset({78}) +# The decoder layers that run the full indexer: the three dense layers and every fourth layer +# from layer 6. The other target layers reuse an earlier layer's top-k selection. +_FULL_INDEXER_LAYERS = frozenset((0, 1, 2) + tuple(range(6, 78, 4))) +_SHARED_INDEXER_LAYERS = _TARGET_LAYERS - _FULL_INDEXER_LAYERS + +#: Per fast path and per layer-keyed capability key family (the key without its layer index), +#: the layers on which the path engages on every rank when this target serves its checkpoint. +#: A decline on a layer outside its family's cells is outside the path's shapes. +LAYER_CELLS: Mapping[str, Mapping[str, FrozenSet[int]]] = { + "absorbed_query": {"layer": _TARGET_LAYERS}, + "draft_absorbed_query": {"layer": _DRAFT_LAYERS}, + "shared_indexer_absorbed_query": {"layer": _SHARED_INDEXER_LAYERS}, + # Only the shared-indexer layer that the last dense layer feeds. + "fused_kv_indexer_projection": {"layer": frozenset({3})}, + "indexer_layer_front": {"target_layer": _FULL_INDEXER_LAYERS, "draft_layer": _DRAFT_LAYERS}, + # Boundaries into the shared-indexer layers that a MoE layer feeds. + "deferred_norm": {"scope[shared]_into_layer": _SHARED_INDEXER_LAYERS - {3}}, + "deferred_norm_evict_hints": {"projection_layer": _SHARED_INDEXER_LAYERS - {3}}, + "sparse_mla_decode": { + "layer": _TARGET_LAYERS | _DRAFT_LAYERS, + "call:layer": _TARGET_LAYERS | _DRAFT_LAYERS, + }, + "attention_output_tail": {"layer": _TARGET_LAYERS}, + "oproj_weight_staging": {"layer": _TARGET_LAYERS}, +} + +#: Reason fragments that turn a path off rather than decline it. +DISABLED_REASONS: Tuple[str, ...] = ("DISABLE",) +#: Reason fragments that name a layer kind or a composition the path does not serve. +LAYER_KIND_REASONS: Tuple[str, ...] = ( + "target layers only", + "draft (MTP) layers only", + "full indexer layer", + "shared-indexer layer", + "layers not selected (", + "draft or unindexed layer", + "not a target layer", + "is a full-indexer layer", + "not a DSA layer", + "not MLA", + "needs the post_attention_hop hop", + "no post_attention_hop hop", + "post_attention_hop without sparse_mla_decode", +) +#: Reason fragments (any case) of a per-call decline that name a missing resource. +RESOURCE_REASONS: Tuple[str, ...] = ( + "workspace", + "compile failed", + "not compiled", + "not loaded", + "not built", + "failed", + "unavailable", + "not warmed up", + "before CUDA graph capture", + "packet does not match", + "does not forward", + "another rank", + "MNNVL", + "fabric", +) + + +def _any_of(fragments: Iterable[str], flags: int = 0) -> "re.Pattern[str]": + return re.compile("|".join(map(re.escape, fragments)), flags) + + +_DISABLED_RE = _any_of(DISABLED_REASONS) +_LAYER_KIND_RE = _any_of(LAYER_KIND_REASONS) +_RESOURCE_RE = _any_of(RESOURCE_REASONS, re.IGNORECASE) + + +@dataclass(frozen=True) +class DeclineSite: + """Capability keys of a fast path that are decided alike. + + ``key`` matches a whole key; a key that names a layer captures its index as ``layer``, and + ``cells`` names the ``LAYER_CELLS`` family of those layers. A ``per_call`` site decides on + every call; the others decide once, at construction or in the warm-up. ``uncovered`` + matches reasons that are outside the path's shapes at this site. + """ + + path: str + key: "re.Pattern[str]" + per_call: bool = False + cells: Optional[str] = None + uncovered: Optional["re.Pattern[str]"] = None + + +def _site( + path: str, + key: str, + per_call: bool = False, + cells: Optional[str] = None, + uncovered: Optional[str] = None, +) -> DeclineSite: + return DeclineSite( + path, + re.compile(key), + per_call, + cells, + None if uncovered is None else re.compile(re.escape(uncovered)), + ) + + +_LAYER = r"(?P\d+)" +_EXPERT_PREFETCH = r"expert_prefetch\[.*\]" + +#: Every capability key the fast paths log, by where and when it is decided. +DECLINE_KINDS: Tuple[DeclineSite, ...] = ( + _site("absorbed_query", "form"), + _site("absorbed_query", f"layer{_LAYER}", cells="layer"), + _site("draft_absorbed_query", f"layer{_LAYER}", cells="layer"), + _site("shared_indexer_absorbed_query", f"layer{_LAYER}", cells="layer"), + _site("fused_kv_indexer_projection", f"layer{_LAYER}", cells="layer"), + _site("indexer_layer_front", f"target_layer{_LAYER}", cells="target_layer"), + _site("indexer_layer_front", f"draft_layer{_LAYER}", cells="draft_layer"), + _site("indexer_layer_front", "rope"), + _site("indexer_layer_front", r"cp_indexer_decode_.+", per_call=True), + _site("deferred_norm", "deferred_hop"), + _site( + "deferred_norm", rf"scope\[shared\]_into_layer{_LAYER}", cells="scope[shared]_into_layer" + ), + _site("deferred_norm", r"deferred_hop_routed\[.*\]"), + _site("deferred_norm", f"projection_layer{_LAYER}", cells="projection_layer"), + _site("deferred_norm_evict_hints", "deferred_hop"), + _site("deferred_norm_evict_hints", r"deferred_hop_routed\[.*\]"), + _site("deferred_norm_evict_hints", f"projection_layer{_LAYER}", cells="projection_layer"), + _site("rope_kv_append", "rope|scatter_defer|bridge", per_call=True), + _site("indexer_topk", "indexer_decode", per_call=True), + _site("cross_rank_topk", "cp_indexer_decode", per_call=True), + _site("cross_rank_topk_cluster_merge", "cp_indexer_decode", per_call=True), + _site("cross_rank_topk_global_emit", "cp_indexer_decode", per_call=True), + _site("sparse_mla_decode", f"layer{_LAYER}", cells="layer"), + _site("sparse_mla_decode", r"request:.*", per_call=True), + _site("sparse_mla_decode", f"call:layer{_LAYER}", per_call=True, cells="call:layer"), + _site("attention_output_tail", f"layer{_LAYER}", cells="layer"), + _site("attention_output_tail_prefetch", f"layer{_LAYER}", cells="layer"), + _site("attention_output_tail_prefetch", "schedule"), + _site("attention_output_into_hop", f"layer{_LAYER}", cells="layer"), + _site("attention_output_into_hop", "into_hop_transport"), + _site("oproj_weight_staging", f"layer{_LAYER}", cells="layer"), + _site("post_attention_hop", r"post_attn_hop(:.*)?"), + _site("hop_routing", "routing_hop"), + _site("hop_routing", r"routed_moe_fork rows=.*", per_call=True), + _site("hop_routing", _EXPERT_PREFETCH), + _site("router_gemm", r"router:.*"), + _site("routed_moe", r"rows=.*", per_call=True), + _site("routed_moe", _EXPERT_PREFETCH), + _site("routed_moe_into_hop", r"rows=.*", per_call=True), + _site("routed_moe_into_hop", r"post_moe_routed_input.*"), + _site("shared_expert_prefetch", r"routed rows=.*", per_call=True), + _site("shared_expert_prefetch", _EXPERT_PREFETCH), + _site("post_moe_hop", r"cluster_kernel(_compile)?|post_moe_hop\[.*\]"), + _site("cross_layer_expert_prefetch", r"expert_prefetch|fused_predict|xc_prefetch|link\[.*\]"), + _site("cross_layer_expert_prefetch", r"spec_experts(_declined)?", per_call=True), + _site("dense_mlp", r"dense_mlp:.*", per_call=True), + _site("draft_front", "draft_front"), + _site("draft_front", r"draft_front_call(_fallback)?", per_call=True), + _site("draft_routed_moe", r"rows=.*", per_call=True), + _site("draft_live_rows", r"live_row_hop(:.*)?"), + _site("draft_live_rows", r"live_row_hop_call(:.*)?|step0:.*", per_call=True), + _site("draft_tail", r"draft_tail\[tp\d+\]"), + _site("draft_tail", r"draft_tail\[(step: .*|graph|eager)\]", per_call=True), + _site("draft_loop", r"(rows|draft_step|finalize|draft_loop):.*", per_call=True), + _site("draft_loop", "draft_embedding"), + _site("decode_prologue", r"target_(prologue|embedding)(\[.*\])?|take_embedding", per_call=True), + _site("deferred_input_prep", r"prep(\[.*\])?", per_call=True), + # Before the overlap executor loop declares its domain every call declines; the decision + # is final only once the loop has declared it. + _site( + "deferred_input_prep", + "loop_domain", + uncovered="no overlap executor loop declared its domain", + ), + _site("deferred_input_prep", "engaged"), + _site("sampler_store", "engaged"), + _site("execution_stream_loop", "engaged"), + _site("host_page_retention", "host_policy"), +) + +_SITES_BY_PATH: Mapping[str, Tuple[DeclineSite, ...]] = { + name: tuple(s for s in DECLINE_KINDS if s.path == name) for name in FAST_PATH_NAMES +} + + +@dataclass(frozen=True) +class TargetPaths: + """What one target of the family serves of the fast paths, and where their code lives. + + ``served`` is the fast paths the target runs: a path outside it is off in every plan of the + target, ``DISABLE`` may not name it and a knob only it reads is refused. ``layer_cells`` is + ``LAYER_CELLS`` for the target's layers and shapes; ``uncovered`` lists, per path, reason + fragments of declines that are outside the target's shapes at every site of the path. + ``kernel_modules`` names the module of each path whose kernel package is the target's own; + every other path's package is the family's (``KERNELS_PACKAGE``). + + Raises: + ValueError: if a name is not a fast path, or a served path extends one the target does not + serve. + """ + + served: FrozenSet[str] = frozenset(FAST_PATH_NAMES) + layer_cells: Mapping[str, Mapping[str, FrozenSet[int]]] = field( + default_factory=lambda: LAYER_CELLS + ) + uncovered: Mapping[str, Tuple[str, ...]] = field(default_factory=dict) + kernel_modules: Mapping[str, str] = field(default_factory=dict) + + def __post_init__(self) -> None: + named = set(self.served) | set(self.layer_cells) | set(self.uncovered) + unknown = sorted((named | set(self.kernel_modules)) - set(FAST_PATH_NAMES)) + if unknown: + raise ValueError(f"{unknown} are not fast paths of the family") + lacking = sorted( + f"{name} extends {base}" + for name in self.served + for base in _BY_NAME[name].depends_on + if base not in self.served + ) + if lacking: + raise ValueError(f"a served fast path extends one the target does not serve: {lacking}") + + +#: Every fast path, with the family's tables. +DEFAULT_TARGET = TargetPaths() +# The tables of the installed plan's target. +_TARGET: List[TargetPaths] = [DEFAULT_TARGET] + + +def _decline_site(path: str, key: str) -> Optional[Tuple[DeclineSite, "re.Match[str]"]]: + """The site of ``path`` whose key pattern matches all of ``key``, with the match.""" + for site in _SITES_BY_PATH.get(path, ()): + match = site.key.fullmatch(key) + if match is not None: + return site, match + return None + + +@functools.lru_cache(maxsize=4096) +def classify_decline(path: str, key: str, reason: str) -> Decline: + """What a decline of fast path ``path`` at capability ``key`` for ``reason`` means. + + In order: a reason that turns the path off is ``DISABLED``; a key no site of the path + matches is ``UNCOVERED``; a per-call decline is ``MISSED_PER_CALL`` when its reason names a + missing resource and ``UNCOVERED`` otherwise; a decline decided once is ``UNCOVERED`` when + its reason names a layer kind or a composition the path does not serve, or its layer is + outside the path's cells, and ``MISSED`` otherwise. The cells and the target's uncovered + reasons are the installed plan's target's (``TargetPaths``). Memoized: a repeated decline + costs one lookup. + """ + if _DISABLED_RE.search(reason): + return Decline.DISABLED + found = _decline_site(path, key) + if found is None: + return Decline.UNCOVERED + site, match = found + if site.uncovered is not None and site.uncovered.search(reason): + return Decline.UNCOVERED + if any(fragment in reason for fragment in _TARGET[0].uncovered.get(path, ())): + return Decline.UNCOVERED + if site.per_call: + return Decline.MISSED_PER_CALL if _RESOURCE_RE.search(reason) else Decline.UNCOVERED + if _LAYER_KIND_RE.search(reason): + return Decline.UNCOVERED + if site.cells is not None: + cells = _TARGET[0].layer_cells.get(path, {}).get(site.cells, frozenset()) + if int(match["layer"]) not in cells: + return Decline.UNCOVERED + return Decline.MISSED + + +def _record_decline(path: str, key: str, reason: str) -> None: + kind = classify_decline(path, key, reason) + if kind is Decline.MISSED: + missed(path, None, f"{key}: {reason}" if reason else key) + elif kind is Decline.MISSED_PER_CALL: + missed(path, None, reason or key, per_call=True) + + +# -------------------------------------------------------------------------------------------------- +# Selection +# -------------------------------------------------------------------------------------------------- + +#: The parts of the decode step's input preparation, each a fast path of its own: the step-input +#: stager, the sampler's store and the overlap loop on the execution stream. +PART_PATHS: Mapping[str, str] = { + "prep": "deferred_input_prep", + "store": "sampler_store", + "stream": "execution_stream_loop", +} +_PART_ONLY_PATHS = frozenset(PART_PATHS.values()) - {PART_PATHS["prep"]} + +#: Every fast path except the parts that only ``deferred_input_prep`` brings along. +SELECTOR_PATHS: Tuple[str, ...] = tuple(n for n in FAST_PATH_NAMES if n not in _PART_ONLY_PATHS) + +#: The family's kernel packages, one directory per fast path under ``kernels/``. +KERNELS_PACKAGE = f"{__package__}.kernels" +#: The fast paths whose code lives in the kernel directory of the path they extend. +KERNEL_DIRECTORY: Mapping[str, str] = { + "draft_absorbed_query": "absorbed_query", + "shared_indexer_absorbed_query": "absorbed_query", + "cross_rank_topk": "indexer_topk", + "attention_output_tail_prefetch": "attention_output_tail", + "attention_output_into_hop": "attention_output_tail", + "routed_moe_into_hop": "routed_moe", + "sampler_store": "deferred_input_prep", + "execution_stream_loop": "deferred_input_prep", +} + + +def _bases() -> Dict[str, FrozenSet[str]]: + """Every path each path extends, directly or transitively.""" + closure: Dict[str, FrozenSet[str]] = {} + + def visit(name: str) -> FrozenSet[str]: + if name not in closure: + bases = set() + for base in _BY_NAME[name].depends_on: + bases |= {base} | visit(base) + closure[name] = frozenset(bases) + return closure[name] + + for name in FAST_PATH_NAMES: + visit(name) + return closure + + +#: The paths each path has no effect without (the transitive closure of ``depends_on``). +REQUIRED_FEATURES: Mapping[str, FrozenSet[str]] = { + name: bases for name, bases in _bases().items() if bases +} + +# Numerical variants (a variant has its own name). Per fast path with variants: the knob that +# selects one (None: a single variant) and the variant names, default first. A path without an +# entry has one numerical form, equal to the generic path's or held to a frozen error budget. +NUMERICAL_VARIANTS: Dict[str, Tuple[Optional[str], Tuple[str, ...]]] = { + # Exact top-k over each page owner's indexer K, identical on every rank. + "cross_rank_topk": (None, ("rank-consistent",)), + # The cluster merge's rows are byte-identical to the cross-rank top-k's (same set and order). + "cross_rank_topk_cluster_merge": (None, ("rank-consistent",)), + # Its id rows are the cluster merge's; its pool and copy rows are the values of the + # rope/KV-append top-k bridge. + "cross_rank_topk_global_emit": (None, ("rank-consistent",)), + # What the routed experts write into the post-MoE hop: fp32 sums the top-k contributions in + # another order (nondeterministic from run to run); identity is bit-exact. + "routed_moe_into_hop": (ACCUMULATION_ENV, ("fp32", "identity")), + # The next layer's RMSNorm deferred into its projection (changes rounding), only on boundaries + # into shared-indexer layers: full-indexer layers keep the post-MoE hop's normalization. + "deferred_norm": (None, ("shared",)), + # One projection launch for the fused-A GEMM and the indexer key/weights GEMM: kva keeps the + # fused-A order (bit-identical); kw (full-indexer and draft layers) takes the same mma.sync + # order instead of the cuBLAS split-K GEMM (rounds differently). This target runs it on the + # dense-fed shared-indexer layer, which computes kva only. + "fused_kv_indexer_projection": (None, ("kw-fused-a-order",)), +} + +#: Prefixes of the set variables the record line lists (``side_selectors``). +SIDE_SELECTOR_PREFIXES = (KNOB_PREFIX,) +#: Shared TensorRT-LLM switches that the kernels also read. +SHARED_ENV = ("TLLM_TEST_MNNVL", "TRTLLM_ENABLE_PDL", "TRTLLM_FORCE_MNNVL_AR") + + +def feature_variants( + paths: Iterable[str], environ: Optional[Mapping[str, str]] = None +) -> Dict[str, str]: + """The numerical variant of every path in ``paths`` that has some. + + Raises: + ValueError: if a knob names an unknown variant of a path in ``paths``. + """ + env = os.environ if environ is None else environ + variants = {} + for path in sorted(paths): + if path not in NUMERICAL_VARIANTS: + continue + name, choices = NUMERICAL_VARIANTS[path] + raw = env.get(name) if name else None + value = (raw or "").strip().lower() or choices[0] + if value not in choices: + raise ValueError( + f"{name}={raw!r} is not a {path} variant; known variants are {list(choices)} " + f"(unset selects {choices[0]!r})" + ) + variants[path] = value + return variants + + +def side_selectors(environ: Optional[Mapping[str, str]] = None) -> Dict[str, str]: + """Every set knob of the family.""" + env = os.environ if environ is None else environ + return {name: env[name] for name in sorted(env) if name.startswith(SIDE_SELECTOR_PREFIXES)} + + +def shared_env(environ: Optional[Mapping[str, str]] = None) -> Dict[str, str]: + """The set shared TensorRT-LLM switches that the kernels read.""" + env = os.environ if environ is None else environ + return {name: env[name] for name in SHARED_ENV if name in env} + + +def feature_record(paths: Iterable[str], environ: Optional[Mapping[str, str]] = None) -> str: + """The one-line record of a selection. + + ``paths=`` lists every path as ``path`` or ``path[variant]``; ``side_selectors=`` and + ``shared_env=`` are JSON objects of the set variables. + + Raises: + ValueError: as ``feature_variants``. + """ + variants = feature_variants(paths, environ) + listed = ",".join(f"{p}[{variants[p]}]" if p in variants else p for p in sorted(paths)) + + def _json(values: Dict[str, str]) -> str: + return json.dumps(values, sort_keys=True, separators=(",", ":")) + + return ( + f"paths={listed or ''} " + f"side_selectors={_json(side_selectors(environ))} " + f"shared_env={_json(shared_env(environ))}" + ) + + +def _log_info(message: str) -> None: + logger.info(message) + + +# The fast paths of the installed plan; None while no plan is installed. +_INSTALLED: List[Optional[FrozenSet[str]]] = [None] +# Whether the installed plan runs the fast paths' diagnostics. +_VALIDATE: List[bool] = [False] + + +def install(plan: Plan, environ: Optional[Mapping[str, str]] = None) -> None: + """Answer ``is_enabled`` from ``plan`` from now on, and log its record line once. + + Raises: + ValueError: as ``feature_variants``, for a knob that names an unknown variant of an + enabled path. + """ + paths = frozenset(n for n in FAST_PATH_NAMES if plan.enabled(n)) + record = feature_record(paths, environ) + _INSTALLED[0] = paths + _VALIDATE[0] = plan.validate + _TARGET[0] = DEFAULT_TARGET if plan.target is None else plan.target + classify_decline.cache_clear() + _log_info(f"{_LOG_TAG} dispatch: record {record}") + + +def installed() -> Optional[FrozenSet[str]]: + """The fast paths of the installed plan, or None.""" + return _INSTALLED[0] + + +def enabled_features() -> FrozenSet[str]: + """The fast paths that run in this process: the installed plan's, else none.""" + plan_paths = _INSTALLED[0] + return plan_paths if plan_paths is not None else frozenset() + + +def validate_mode() -> bool: + """Whether the installed plan runs the fast paths' diagnostics (False without a plan).""" + return _VALIDATE[0] + + +def is_enabled(path: str) -> bool: + """Whether fast path ``path`` runs in this process. + + Raises: + ValueError: if ``path`` is not a fast path of the family. + """ + if path not in _BY_NAME: + raise ValueError(f"{path!r} is not a fast path of this target") + return path in enabled_features() + + +# Entry points per fast path. The kernel package of a path registers them when it is imported; +# a call site looks one up with ``get_impl`` only after ``is_enabled`` returned True. +_REGISTRY: Dict[str, Dict[str, Callable]] = {} + +# Per-rank capability decisions, logged once per (path, key). +_CAPABILITY_LOGGED: Dict[Tuple[str, str], bool] = {} + + +def register(path: str, name: str, fn: Callable) -> Callable: + """Register ``fn`` as entry point ``name`` of fast path ``path``. + + Raises: + ValueError: if ``path`` is not a fast path of the family. + """ + if path not in _BY_NAME: + raise ValueError(f"{path!r} is not a fast path of this target") + _REGISTRY.setdefault(path, {})[name] = fn + return fn + + +def get_impl(path: str, name: str) -> Callable: + """Import the kernel package of ``path`` on first use and return its entry ``name``. + + The package is the installed plan's target's own (``TargetPaths.kernel_modules``) or the + family's. + + Raises: + KeyError: if the package does not register ``name`` for ``path``. + """ + if path not in _REGISTRY: + module = _TARGET[0].kernel_modules.get(path) + importlib.import_module(module or f"{KERNELS_PACKAGE}.{KERNEL_DIRECTORY.get(path, path)}") + entries = _REGISTRY.get(path, {}) + if name not in entries: + raise KeyError( + f"fast path {path!r} does not register {name!r}; registered: {sorted(entries)}" + ) + return entries[name] + + +def log_capability(path: str, key: str, eligible: bool, reason: str = "") -> bool: + """Log a per-rank capability decision once per (path, key) and return ``eligible``. + + Every decline, logged or not, is also classified (``classify_decline``): a miss decided + once goes to ``missed``, which raises under ``require``; a miss decided per call is counted + and logged once; a decline outside the path's shapes or of a path turned off adds nothing + to the capability line. + """ + if (path, key) not in _CAPABILITY_LOGGED: + _CAPABILITY_LOGGED[(path, key)] = eligible + state = "engaged" if eligible else "native fallback" + suffix = f" ({reason})" if reason else "" + _log_info(f"{_LOG_TAG} {path}/{key}: {state}{suffix}") + if not eligible: + _record_decline(path, key, reason) + return eligible diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/__init__.py new file mode 100644 index 000000000000..61038c261bb1 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Kernels two or more glm_moe_dsa targets call, one package per fast path.""" diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/__init__.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/__init__.py new file mode 100644 index 000000000000..11a8ee3db664 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/__init__.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""absorbed_query: target query/projection region (the query_front contract, token-wise part of the +index_selection contract). + +Registered entry points (``dispatch.get_impl("absorbed_query", name)``): + +* ``dsa_proj(mla, position_ids, hidden_states, attn_metadata)`` -> + ``Optional[AbsorbedQueryPacket]`` for target layers (``absorbed_query``); ``None`` + means "run the native path". +* ``mtp_dsa_proj(mla, position_ids, hidden_states, attn_metadata)`` -> the + same packet for the one-model MTP draft layer at every draft step + (package ``draft_absorbed_query``, which extends absorbed_query to the draft layer); ``None`` for + target layers and outside the support domain. +* ``shared_dsa_proj(mla, position_ids, hidden_states, attn_metadata)`` -> + the preabsorbed packet of a shared-indexer target layer from one kernel + (package ``shared_indexer_absorbed_query``, which extends absorbed_query's shared-layer path); + ``None`` for full-indexer and draft layers and outside the support domain. +""" + +from ...dispatch import register +from .query_proj import AbsorbedQueryPacket, AbsorbedQueryProjection, dsa_proj, mtp_dsa_proj +from .shared_indexer import SharedIndexerProjection, shared_dsa_proj + +register("absorbed_query", "dsa_proj", dsa_proj) +register("absorbed_query", "mtp_dsa_proj", mtp_dsa_proj) +register("absorbed_query", "shared_dsa_proj", shared_dsa_proj) + +__all__ = [ + "AbsorbedQueryPacket", + "AbsorbedQueryProjection", + "SharedIndexerProjection", + "dsa_proj", + "mtp_dsa_proj", + "shared_dsa_proj", +] diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/kernels.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/kernels.py new file mode 100644 index 000000000000..be6c61752cd6 --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/kernels.py @@ -0,0 +1,1764 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Triton kernels of the absorbed_query/projection package. + +``qproj_kernel`` consumes the native fused ``kv_a_proj_with_mqa`` output +``kva = [q_a | compressed_kv | k_pe]`` of ``R <= BLOCK_M`` token rows (and, +with ``PRENORM``, the native ``q_a_layernorm`` output) and produces, in one +launch: + +* ``q = q_b_proj(rmsnorm(q_a))`` ``[R, H * 256]`` (BF16, the native ``q``), +* optionally ``qr = rmsnorm(q_a)`` ``[R, 2048]`` for the native indexer, +* ``latent = [rmsnorm(compressed_kv) | k_pe]`` ``[R, 576]``, +* optionally the absorbed query ``fused_q[:, h, :512] = q_nope[:, h] @ W_UK[h]`` + written into the caller's ``[R, H, 576]`` buffer. + +``qproj_indexer_kernel`` additionally produces the token-wise indexer +outputs of full indexer layers. + +Numerics follow the native chain bit for bit on the default path +(``PRENORM``, the native kv_a GEMM, ``q_a_layernorm``, fused ``wk`` / +``weights_proj`` GEMM and ``k_norm`` as bridges): ``q`` and the indexer +``wq_b`` output are FP32-accumulated MMA chains in ascending K order (equal to +the native GEMMs), the ``compressed_kv`` RMSNorm reproduces the native +FlashInfer kernel's reduction order (``_fi_rmsnorm_rows_exact``), the +indexer RoPE its FMA pattern (``_rope_pairs``) and the FP8 tail the native +``fusedCatFp8Kernel``; ``q`` is rounded to BF16 before the absorption (native +intermediate). The experimental paths (own hidden GEMV, prefetching kernel's +in-kernel q_a norm, ``PRENORM=False``) are not bit-exact. + +Program roles, in program-id order (each kernel documents its own list): +``q_b`` producer tiles ``[0, N_QB)`` (each ``BN`` output columns over the full +``K``; ``TPH = HEAD_DIM // BN`` tiles per head, the first ``NOPE // BN`` of a +head are its no-RoPE tiles), then indexer ``wq_b`` producer tiles, then the +programs that only consume. + +Inter-program synchronization contract +-------------------------------------- +* A producer stores its tile, executes a CTA barrier and then one thread + releases the tile with ``atom.add.release.gpu`` on a per-head counter. A + consumer spins with ``atom.add.acquire.gpu`` (adding 0) until the counter + reaches the number of producer tiles it needs, executes a CTA barrier and + only then reads the tiles, with ``ld.global.cg`` (L2, never a stale L1 line). +* Every counter is per module (per layer) and is zero between launches. The + last consumer of a counter resets it (relaxed exchange) after every + consumer of that counter has read its tiles: for the absorption counters a + second ``done`` counter (``acq_rel``) counts the consumers of a head; an + indexer-head counter has a single consumer. Counters are touched only after + ``griddepcontrol.wait``, so under PDL a launch never observes the counters + of an earlier launch that has not completed. +* Forward progress: every spin-wait waits only on counters released by + programs with a *lower* program id. Absorption job ``j`` (head + ``j // TPH``, ``BC`` latent columns) is run, with ``ABSORB_IN_PRODUCER`` + and ``PRODUCER_JOB_SHIFT == TPH``, by producer ``j + TPH`` (a producer of + the next head) after it has published its own tile, and the last head's + jobs by ``TPH`` extra consumer programs placed after all producers. So the + lowest-numbered unfinished program never waits, and a launch completes + with a single resident program slot when programs are dispatched in id + order, and on any dispatch order when the whole grid is resident at once + (the default pipelined grids, <= 234 programs of <= 96 KB shared memory, + fit on an idle sm_103 GPU; the experimental prefetching kernel's do not). + ``PRODUCER_JOB_SHIFT == 0`` is the legacy layout (producer ``p`` runs job + ``p`` of its own head group), which needs ``NOPE // BN`` co-resident + programs of one head group; it is kept only as a stress-test reference. +* ``DELAY_NS > 0`` (tests only) delays a deterministic subset of producers + before they publish and of consumers before they finish, by up to + ``DELAY_NS`` nanoseconds of ``%globaltimer``, to stress the protocol. +""" + +import triton +import triton.language as tl + + +@triton.jit +def _rms_rcp( + src_ptr, stride_src, rows, rmask, N: tl.constexpr, BLOCK_M: tl.constexpr, BK: tl.constexpr, eps +): + cols = tl.arange(0, BK) + ss = tl.zeros([BLOCK_M], dtype=tl.float32) + for k0 in tl.range(0, N, BK, num_stages=N // BK): + x = tl.load( + src_ptr + rows[:, None] * stride_src + (k0 + cols)[None, :], + mask=rmask[:, None], + other=0.0, + ).to(tl.float32) + ss += tl.sum(x * x, axis=1) + return tl.rsqrt(ss / N + eps) + + +@triton.jit +def _rms_rcp_partials( + ss_ptr, + group: tl.constexpr, + n_parts, + N: tl.constexpr, + BLOCK_M: tl.constexpr, + MAX_PARTS: tl.constexpr, + eps, +): + parts = tl.arange(0, MAX_PARTS) + rows = tl.arange(0, BLOCK_M) + ss = tl.load( + ss_ptr + parts[:, None] * 2 * BLOCK_M + group * BLOCK_M + rows[None, :], + mask=(parts < n_parts)[:, None], + other=0.0, + ) + return tl.rsqrt(tl.sum(ss, axis=0) / N + eps) + + +# -------------------------------------------------------------------------- +# Bit-exact reproduction of the native FlashInfer CuTe-DSL RMSNorm (the +# ``kv_a_layernorm`` / ``q_a_layernorm`` kernels, H in {512, 2048}): one warp +# per row, lane t owns columns 256 * b + 8 * t + v (b < H / 256, v < 8), a +# sequential per-lane fold of the squares in column order (blocks outer, +# columns inner), a butterfly over the 32 lanes (xor offsets 1, 2, 4, 8, 16), +# rstd = rsqrt(sum / H + eps), y = bf16((x * rstd) * (w + 0)). Every add is +# explicit (reshape/split, sums over axes of size 2), so the result does not +# depend on the compiled layout or on the rows per program. The same +# arithmetic as ``shared_indexer._fi_rmsnorm_rows_vec`` (shared_indexer_absorbed_query), checked bit +# for bit against the native kernel on real and wide-range rows. +# -------------------------------------------------------------------------- + + +@triton.jit +def _fi_butterfly32(s, ROWS: tl.constexpr): + """Sum of ``s [ROWS, 32]`` over the lanes in shuffle-xor butterfly order + (offsets 1, 2, 4, 8, 16): a balanced pairwise tree in lane order.""" + s = tl.sum(tl.reshape(s, [ROWS, 16, 2]), axis=2) + s = tl.sum(tl.reshape(s, [ROWS, 8, 2]), axis=2) + s = tl.sum(tl.reshape(s, [ROWS, 4, 2]), axis=2) + s = tl.sum(tl.reshape(s, [ROWS, 2, 2]), axis=2) + return tl.sum(s, axis=1) + + +@triton.jit +def _v8_leaves(x, ROWS: tl.constexpr): + """The 8 columns ``v`` of ``x [ROWS, 32, 8]`` as ``[ROWS, 32]`` tensors, + in ``v`` order (register-level reshape/split, no data movement).""" + e, o = tl.split(tl.reshape(x, [ROWS, 32, 4, 2])) + ee, eo = tl.split(tl.reshape(e, [ROWS, 32, 2, 2])) + oe, oo = tl.split(tl.reshape(o, [ROWS, 32, 2, 2])) + v0, v4 = tl.split(ee) + v2, v6 = tl.split(eo) + v1, v5 = tl.split(oe) + v3, v7 = tl.split(oo) + return v0, v1, v2, v3, v4, v5, v6, v7 + + +@triton.jit +def _fi_rmsnorm_rows_exact( + x_ptr, stride_x, w_ptr, y_ptr, stride_y, rows, rmask, eps, H: tl.constexpr, ROWS: tl.constexpr +): + """``y[rows, :H] = bf16(rmsnorm(x[rows, :H]) * w)`` bit for bit as the + native FlashInfer RMSNorm (see above).""" + lanes = tl.arange(0, 32) + vv = tl.arange(0, 8) + offs = rows[:, None, None] * stride_x + 8 * lanes[None, :, None] + vv[None, None, :] + s = tl.zeros([ROWS, 32], dtype=tl.float32) + for b in tl.static_range(H // 256): + x = tl.load(x_ptr + offs + 256 * b, mask=rmask[:, None, None], other=0.0).to(tl.float32) + v0, v1, v2, v3, v4, v5, v6, v7 = _v8_leaves(x, ROWS) + s = s + v0 * v0 + s = s + v1 * v1 + s = s + v2 * v2 + s = s + v3 * v3 + s = s + v4 * v4 + s = s + v5 * v5 + s = s + v6 * v6 + s = s + v7 * v7 + rstd = tl.rsqrt(_fi_butterfly32(s, ROWS) / H + eps) + cols = tl.arange(0, H) + x = tl.load( + x_ptr + rows[:, None] * stride_x + cols[None, :], mask=rmask[:, None], other=0.0 + ).to(tl.float32) + w = tl.load(w_ptr + cols).to(tl.float32) + 0.0 + y = (x * rstd[:, None]) * w[None, :] + tl.store( + y_ptr + rows[:, None] * stride_y + cols[None, :], y.to(tl.bfloat16), mask=rmask[:, None] + ) + + +@triton.jit +def _stress_delay(pid, salt: tl.constexpr, DELAY_NS: tl.constexpr): + """Test-only skew: spin ``(h / 10) * DELAY_NS`` ns, ``h = (37 pid + salt) + mod 11`` (so some programs do not wait and others wait up to DELAY_NS).""" + if DELAY_NS > 0: + wait_ns = ((pid * 37 + salt) % 11) * (DELAY_NS // 10) + t0 = tl.extra.cuda.globaltimer() + while tl.extra.cuda.globaltimer() - t0 < wait_ns: + pass + + +@triton.jit +def _ld_wuk(a, valid, wuk_ptr, KV_LORA: tl.constexpr, NOPE: tl.constexpr, BC: tl.constexpr): + """``W_UK[h, c0:c0+BC, :NOPE]`` of absorption job ``a`` as [NOPE, BC] + (split 128 + NOPE-128 along NOPE); nothing is read unless ``valid``.""" + chunks_per_head: tl.constexpr = KV_LORA // BC + h = a // chunks_per_head + c0 = (a % chunks_per_head) * BC + cc = tl.arange(0, BC) + w_base = wuk_ptr + h * (KV_LORA * NOPE) + (c0 + cc)[None, :] * NOPE + w_lo = tl.load(w_base + tl.arange(0, 128)[:, None], mask=valid, other=0.0) + w_hi = tl.load(w_base + 128 + tl.arange(0, NOPE - 128)[:, None], mask=valid, other=0.0) + return w_lo, w_hi + + +@triton.jit +def _qb_program( + pid, + kva_ptr, + stride_kva, + gq_ptr, + wqb_ptr, + q_ptr, + qr_ptr, + sync_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps, + Q_LORA: tl.constexpr, + HEAD_DIM: tl.constexpr, + NOPE: tl.constexpr, + H: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, + WRITE_QR: tl.constexpr, + DO_ABSORB: tl.constexpr, + SS_PARTIALS: tl.constexpr, + MAX_PARTS: tl.constexpr, + PRENORM: tl.constexpr, + DELAY_NS: tl.constexpr = 0, +): + if PRENORM: + # kva_ptr/stride_kva address the already normalized BF16 q_a rows. + _prenorm_gemm( + pid, + kva_ptr, + stride_kva, + wqb_ptr, + q_ptr, + H * HEAD_DIM, + rows, + rmask, + Q_LORA, + BLOCK_M, + BN, + BK, + ) + else: + _norm_gemm( + pid, + kva_ptr, + stride_kva, + gq_ptr, + wqb_ptr, + q_ptr, + qr_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps, + Q_LORA, + HEAD_DIM, + H, + BLOCK_M, + BN, + BK, + WRITE_QR, + SS_PARTIALS, + MAX_PARTS, + ) + if DO_ABSORB: + if (pid % (HEAD_DIM // BN)) < (NOPE // BN): + _stress_delay(pid, 1, DELAY_NS) + # Publish this no-RoPE tile of head pid // (HEAD_DIM // BN): every + # thread's store is ordered before the release by the barrier. + tl.debug_barrier() + tl.atomic_add(sync_ptr + pid // (HEAD_DIM // BN), 1, sem="release", scope="gpu") + + +@triton.jit +def _prenorm_gemm( + t, + x_ptr, + stride_x, + w_ptr, + out_ptr, + OUT_COLS: tl.constexpr, + rows, + rmask, + K: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, +): + """``out[:, t*BN:(t+1)*BN] = x @ W[t*BN:(t+1)*BN]^T`` for BF16 ``x``.""" + cols = tl.arange(0, BK) + n_offs = t * BN + tl.arange(0, BN) + acc = tl.zeros([BLOCK_M, BN], dtype=tl.float32) + for k0 in tl.range(0, K, BK): + x = tl.load( + x_ptr + rows[:, None] * stride_x + (k0 + cols)[None, :], mask=rmask[:, None], other=0.0 + ) + w = tl.load(w_ptr + n_offs[None, :] * K + (k0 + cols)[:, None]) + acc = tl.dot(x, w, acc) + tl.store( + out_ptr + rows[:, None] * OUT_COLS + n_offs[None, :], + acc.to(tl.bfloat16), + mask=rmask[:, None], + ) + + +@triton.jit +def _norm_gemm( + pid, + kva_ptr, + stride_kva, + gq_ptr, + wqb_ptr, + q_ptr, + qr_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps, + Q_LORA: tl.constexpr, + HEAD_DIM: tl.constexpr, + H: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, + WRITE_QR: tl.constexpr, + SS_PARTIALS: tl.constexpr, + MAX_PARTS: tl.constexpr, +): + if SS_PARTIALS: + rcp = _rms_rcp_partials(ss_ptr, 0, n_parts, Q_LORA, BLOCK_M, MAX_PARTS, eps) + else: + rcp = _rms_rcp(kva_ptr, stride_kva, rows, rmask, Q_LORA, BLOCK_M, BK, eps) + cols = tl.arange(0, BK) + n_offs = pid * BN + tl.arange(0, BN) + acc = tl.zeros([BLOCK_M, BN], dtype=tl.float32) + for k0 in tl.range(0, Q_LORA, BK): + x = tl.load( + kva_ptr + rows[:, None] * stride_kva + (k0 + cols)[None, :], + mask=rmask[:, None], + other=0.0, + ).to(tl.float32) + g = tl.load(gq_ptr + k0 + cols).to(tl.float32) + xn = (x * rcp[:, None] * g[None, :]).to(tl.bfloat16) + if WRITE_QR: + if pid == 0: + tl.store( + qr_ptr + rows[:, None] * Q_LORA + (k0 + cols)[None, :], xn, mask=rmask[:, None] + ) + w = tl.load(wqb_ptr + n_offs[None, :] * Q_LORA + (k0 + cols)[:, None]) + acc = tl.dot(xn, w, acc) + tl.store( + q_ptr + rows[:, None] * (H * HEAD_DIM) + n_offs[None, :], + acc.to(tl.bfloat16), + mask=rmask[:, None], + ) + + +@triton.jit +def _absorb_program( + a, + wuk_ptr, + q_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + rows, + rmask, + KV_LORA: tl.constexpr, + ROPE: tl.constexpr, + HEAD_DIM: tl.constexpr, + NOPE: tl.constexpr, + H: tl.constexpr, + BN: tl.constexpr, + BC: tl.constexpr, + USE_PDL: tl.constexpr, + DELAY_NS: tl.constexpr = 0, +): + chunks_per_head: tl.constexpr = KV_LORA // BC + h = a // chunks_per_head + c0 = (a % chunks_per_head) * BC + cc = tl.arange(0, BC) + d_lo = tl.arange(0, 128) + d_hi = tl.arange(0, NOPE - 128) + # W_UK[h] is k_b_proj_trans[h] = [KV_LORA, NOPE]; load the [NOPE, BC] + # slice for this program before waiting so it overlaps the q_b GEMM. + w_base = wuk_ptr + h * (KV_LORA * NOPE) + (c0 + cc)[None, :] * NOPE + w_lo = tl.load(w_base + d_lo[:, None]) + w_hi = tl.load(w_base + 128 + d_hi[:, None]) + # Weights are never written by an earlier grid, so only the counter and + # buffer accesses below have to wait for the programmatic dependency. + if USE_PDL: + tl.extra.cuda.gdc_wait() + + ready = tl.atomic_add(sync_ptr + h, 0, sem="acquire", scope="gpu") + while ready < NOPE // BN: + ready = tl.atomic_add(sync_ptr + h, 0, sem="acquire", scope="gpu") + tl.debug_barrier() + + q_base = q_ptr + rows[:, None] * (H * HEAD_DIM) + h * HEAD_DIM + q_lo = tl.load(q_base + d_lo[None, :], mask=rmask[:, None], other=0.0, cache_modifier=".cg") + q_hi = tl.load( + q_base + 128 + d_hi[None, :], mask=rmask[:, None], other=0.0, cache_modifier=".cg" + ) + acc = tl.dot(q_lo, w_lo) + acc = tl.dot(q_hi, w_hi, acc) + tl.store( + fq_ptr + rows[:, None] * stride_fq_row + h * (KV_LORA + ROPE) + (c0 + cc)[None, :], + acc.to(tl.bfloat16), + mask=rmask[:, None], + ) + + # Every program of head h has read the q tiles once this count reaches + # chunks_per_head; the last one resets both counters for the next launch. + _stress_delay(a, 3, DELAY_NS) + tl.debug_barrier() + done = tl.atomic_add(sync_ptr + H + h, 1, sem="acq_rel", scope="gpu") + if done == chunks_per_head - 1: + tl.atomic_xchg(sync_ptr + h, 0, sem="relaxed", scope="gpu") + tl.atomic_xchg(sync_ptr + H + h, 0, sem="relaxed", scope="gpu") + + +@triton.jit +def _kv_program( + kva_ptr, + stride_kva, + gkv_ptr, + latent_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps, + Q_LORA: tl.constexpr, + KV_LORA: tl.constexpr, + ROPE: tl.constexpr, + BLOCK_M: tl.constexpr, + SS_PARTIALS: tl.constexpr, + MAX_PARTS: tl.constexpr, +): + if SS_PARTIALS: + # Experimental own hidden GEMV only (not bit-exact: its partial sums + # follow the GEMV's column split). + kc = tl.arange(0, KV_LORA) + x = tl.load( + kva_ptr + rows[:, None] * stride_kva + Q_LORA + kc[None, :], + mask=rmask[:, None], + other=0.0, + ).to(tl.float32) + rcp = _rms_rcp_partials(ss_ptr, 1, n_parts, KV_LORA, BLOCK_M, MAX_PARTS, eps) + g = tl.load(gkv_ptr + kc).to(tl.float32) + y = (x * rcp[:, None] * g[None, :]).to(tl.bfloat16) + tl.store( + latent_ptr + rows[:, None] * (KV_LORA + ROPE) + kc[None, :], y, mask=rmask[:, None] + ) + else: + # The native kv_a_layernorm, bit for bit. + _fi_rmsnorm_rows_exact( + kva_ptr + Q_LORA, + stride_kva, + gkv_ptr, + latent_ptr, + KV_LORA + ROPE, + rows, + rmask, + eps, + KV_LORA, + BLOCK_M, + ) + rc = tl.arange(0, ROPE) + k_pe = tl.load( + kva_ptr + rows[:, None] * stride_kva + Q_LORA + KV_LORA + rc[None, :], + mask=rmask[:, None], + other=0.0, + ) + tl.store( + latent_ptr + rows[:, None] * (KV_LORA + ROPE) + KV_LORA + rc[None, :], + k_pe, + mask=rmask[:, None], + ) + + +@triton.jit +def qproj_kernel( + kva_ptr, + stride_kva, + gq_ptr, + gkv_ptr, + wqb_ptr, + wuk_ptr, + q_ptr, + qr_ptr, + latent_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + ss_ptr, + n_parts, + R, + eps_q, + eps_kv, + Q_LORA: tl.constexpr, + KV_LORA: tl.constexpr, + ROPE: tl.constexpr, + HEAD_DIM: tl.constexpr, + NOPE: tl.constexpr, + H: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, + BC: tl.constexpr, + N_QB: tl.constexpr, + N_ABS: tl.constexpr, + WRITE_QR: tl.constexpr, + DO_ABSORB: tl.constexpr, + SS_PARTIALS: tl.constexpr, + MAX_PARTS: tl.constexpr, + USE_PDL: tl.constexpr, + PRENORM: tl.constexpr = False, + qn_ptr=None, + stride_qn=0, + ABSORB_IN_PRODUCER: tl.constexpr = False, + PRODUCER_JOB_SHIFT: tl.constexpr = 0, + ABS_JOB0: tl.constexpr = 0, + DELAY_NS: tl.constexpr = 0, +): + """Programs: ``[0, N_QB)`` q_b producers (with ``ABSORB_IN_PRODUCER``, + producer ``p >= PRODUCER_JOB_SHIFT`` then runs absorption job + ``p - PRODUCER_JOB_SHIFT``); ``[N_QB, N_QB + N_ABS)`` absorption jobs + ``ABS_JOB0 + i``; the last program normalizes ``compressed_kv``.""" + pid = tl.program_id(0) + rows = tl.arange(0, BLOCK_M) + rmask = rows < R + if pid < N_QB: + if ABSORB_IN_PRODUCER: + job = tl.maximum(pid - PRODUCER_JOB_SHIFT, 0) + w_lo, w_hi = _ld_wuk(job, pid >= PRODUCER_JOB_SHIFT, wuk_ptr, KV_LORA, NOPE, BC) + if USE_PDL: + tl.extra.cuda.gdc_wait() + if PRENORM: + _qb_program( + pid, + qn_ptr, + stride_qn, + gq_ptr, + wqb_ptr, + q_ptr, + qr_ptr, + sync_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps_q, + Q_LORA, + HEAD_DIM, + NOPE, + H, + BLOCK_M, + BN, + BK, + False, + DO_ABSORB, + False, + MAX_PARTS, + True, + DELAY_NS, + ) + else: + _qb_program( + pid, + kva_ptr, + stride_kva, + gq_ptr, + wqb_ptr, + q_ptr, + qr_ptr, + sync_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps_q, + Q_LORA, + HEAD_DIM, + NOPE, + H, + BLOCK_M, + BN, + BK, + WRITE_QR, + DO_ABSORB, + SS_PARTIALS, + MAX_PARTS, + False, + DELAY_NS, + ) + if ABSORB_IN_PRODUCER: + if pid >= PRODUCER_JOB_SHIFT: + _absorb_job( + job, + w_lo, + w_hi, + q_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + rows, + rmask, + KV_LORA, + ROPE, + HEAD_DIM, + NOPE, + H, + BN, + BC, + DELAY_NS, + ) + elif pid < N_QB + N_ABS: + _absorb_program( + ABS_JOB0 + pid - N_QB, + wuk_ptr, + q_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + rows, + rmask, + KV_LORA, + ROPE, + HEAD_DIM, + NOPE, + H, + BN, + BC, + USE_PDL, + DELAY_NS, + ) + else: + if USE_PDL: + tl.extra.cuda.gdc_wait() + _kv_program( + kva_ptr, + stride_kva, + gkv_ptr, + latent_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps_kv, + Q_LORA, + KV_LORA, + ROPE, + BLOCK_M, + SS_PARTIALS, + MAX_PARTS, + ) + + +@triton.jit +def _gemv_cols( + x_ptr, + stride_x, + w_ptr, + out_ptr, + stride_o, + ss_ptr, + R, + col0, + n_cols, + SS_SPLIT: tl.constexpr, + SS_END: tl.constexpr, + WRITE_SS: tl.constexpr, + CPP: tl.constexpr, + K: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, +): + rows = tl.arange(0, BLOCK_M) + rmask = rows < R + lane = tl.arange(0, BN) + cols = col0 + lane + cmask = (lane < CPP) & (cols < n_cols) + kk = tl.arange(0, BK) + acc = tl.zeros([BLOCK_M, BN], dtype=tl.float32) + for k0 in tl.range(0, K, BK): + x = tl.load( + x_ptr + rows[:, None] * stride_x + (k0 + kk)[None, :], mask=rmask[:, None], other=0.0 + ) + w = tl.load(w_ptr + cols[None, :] * K + (k0 + kk)[:, None], mask=cmask[None, :], other=0.0) + acc = tl.dot(x, w, acc) + out = acc.to(tl.bfloat16) + tl.store( + out_ptr + rows[:, None] * stride_o + cols[None, :], + out, + mask=rmask[:, None] & cmask[None, :], + ) + if WRITE_SS: + # Per-program partial sums of squares of the BF16 outputs over the + # RMSNorm groups [0, SS_SPLIT) and [SS_SPLIT, SS_END); the query + # kernel reduces them instead of re-reading the rows. + sq = out.to(tl.float32) + sq = sq * sq + in_a = cmask & (cols < SS_SPLIT) + in_b = cmask & (cols >= SS_SPLIT) & (cols < SS_END) + ss_a = tl.sum(tl.where(in_a[None, :], sq, 0.0), axis=1) + ss_b = tl.sum(tl.where(in_b[None, :], sq, 0.0), axis=1) + pid = tl.program_id(0) + tl.store(ss_ptr + pid * 2 * BLOCK_M + rows, ss_a) + tl.store(ss_ptr + pid * 2 * BLOCK_M + BLOCK_M + rows, ss_b) + + +@triton.jit +def hidden_gemv_kernel( + x_ptr, + stride_x, + w0_ptr, + w1_ptr, + out0_ptr, + stride_o0, + out1_ptr, + stride_o1, + ss_ptr, + R, + N0, + N1, + P0: tl.constexpr, + CPP0: tl.constexpr, + CPP1: tl.constexpr, + SS_SPLIT: tl.constexpr, + SS_END: tl.constexpr, + WRITE_SS: tl.constexpr, + K: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, + USE_PDL: tl.constexpr, +): + """``out0 = x @ W0^T`` and ``out1 = x @ W1^T`` for ``R <= BLOCK_M`` rows. + + ``W0 [N0, K]`` (the native fused ``kv_a_proj_with_mqa`` weight) and the + optional ``W1 [N1, K]`` (the indexer's fused ``wk``/``weights_proj`` + weight) share the input. Programs ``[0, P0)`` own ``CPP0`` consecutive + columns of ``W0``, the rest ``CPP1`` columns of ``W1`` (``BN`` is the + next power of two), each over the full ``K``, so the work is split + evenly over about one wave. FP32 accumulation, BF16 outputs. With + ``WRITE_SS``, ``W0`` programs also emit RMSNorm partial sums (see + ``_gemv_cols``) into ``ss_ptr [P0, 2, BLOCK_M]``. + """ + pid = tl.program_id(0) + if USE_PDL: + tl.extra.cuda.gdc_wait() + if pid < P0: + _gemv_cols( + x_ptr, + stride_x, + w0_ptr, + out0_ptr, + stride_o0, + ss_ptr, + R, + pid * CPP0, + N0, + SS_SPLIT, + SS_END, + WRITE_SS, + CPP0, + K, + BLOCK_M, + BN, + BK, + ) + else: + _gemv_cols( + x_ptr, + stride_x, + w1_ptr, + out1_ptr, + stride_o1, + ss_ptr, + R, + (pid - P0) * CPP1, + N1, + 0, + 0, + False, + CPP1, + K, + BLOCK_M, + BN, + BK, + ) + + +# -------------------------------------------------------------------------- +# Full-indexer layers: the same query kernel plus the token-wise indexer +# projection (the index_selection contract, token-wise part), reproducing the native +# ``Indexer.pre_indexer_proj`` chain for BF16 projections and FP8 keys bit +# for bit: +# q = wq_b(qr); rope(q[:, :, :64]); fused_cat_fp8(q) (per head) +# k = k_norm(wk(hidden)); rope(k[:, :64]); fused_cat_fp8(k) +# weights = weights_proj(hidden) * q_scale * softmax_scale * n_heads^-0.5 +# The fused wk/weights_proj GEMM and ``k_norm`` (LayerNorm under +# torch.compile) run natively as bridges; the kernel consumes their outputs. +# -------------------------------------------------------------------------- + + +@triton.jit +def _iq_program( + t, + kva_ptr, + stride_kva, + gq_ptr, + wiq_ptr, + iq_ptr, + isync_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps, + Q_LORA: tl.constexpr, + IHD: tl.constexpr, + IH: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, + SS_PARTIALS: tl.constexpr, + MAX_PARTS: tl.constexpr, + PRENORM: tl.constexpr, + DELAY_NS: tl.constexpr = 0, +): + if PRENORM: + _prenorm_gemm( + t, kva_ptr, stride_kva, wiq_ptr, iq_ptr, IH * IHD, rows, rmask, Q_LORA, BLOCK_M, BN, BK + ) + else: + _iq_norm_gemm( + t, + kva_ptr, + stride_kva, + gq_ptr, + wiq_ptr, + iq_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps, + Q_LORA, + IHD, + IH, + BLOCK_M, + BN, + BK, + SS_PARTIALS, + MAX_PARTS, + ) + _stress_delay(t, 5, DELAY_NS) + tl.debug_barrier() + tl.atomic_add(isync_ptr + t // (IHD // BN), 1, sem="release", scope="gpu") + + +@triton.jit +def _iq_norm_gemm( + t, + kva_ptr, + stride_kva, + gq_ptr, + wiq_ptr, + iq_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps, + Q_LORA: tl.constexpr, + IHD: tl.constexpr, + IH: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, + SS_PARTIALS: tl.constexpr, + MAX_PARTS: tl.constexpr, +): + if SS_PARTIALS: + rcp = _rms_rcp_partials(ss_ptr, 0, n_parts, Q_LORA, BLOCK_M, MAX_PARTS, eps) + else: + rcp = _rms_rcp(kva_ptr, stride_kva, rows, rmask, Q_LORA, BLOCK_M, BK, eps) + cols = tl.arange(0, BK) + n_offs = t * BN + tl.arange(0, BN) + acc = tl.zeros([BLOCK_M, BN], dtype=tl.float32) + for k0 in tl.range(0, Q_LORA, BK): + x = tl.load( + kva_ptr + rows[:, None] * stride_kva + (k0 + cols)[None, :], + mask=rmask[:, None], + other=0.0, + ).to(tl.float32) + g = tl.load(gq_ptr + k0 + cols).to(tl.float32) + xn = (x * rcp[:, None] * g[None, :]).to(tl.bfloat16) + w = tl.load(wiq_ptr + n_offs[None, :] * Q_LORA + (k0 + cols)[:, None]) + acc = tl.dot(xn, w, acc) + tl.store( + iq_ptr + rows[:, None] * (IH * IHD) + n_offs[None, :], + acc.to(tl.bfloat16), + mask=rmask[:, None], + ) + + +@triton.jit +def _fp8_ue8m0_scale(amax): + """``2^ceil(log2(max(amax, 1e-12) / 448))`` and its exact inverse, as in + the native ``fusedCatFp8Kernel``.""" + ratio = tl.maximum(amax, 1.0e-12) * (1.0 / 448.0) + bits = ratio.to(tl.uint32, bitcast=True) + exp_bits = bits & 0x7F800000 + exp_bits = tl.where((bits & 0x007FFFFF) != 0, exp_bits + 0x00800000, exp_bits) + scale = exp_bits.to(tl.float32, bitcast=True) + inv_scale = ((254 << 23) - exp_bits).to(tl.float32, bitcast=True) + return scale, inv_scale + + +@triton.jit +def _mul_rn(a, b): + """FP32 multiply rounded once (never contracted into an FMA).""" + return tl.inline_asm_elementwise( + "mul.rn.f32 $0, $1, $2;", "=r,r,r", [a, b], dtype=tl.float32, is_pure=True, pack=1 + ) + + +@triton.jit +def _fma_rn(a, b, c): + """``a * b + c`` with a single rounding.""" + return tl.inline_asm_elementwise( + "fma.rn.f32 $0, $1, $2, $3;", "=r,r,r,r", [a, b, c], dtype=tl.float32, is_pure=True, pack=1 + ) + + +@triton.jit +def _rope_pairs(x_even, x_odd, cos, sin): + """Interleaved (GPT-J) RoPE on BF16 pairs, FP32 math, BF16 result, with + the exact arithmetic of the native indexer RoPE (FlashInfer + ``vec_apply_llama_rope_cos_sin_interleave_reuse_half``: ``x[i] * cos + + (+-x[i ^ 1]) * sin`` compiled as ``fma(x[i], cos, rn((+-x[i ^ 1]) * + sin))``), pinned with explicit ``fma.rn`` / ``mul.rn``. Checked bit for + bit against the native kernel on real and synthetic rows.""" + o_even = _fma_rn(x_even, cos, -_mul_rn(x_odd, sin)) + o_odd = _fma_rn(x_odd, cos, _mul_rn(x_even, sin)) + return (o_even.to(tl.bfloat16).to(tl.float32), o_odd.to(tl.bfloat16).to(tl.float32)) + + +@triton.jit +def _qpost_program( + i, + iq_ptr, + isync_ptr, + kw_ptr, + stride_kw, + pos_ptr, + cos_sin_ptr, + qfp8_ptr, + qscale_ptr, + wout_ptr, + w_scale, + rows, + rmask, + IHD: tl.constexpr, + IH: tl.constexpr, + ROPE_I: tl.constexpr, + BN: tl.constexpr, + USE_PDL: tl.constexpr, + DELAY_NS: tl.constexpr = 0, +): + half: tl.constexpr = ROPE_I // 2 + nope: tl.constexpr = IHD - ROPE_I + j = tl.arange(0, half) + c = tl.arange(0, nope) + pos = tl.load(pos_ptr + rows, mask=rmask, other=0).to(tl.int64) + cs_row = cos_sin_ptr + pos[:, None] * ROPE_I + cos = tl.load(cs_row + j[None, :], mask=rmask[:, None], other=0.0) + sin = tl.load(cs_row + half + j[None, :], mask=rmask[:, None], other=0.0) + if USE_PDL: + tl.extra.cuda.gdc_wait() + w = tl.load(kw_ptr + rows * stride_kw + IHD + i, mask=rmask, other=0.0).to(tl.float32) + + ready = tl.atomic_add(isync_ptr + i, 0, sem="acquire", scope="gpu") + while ready < IHD // BN: + ready = tl.atomic_add(isync_ptr + i, 0, sem="acquire", scope="gpu") + tl.debug_barrier() + + base = iq_ptr + rows[:, None] * (IH * IHD) + i * IHD + xe = tl.load(base + 2 * j[None, :], mask=rmask[:, None], other=0.0, cache_modifier=".cg").to( + tl.float32 + ) + xo = tl.load( + base + 2 * j[None, :] + 1, mask=rmask[:, None], other=0.0, cache_modifier=".cg" + ).to(tl.float32) + xn = tl.load( + base + ROPE_I + c[None, :], mask=rmask[:, None], other=0.0, cache_modifier=".cg" + ).to(tl.float32) + oe, oo = _rope_pairs(xe, xo, cos, sin) + amax = tl.maximum( + tl.maximum(tl.max(tl.abs(oe), axis=1), tl.max(tl.abs(oo), axis=1)), + tl.max(tl.abs(xn), axis=1), + ) + scale, inv = _fp8_ue8m0_scale(amax) + out = qfp8_ptr + rows[:, None] * (IH * IHD) + i * IHD + tl.store(out + 2 * j[None, :], (oe * inv[:, None]).to(tl.float8e4nv), mask=rmask[:, None]) + tl.store(out + 2 * j[None, :] + 1, (oo * inv[:, None]).to(tl.float8e4nv), mask=rmask[:, None]) + tl.store(out + ROPE_I + c[None, :], (xn * inv[:, None]).to(tl.float8e4nv), mask=rmask[:, None]) + tl.store(qscale_ptr + rows * IH + i, scale, mask=rmask) + tl.store(wout_ptr + rows * IH + i, w * scale * w_scale, mask=rmask) + # Single consumer per head: reset for the next launch. + _stress_delay(i, 7, DELAY_NS) + tl.atomic_xchg(isync_ptr + i, 0, sem="relaxed", scope="gpu") + + +@triton.jit +def _kpost_program( + kln_ptr, + stride_kln, + pos_ptr, + cos_sin_ptr, + kfp8_ptr, + kscale_ptr, + rows, + rmask, + IHD: tl.constexpr, + ROPE_I: tl.constexpr, + USE_PDL: tl.constexpr, +): + """Indexer key tail on the native ``k_norm`` output ``kln [R, IHD]`` + (the LayerNorm itself stays native: its inductor reduction order + depends on the compiled configuration): native RoPE arithmetic + (``_rope_pairs``) and the native ``fusedCatFp8Kernel`` + quantization.""" + half: tl.constexpr = ROPE_I // 2 + nope: tl.constexpr = IHD - ROPE_I + j = tl.arange(0, half) + c = tl.arange(0, nope) + pos = tl.load(pos_ptr + rows, mask=rmask, other=0).to(tl.int64) + cs_row = cos_sin_ptr + pos[:, None] * ROPE_I + cos = tl.load(cs_row + j[None, :], mask=rmask[:, None], other=0.0) + sin = tl.load(cs_row + half + j[None, :], mask=rmask[:, None], other=0.0) + if USE_PDL: + tl.extra.cuda.gdc_wait() + base = kln_ptr + rows[:, None] * stride_kln + ye = tl.load(base + 2 * j[None, :], mask=rmask[:, None], other=0.0).to(tl.float32) + yo = tl.load(base + 2 * j[None, :] + 1, mask=rmask[:, None], other=0.0).to(tl.float32) + yn = tl.load(base + ROPE_I + c[None, :], mask=rmask[:, None], other=0.0).to(tl.float32) + oe, oo = _rope_pairs(ye, yo, cos, sin) + amax = tl.maximum( + tl.maximum(tl.max(tl.abs(oe), axis=1), tl.max(tl.abs(oo), axis=1)), + tl.max(tl.abs(yn), axis=1), + ) + scale, inv = _fp8_ue8m0_scale(amax) + out = kfp8_ptr + rows[:, None] * IHD + tl.store(out + 2 * j[None, :], (oe * inv[:, None]).to(tl.float8e4nv), mask=rmask[:, None]) + tl.store(out + 2 * j[None, :] + 1, (oo * inv[:, None]).to(tl.float8e4nv), mask=rmask[:, None]) + tl.store(out + ROPE_I + c[None, :], (yn * inv[:, None]).to(tl.float8e4nv), mask=rmask[:, None]) + tl.store(kscale_ptr + rows, scale, mask=rmask) + + +@triton.jit +def qproj_indexer_kernel( + kva_ptr, + stride_kva, + gq_ptr, + gkv_ptr, + wqb_ptr, + wuk_ptr, + q_ptr, + latent_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + ss_ptr, + n_parts, + R, + eps_q, + eps_kv, + wiq_ptr, + iq_ptr, + kw_ptr, + stride_kw, + kln_ptr, + stride_kln, + pos_ptr, + cos_sin_ptr, + qfp8_ptr, + qscale_ptr, + kfp8_ptr, + kscale_ptr, + wout_ptr, + w_scale, + Q_LORA: tl.constexpr, + KV_LORA: tl.constexpr, + ROPE: tl.constexpr, + HEAD_DIM: tl.constexpr, + NOPE: tl.constexpr, + H: tl.constexpr, + IHD: tl.constexpr, + IH: tl.constexpr, + ROPE_I: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BK: tl.constexpr, + BC: tl.constexpr, + N_QB: tl.constexpr, + N_IQ: tl.constexpr, + N_ABS: tl.constexpr, + DO_ABSORB: tl.constexpr, + SS_PARTIALS: tl.constexpr, + MAX_PARTS: tl.constexpr, + USE_PDL: tl.constexpr, + PRENORM: tl.constexpr = False, + qn_ptr=None, + stride_qn=0, + ABSORB_IN_PRODUCER: tl.constexpr = False, + PRODUCER_JOB_SHIFT: tl.constexpr = 0, + ABS_JOB0: tl.constexpr = 0, + DELAY_NS: tl.constexpr = 0, +): + """``qproj_kernel`` plus the indexer's token-wise projection. + + Program roles, in program-id order (every consumer after the producers + it waits on): ``q_b`` tiles (with ``ABSORB_IN_PRODUCER``, producer + ``p >= PRODUCER_JOB_SHIFT`` then runs absorption job + ``p - PRODUCER_JOB_SHIFT``), indexer ``wq_b`` tiles (publish per indexer + head), absorption jobs ``ABS_JOB0 + i``, indexer query post-processing + (RoPE, FP8, weights; one program per indexer head), ``compressed_kv`` + norm, indexer key post-processing. ``sync_ptr`` holds ``2 * H`` + absorption counters followed by ``IH`` indexer-head counters. + """ + pid = tl.program_id(0) + rows = tl.arange(0, BLOCK_M) + rmask = rows < R + if pid < N_QB: + if ABSORB_IN_PRODUCER: + job = tl.maximum(pid - PRODUCER_JOB_SHIFT, 0) + w_lo, w_hi = _ld_wuk(job, pid >= PRODUCER_JOB_SHIFT, wuk_ptr, KV_LORA, NOPE, BC) + if USE_PDL: + tl.extra.cuda.gdc_wait() + if PRENORM: + _qb_program( + pid, + qn_ptr, + stride_qn, + gq_ptr, + wqb_ptr, + q_ptr, + q_ptr, + sync_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps_q, + Q_LORA, + HEAD_DIM, + NOPE, + H, + BLOCK_M, + BN, + BK, + False, + DO_ABSORB, + False, + MAX_PARTS, + True, + DELAY_NS, + ) + else: + _qb_program( + pid, + kva_ptr, + stride_kva, + gq_ptr, + wqb_ptr, + q_ptr, + q_ptr, + sync_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps_q, + Q_LORA, + HEAD_DIM, + NOPE, + H, + BLOCK_M, + BN, + BK, + False, + DO_ABSORB, + SS_PARTIALS, + MAX_PARTS, + False, + DELAY_NS, + ) + if ABSORB_IN_PRODUCER: + if pid >= PRODUCER_JOB_SHIFT: + _absorb_job( + job, + w_lo, + w_hi, + q_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + rows, + rmask, + KV_LORA, + ROPE, + HEAD_DIM, + NOPE, + H, + BN, + BC, + DELAY_NS, + ) + elif pid < N_QB + N_IQ: + if USE_PDL: + tl.extra.cuda.gdc_wait() + if PRENORM: + _iq_program( + pid - N_QB, + qn_ptr, + stride_qn, + gq_ptr, + wiq_ptr, + iq_ptr, + sync_ptr + 2 * H, + ss_ptr, + n_parts, + rows, + rmask, + eps_q, + Q_LORA, + IHD, + IH, + BLOCK_M, + BN, + BK, + False, + MAX_PARTS, + True, + DELAY_NS, + ) + else: + _iq_program( + pid - N_QB, + kva_ptr, + stride_kva, + gq_ptr, + wiq_ptr, + iq_ptr, + sync_ptr + 2 * H, + ss_ptr, + n_parts, + rows, + rmask, + eps_q, + Q_LORA, + IHD, + IH, + BLOCK_M, + BN, + BK, + SS_PARTIALS, + MAX_PARTS, + False, + DELAY_NS, + ) + elif pid < N_QB + N_IQ + N_ABS: + _absorb_program( + ABS_JOB0 + pid - N_QB - N_IQ, + wuk_ptr, + q_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + rows, + rmask, + KV_LORA, + ROPE, + HEAD_DIM, + NOPE, + H, + BN, + BC, + USE_PDL, + DELAY_NS, + ) + elif pid < N_QB + N_IQ + N_ABS + IH: + _qpost_program( + pid - N_QB - N_IQ - N_ABS, + iq_ptr, + sync_ptr + 2 * H, + kw_ptr, + stride_kw, + pos_ptr, + cos_sin_ptr, + qfp8_ptr, + qscale_ptr, + wout_ptr, + w_scale, + rows, + rmask, + IHD, + IH, + ROPE_I, + BN, + USE_PDL, + DELAY_NS, + ) + elif pid == N_QB + N_IQ + N_ABS + IH: + if USE_PDL: + tl.extra.cuda.gdc_wait() + _kv_program( + kva_ptr, + stride_kva, + gkv_ptr, + latent_ptr, + ss_ptr, + n_parts, + rows, + rmask, + eps_kv, + Q_LORA, + KV_LORA, + ROPE, + BLOCK_M, + SS_PARTIALS, + MAX_PARTS, + ) + else: + _kpost_program( + kln_ptr, + stride_kln, + pos_ptr, + cos_sin_ptr, + kfp8_ptr, + kscale_ptr, + rows, + rmask, + IHD, + ROPE_I, + USE_PDL, + ) + + +# -------------------------------------------------------------------------- +# Prefetching variant (shared and full indexer layers). +# +# The cp.async-pipelined producers above are latency bound: each CTA keeps +# only (num_stages - 1) K-tiles in flight and starts its weight stream only +# after the RMSNorm pre-pass. Here every producer issues its whole [K, BN] +# weight tile (and its absorption W_UK slice) as independent loads at +# program start -- before ``griddepcontrol.wait``, so under programmatic +# dependent launch the weight stream overlaps the preceding native kv_a +# GEMM -- then waits, loads the eight q_a row tiles at once, normalizes and +# multiplies. +# Each producer program of head h + 1 also performs one absorption job of +# head h after publishing its own tile (produce-then-consume), and TPH extra +# programs after all producers absorb the last head, so every wait targets +# lower program ids (see the module docstring). +# -------------------------------------------------------------------------- + + +@triton.jit +def _ld_w(w_ptr, n_offs, j: tl.constexpr, K: tl.constexpr, BK: tl.constexpr): + k = j * BK + tl.arange(0, BK) + return tl.load(w_ptr + n_offs[None, :] * K + k[:, None]) + + +@triton.jit +def _ld_x(kva_ptr, stride_kva, rows, rmask, j: tl.constexpr, BK: tl.constexpr): + k = j * BK + tl.arange(0, BK) + return tl.load( + kva_ptr + rows[:, None] * stride_kva + k[None, :], mask=rmask[:, None], other=0.0 + ) + + +@triton.jit +def _sq(x): + xf = x.to(tl.float32) + return tl.sum(xf * xf, axis=1) + + +@triton.jit +def _nx(x, rcp, gq_ptr, j: tl.constexpr, BK: tl.constexpr): + g = tl.load(gq_ptr + j * BK + tl.arange(0, BK)).to(tl.float32) + return (x.to(tl.float32) * rcp[:, None] * g[None, :]).to(tl.bfloat16) + + +@triton.jit +def _prefetch_tile_gemm( + t, + w_ptr, + out_ptr, + OUT_COLS: tl.constexpr, + kva_ptr, + stride_kva, + gq_ptr, + qr_ptr, + rows, + rmask, + eps, + Q_LORA: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + WRITE_QR: tl.constexpr, + USE_PDL: tl.constexpr, +): + """One [R, BN] tile of ``rmsnorm(q_a) @ W[t*BN:(t+1)*BN]^T`` with the + whole weight tile requested before the grid-dependency wait.""" + BK: tl.constexpr = Q_LORA // 8 + n_offs = t * BN + tl.arange(0, BN) + w0 = _ld_w(w_ptr, n_offs, 0, Q_LORA, BK) + w1 = _ld_w(w_ptr, n_offs, 1, Q_LORA, BK) + w2 = _ld_w(w_ptr, n_offs, 2, Q_LORA, BK) + w3 = _ld_w(w_ptr, n_offs, 3, Q_LORA, BK) + w4 = _ld_w(w_ptr, n_offs, 4, Q_LORA, BK) + w5 = _ld_w(w_ptr, n_offs, 5, Q_LORA, BK) + w6 = _ld_w(w_ptr, n_offs, 6, Q_LORA, BK) + w7 = _ld_w(w_ptr, n_offs, 7, Q_LORA, BK) + if USE_PDL: + tl.extra.cuda.gdc_wait() + x0 = _ld_x(kva_ptr, stride_kva, rows, rmask, 0, BK) + x1 = _ld_x(kva_ptr, stride_kva, rows, rmask, 1, BK) + x2 = _ld_x(kva_ptr, stride_kva, rows, rmask, 2, BK) + x3 = _ld_x(kva_ptr, stride_kva, rows, rmask, 3, BK) + x4 = _ld_x(kva_ptr, stride_kva, rows, rmask, 4, BK) + x5 = _ld_x(kva_ptr, stride_kva, rows, rmask, 5, BK) + x6 = _ld_x(kva_ptr, stride_kva, rows, rmask, 6, BK) + x7 = _ld_x(kva_ptr, stride_kva, rows, rmask, 7, BK) + ss = _sq(x0) + _sq(x1) + _sq(x2) + _sq(x3) + _sq(x4) + _sq(x5) + _sq(x6) + _sq(x7) + rcp = tl.rsqrt(ss / Q_LORA + eps) + xn0 = _nx(x0, rcp, gq_ptr, 0, BK) + xn1 = _nx(x1, rcp, gq_ptr, 1, BK) + xn2 = _nx(x2, rcp, gq_ptr, 2, BK) + xn3 = _nx(x3, rcp, gq_ptr, 3, BK) + xn4 = _nx(x4, rcp, gq_ptr, 4, BK) + xn5 = _nx(x5, rcp, gq_ptr, 5, BK) + xn6 = _nx(x6, rcp, gq_ptr, 6, BK) + xn7 = _nx(x7, rcp, gq_ptr, 7, BK) + if WRITE_QR: + if t == 0: + kk = tl.arange(0, BK) + qr_row = qr_ptr + rows[:, None] * Q_LORA + kk[None, :] + tl.store(qr_row + 0 * BK, xn0, mask=rmask[:, None]) + tl.store(qr_row + 1 * BK, xn1, mask=rmask[:, None]) + tl.store(qr_row + 2 * BK, xn2, mask=rmask[:, None]) + tl.store(qr_row + 3 * BK, xn3, mask=rmask[:, None]) + tl.store(qr_row + 4 * BK, xn4, mask=rmask[:, None]) + tl.store(qr_row + 5 * BK, xn5, mask=rmask[:, None]) + tl.store(qr_row + 6 * BK, xn6, mask=rmask[:, None]) + tl.store(qr_row + 7 * BK, xn7, mask=rmask[:, None]) + acc = tl.dot(xn0, w0) + acc = tl.dot(xn1, w1, acc) + acc = tl.dot(xn2, w2, acc) + acc = tl.dot(xn3, w3, acc) + acc = tl.dot(xn4, w4, acc) + acc = tl.dot(xn5, w5, acc) + acc = tl.dot(xn6, w6, acc) + acc = tl.dot(xn7, w7, acc) + tl.store( + out_ptr + rows[:, None] * OUT_COLS + n_offs[None, :], + acc.to(tl.bfloat16), + mask=rmask[:, None], + ) + + +@triton.jit +def _absorb_job( + a, + w_lo, + w_hi, + q_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + rows, + rmask, + KV_LORA: tl.constexpr, + ROPE: tl.constexpr, + HEAD_DIM: tl.constexpr, + NOPE: tl.constexpr, + H: tl.constexpr, + BN: tl.constexpr, + BC: tl.constexpr, + DELAY_NS: tl.constexpr = 0, +): + chunks_per_head: tl.constexpr = KV_LORA // BC + h = a // chunks_per_head + c0 = (a % chunks_per_head) * BC + cc = tl.arange(0, BC) + d_lo = tl.arange(0, 128) + d_hi = tl.arange(0, NOPE - 128) + ready = tl.atomic_add(sync_ptr + h, 0, sem="acquire", scope="gpu") + while ready < NOPE // BN: + ready = tl.atomic_add(sync_ptr + h, 0, sem="acquire", scope="gpu") + tl.debug_barrier() + q_base = q_ptr + rows[:, None] * (H * HEAD_DIM) + h * HEAD_DIM + q_lo = tl.load(q_base + d_lo[None, :], mask=rmask[:, None], other=0.0, cache_modifier=".cg") + q_hi = tl.load( + q_base + 128 + d_hi[None, :], mask=rmask[:, None], other=0.0, cache_modifier=".cg" + ) + acc = tl.dot(q_lo, w_lo) + acc = tl.dot(q_hi, w_hi, acc) + tl.store( + fq_ptr + rows[:, None] * stride_fq_row + h * (KV_LORA + ROPE) + (c0 + cc)[None, :], + acc.to(tl.bfloat16), + mask=rmask[:, None], + ) + _stress_delay(a, 3, DELAY_NS) + tl.debug_barrier() + done = tl.atomic_add(sync_ptr + H + h, 1, sem="acq_rel", scope="gpu") + if done == chunks_per_head - 1: + tl.atomic_xchg(sync_ptr + h, 0, sem="relaxed", scope="gpu") + tl.atomic_xchg(sync_ptr + H + h, 0, sem="relaxed", scope="gpu") + + +@triton.jit +def qproj_prefetch_kernel( + kva_ptr, + stride_kva, + gq_ptr, + gkv_ptr, + wqb_ptr, + wuk_ptr, + q_ptr, + qr_ptr, + latent_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + R, + eps_q, + eps_kv, + wiq_ptr, + iq_ptr, + kw_ptr, + stride_kw, + kln_ptr, + stride_kln, + pos_ptr, + cos_sin_ptr, + qfp8_ptr, + qscale_ptr, + kfp8_ptr, + kscale_ptr, + wout_ptr, + w_scale, + Q_LORA: tl.constexpr, + KV_LORA: tl.constexpr, + ROPE: tl.constexpr, + HEAD_DIM: tl.constexpr, + NOPE: tl.constexpr, + H: tl.constexpr, + IHD: tl.constexpr, + IH: tl.constexpr, + ROPE_I: tl.constexpr, + BLOCK_M: tl.constexpr, + BN: tl.constexpr, + BC: tl.constexpr, + N_QB: tl.constexpr, + N_IQ: tl.constexpr, + WRITE_QR: tl.constexpr, + DO_ABSORB: tl.constexpr, + FUSE_INDEXER: tl.constexpr, + USE_PDL: tl.constexpr, + N_ABS: tl.constexpr = 0, + PRODUCER_JOB_SHIFT: tl.constexpr = 0, + ABS_JOB0: tl.constexpr = 0, + DELAY_NS: tl.constexpr = 0, +): + """Prefetching query kernel. Program roles in program-id order: + + * ``[0, N_QB)``: ``q_b`` tile ``pid`` (prefetched), then (``DO_ABSORB``, + ``pid >= PRODUCER_JOB_SHIFT``) absorption job ``pid - + PRODUCER_JOB_SHIFT`` (``N_QB == H * KV_LORA // BC``; its ``W_UK`` slice + is also prefetched before the wait); + * ``[N_QB, N_QB + N_IQ)`` (``FUSE_INDEXER``): indexer ``wq_b`` tiles; + * ``N_ABS`` absorption jobs ``ABS_JOB0 + i``; + * ``IH`` indexer-query post-processing programs (``FUSE_INDEXER``); + * the ``compressed_kv`` norm / latent program; + * the indexer-key program (``FUSE_INDEXER``). + """ + pid = tl.program_id(0) + rows = tl.arange(0, BLOCK_M) + rmask = rows < R + if pid < N_QB: + if DO_ABSORB: + job = tl.maximum(pid - PRODUCER_JOB_SHIFT, 0) + w_lo, w_hi = _ld_wuk(job, pid >= PRODUCER_JOB_SHIFT, wuk_ptr, KV_LORA, NOPE, BC) + _prefetch_tile_gemm( + pid, + wqb_ptr, + q_ptr, + H * HEAD_DIM, + kva_ptr, + stride_kva, + gq_ptr, + qr_ptr, + rows, + rmask, + eps_q, + Q_LORA, + BLOCK_M, + BN, + WRITE_QR, + USE_PDL, + ) + if DO_ABSORB: + if (pid % (HEAD_DIM // BN)) < (NOPE // BN): + _stress_delay(pid, 1, DELAY_NS) + tl.debug_barrier() + tl.atomic_add(sync_ptr + pid // (HEAD_DIM // BN), 1, sem="release", scope="gpu") + if pid >= PRODUCER_JOB_SHIFT: + _absorb_job( + job, + w_lo, + w_hi, + q_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + rows, + rmask, + KV_LORA, + ROPE, + HEAD_DIM, + NOPE, + H, + BN, + BC, + DELAY_NS, + ) + elif pid < N_QB + N_IQ: + t = pid - N_QB + _prefetch_tile_gemm( + t, + wiq_ptr, + iq_ptr, + IH * IHD, + kva_ptr, + stride_kva, + gq_ptr, + qr_ptr, + rows, + rmask, + eps_q, + Q_LORA, + BLOCK_M, + BN, + False, + USE_PDL, + ) + _stress_delay(t, 5, DELAY_NS) + tl.debug_barrier() + tl.atomic_add(sync_ptr + 2 * H + t // (IHD // BN), 1, sem="release", scope="gpu") + elif pid < N_QB + N_IQ + N_ABS: + _absorb_program( + ABS_JOB0 + pid - N_QB - N_IQ, + wuk_ptr, + q_ptr, + fq_ptr, + stride_fq_row, + sync_ptr, + rows, + rmask, + KV_LORA, + ROPE, + HEAD_DIM, + NOPE, + H, + BN, + BC, + USE_PDL, + DELAY_NS, + ) + elif pid < N_QB + N_IQ + N_ABS + IH: + _qpost_program( + pid - N_QB - N_IQ - N_ABS, + iq_ptr, + sync_ptr + 2 * H, + kw_ptr, + stride_kw, + pos_ptr, + cos_sin_ptr, + qfp8_ptr, + qscale_ptr, + wout_ptr, + w_scale, + rows, + rmask, + IHD, + IH, + ROPE_I, + BN, + USE_PDL, + DELAY_NS, + ) + elif pid == N_QB + N_IQ + N_ABS + IH: + if USE_PDL: + tl.extra.cuda.gdc_wait() + _kv_program( + kva_ptr, + stride_kva, + gkv_ptr, + latent_ptr, + sync_ptr, + 0, + rows, + rmask, + eps_kv, + Q_LORA, + KV_LORA, + ROPE, + BLOCK_M, + False, + 16, + ) + else: + _kpost_program( + kln_ptr, + stride_kln, + pos_ptr, + cos_sin_ptr, + kfp8_ptr, + kscale_ptr, + rows, + rmask, + IHD, + ROPE_I, + USE_PDL, + ) diff --git a/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/query_proj.py b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/query_proj.py new file mode 100644 index 000000000000..ecf1e58de8cc --- /dev/null +++ b/tensorrt_llm/_torch/_experimental/modeling_v2/models/glm_moe_dsa/shared/kernels/absorbed_query/query_proj.py @@ -0,0 +1,886 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""absorbed_query: target query/projection region of GLM-5.x DSA MLA (the query_front contract). + +``dsa_proj`` is the package entry point. It replaces the token-wise work of +the native ``forward_dsa_proj`` for one generation-only target layer and +returns a :class:`AbsorbedQueryPacket`. ``mtp_dsa_proj`` (``draft_absorbed_query``) does +the same for the one-model MTP draft layer at every draft step. The packet +holds: + +* ``proj_outputs``: the native 4/9-tensor list ``[q, compressed_kv, k_pe, + latent_cache(, q_fp8, k_fp8, k_scale, weights, q_scale)]`` with native + shapes, dtypes and values (``compressed_kv``/``k_pe`` are views of + ``latent_cache``); +* ``fused_q``: in the preabsorbed form, the ``[R, H, kv_lora + rope]`` buffer + whose ``[..., :kv_lora]`` already holds ``q_nope @ W_UK`` (BF16). The + caller hands it to the native ``forward_absorption_generation`` in place of + its own allocation and skips only the absorption BMM; native RoPE, cache + append, FP8 query quantization and scheduler buffers still write the rest. + ``None`` in the native-q form. + +The kv_a GEMM and, on full indexer layers, ``Indexer.pre_indexer_proj`` run +natively around the absorbed_query kernel (adapter bridges). ``dsa_proj`` returns ``None`` +before any GPU work when the call is outside the support domain, so the +caller runs the unchanged native path. +""" + +from dataclasses import dataclass +from typing import List, Optional, Tuple + +import torch + +from ...dispatch import log_capability +from . import kernels + +#: How the form's capability line names it (``