""" ex_engine/factors/attn_dispatch.py Layer 3: Attention prefill/decode dispatch Upstream parallel: xllm_layers/ilu/attention.h + attention.cpp (82 + ~200 lines) → batch_prefill() dispatches to ixinfer_flash_attn_unpad_with_block_tables → batch_decode() dispatches to xllm_paged_attention The key dispatch decision: prefill (num_prefill_tokens > 0): → flash attention with block tables (unpadded, variable-length) → supports cu_seqlens for multi-request batching → window attention via window_size_left/right decode (pure autoregressive, 1 token per sequence): → paged attention v1/v2 → v1 vs v2 decision: total_tiles vs 2 × sm_count → BI-V100: 16 SMs → V2 beneficial when seq_len > 1024 GDN (GatedDeltaNet) layers [1,7,13,19] bypass this entirely — they use the gdn_dispatch module instead. Call chain: qwen3_5.py Qwen3_5DecoderLayer.forward() → (full attention layers): attn_dispatch.dispatch_attention() → prefill: flash_attn_with_block_tables → decode: paged_attention_v1 or v2 → (GDN layers): gdn_dispatch.dispatch_gdn() """ import logging from typing import Optional, Tuple import torch logger = logging.getLogger("ex_engine.attn_dispatch") # BI-V100 dispatch thresholds # Source: SYSTEM_DESIGN.md, sub694 TPS analysis _SM_COUNT = 16 _V2_THRESHOLD_FACTOR = 2 # V2 when total_tiles > 2 × SM_COUNT _PAGE_BLOCK_SIZE = 16 # default paged attention block size class AttnDispatchConfig: """ Attention dispatch configuration. Parallels xllm_layers/ilu/attention.h struct members: scale, is_causal, window_size_left, window_size_right, softcap """ __slots__ = ( 'num_heads', 'num_kv_heads', 'head_dim', 'scale', 'is_causal', 'window_left', 'window_right', 'block_size', 'max_context_len', 'softcap', ) def __init__( self, num_heads: int = 28, num_kv_heads: int = 4, head_dim: int = 128, scale: Optional[float] = None, is_causal: bool = True, window_left: int = -1, window_right: int = -1, block_size: int = 16, max_context_len: int = 131072, softcap: float = 0.0, ): self.num_heads = num_heads self.num_kv_heads = num_kv_heads self.head_dim = head_dim self.scale = scale or (head_dim ** -0.5) self.is_causal = is_causal self.window_left = window_left self.window_right = window_right self.block_size = block_size self.max_context_len = max_context_len self.softcap = softcap def should_use_paged_v2( seq_len: int, num_kv_heads: int, block_size: int = 16, partition_size: int = 512, ) -> bool: """ V1 vs V2 decision for paged attention. Upstream: vllm/attention/ops/paged_attn.py PagedAttention._use_v2() Rule: total_tiles = num_kv_heads × ceil(seq_len / partition_size) use V2 when total_tiles > 2 × SM_COUNT (BI-V100: 32) On BI-V100 with 16 SMs and 4 KV heads: V2 when seq_len > 512 × (2 × 16 / 4) = 4096 In practice, V2 is better for seq_len > 1024 due to latency hiding. """ num_tiles = num_kv_heads * ((seq_len + partition_size - 1) // partition_size) return num_tiles > _V2_THRESHOLD_FACTOR * _SM_COUNT def dispatch_prefill( config: AttnDispatchConfig, query: torch.Tensor, # (total_q_tokens, num_heads, head_dim) key_cache: torch.Tensor, # (num_blocks, num_kv_heads, block_size, head_dim) value_cache: torch.Tensor, # (num_blocks, num_kv_heads, block_size, head_dim) block_tables: torch.Tensor, # (batch_size, max_blocks_per_seq) cu_seq_q: torch.Tensor, # (batch_size + 1,) int32 — query cumulative lengths cu_seq_k: torch.Tensor, # (batch_size + 1,) int32 — key cumulative lengths max_seq_q: int, max_seq_k: int, ) -> torch.Tensor: """ Prefill attention via flash attention with block tables. Upstream: xllm::kernel::ilu::batch_prefill → ixformer::infer::ixinfer_flash_attn_unpad_with_block_tables The BI-V100 ixformer implements this as a modified flash attention that reads KV from paged cache (block_tables → physical blocks). """ # Try ix_ops_dispatch first try: from ex_engine.python import ix_ops_dispatch output = ix_ops_dispatch.flash_attn_with_block_tables( query, key_cache, value_cache, block_tables, cu_seq_q, cu_seq_k, max_seq_q, max_seq_k, config.scale, is_causal=config.is_causal, window_left=config.window_left, window_right=config.window_right, softcap=config.softcap, ) return output except (ImportError, RuntimeError, AttributeError) as e: logger.debug("flash_attn dispatch failed, using fallback: %s", e) # Try direct ixformer try: import ixformer.functions as ixf_F output = torch.empty_like(query) ixf_F.ixinfer_flash_attn_unpad_with_block_tables( query, key_cache, value_cache, output, block_tables, cu_seq_q, cu_seq_k, max_seq_q, max_seq_k, config.is_causal, config.window_left, config.window_right, config.scale, config.softcap, False, None, None, None) return output except (ImportError, AttributeError): pass # PyTorch fallback — SDPA (no block table support, for testing only) logger.warning("prefill: using PyTorch SDPA fallback (no block tables)") output = torch.nn.functional.scaled_dot_product_attention( query.unsqueeze(0), query.unsqueeze(0), query.unsqueeze(0), scale=config.scale, is_causal=config.is_causal) return output.squeeze(0) def dispatch_decode( config: AttnDispatchConfig, output: torch.Tensor, # (batch_size, num_heads, head_dim) preallocated query: torch.Tensor, # (batch_size, num_heads, head_dim) key_cache: torch.Tensor, # (num_blocks, num_kv_heads, block_size, head_dim) value_cache: torch.Tensor, # (num_blocks, num_kv_heads, block_size, head_dim) block_tables: torch.Tensor, # (batch_size, max_blocks_per_seq) context_lens: torch.Tensor, # (batch_size,) int32 max_context_len: int, ) -> None: """ Decode attention via paged attention v1/v2. Upstream: xllm::kernel::ilu::batch_decode → ixformer::infer::xllm_paged_attention """ # Try ix_ops_dispatch try: from ex_engine.python import ix_ops_dispatch ix_ops_dispatch.paged_attention_v1( output, query, key_cache, value_cache, config.num_kv_heads, config.scale, block_tables, context_lens, config.block_size, max_context_len, window_left=config.window_left, window_right=config.window_right, softcap=config.softcap, ) return except (ImportError, RuntimeError, AttributeError) as e: logger.debug("paged_attention dispatch failed: %s", e) # Try direct ixformer try: import ixformer.functions as ixf_F ixf_F.vllm_single_query_cached_kv_attention( output, query, key_cache, value_cache, config.num_kv_heads, config.scale, block_tables, context_lens, config.block_size, max_context_len, None) return except (ImportError, AttributeError): pass # PyTorch fallback — extremely slow, decode-only test path logger.warning("decode: using PyTorch fallback (very slow)") batch_size = query.shape[0] for b in range(batch_size): ctx_len = context_lens[b].item() q = query[b] # (num_heads, head_dim) # Reconstruct KV from cache blocks = block_tables[b] num_blocks_used = (ctx_len + config.block_size - 1) // config.block_size k_list, v_list = [], [] for bi in range(num_blocks_used): block_idx = blocks[bi].item() tokens_in_block = min(config.block_size, ctx_len - bi * config.block_size) k_list.append(key_cache[block_idx, :, :tokens_in_block]) v_list.append(value_cache[block_idx, :, :tokens_in_block]) k = torch.cat(k_list, dim=1) # (kv_heads, ctx_len, head_dim) v = torch.cat(v_list, dim=1) # GQA: expand kv heads num_q_per_kv = config.num_heads // config.num_kv_heads k = k.repeat_interleave(num_q_per_kv, dim=0) v = v.repeat_interleave(num_q_per_kv, dim=0) # Standard attention scores = torch.einsum('hd,hsd->hs', q.float(), k.float()) scores = scores * config.scale scores = torch.softmax(scores, dim=-1) out = torch.einsum('hs,hsd->hd', scores, v.float()) output[b] = out.to(output.dtype) def dispatch_attention( config: AttnDispatchConfig, is_prefill: bool, query: torch.Tensor, key_cache: torch.Tensor, value_cache: torch.Tensor, block_tables: torch.Tensor, # Prefill-specific cu_seq_q: Optional[torch.Tensor] = None, cu_seq_k: Optional[torch.Tensor] = None, max_seq_q: int = 0, max_seq_k: int = 0, # Decode-specific context_lens: Optional[torch.Tensor] = None, max_context_len: int = 0, ) -> torch.Tensor: """ Top-level attention dispatcher. Mirrors xllm's split between batch_prefill and batch_decode, routing to the correct kernel based on is_prefill flag. """ if is_prefill: return dispatch_prefill( config, query, key_cache, value_cache, block_tables, cu_seq_q, cu_seq_k, max_seq_q, max_seq_k) else: output = torch.empty_like(query) dispatch_decode( config, output, query, key_cache, value_cache, block_tables, context_lens, max_context_len) return output