diff --git a/mojo_opset/backends/ttx/kernels/__init__.py b/mojo_opset/backends/ttx/kernels/__init__.py index 2c9f8f16..5456c5c7 100644 --- a/mojo_opset/backends/ttx/kernels/__init__.py +++ b/mojo_opset/backends/ttx/kernels/__init__.py @@ -36,6 +36,14 @@ def _not_impl(*args, **kwargs): dynamic_quant_impl = _get_kernel_impl(ttx_backend_module, "dynamic_quant_impl") lightning_indexer_impl = _get_kernel_impl(ttx_backend_module, "lightning_indexer_impl") +conformer_sliding_window_attention_impl = _get_kernel_impl( + ttx_backend_module, "conformer_sliding_window_attention_impl" +) +conformer_chunk_attention_impl = _get_kernel_impl(ttx_backend_module, "conformer_chunk_attention_impl") +prepare_conformer_chunk_attention_q_block_indices = _get_kernel_impl( + ttx_backend_module, + "prepare_conformer_chunk_attention_q_block_indices", +) rot_pos_embed_impl = _get_kernel_impl(ttx_backend_module, "rot_pos_embed_impl") rope_fwd_impl = _get_kernel_impl(ttx_backend_module, "rope_fwd_impl") @@ -304,7 +312,9 @@ def rot_pos_embed_fake( else: seq_dim = x.shape[0] rope_dim = cos.shape[-1] - return torch.empty((seq_dim, rope_dim), device=x.device, dtype=torch.float32), torch.empty((seq_dim, rope_dim), device=x.device, dtype=torch.float32) + return torch.empty((seq_dim, rope_dim), device=x.device, dtype=torch.float32), torch.empty( + (seq_dim, rope_dim), device=x.device, dtype=torch.float32 + ) @torch.library.custom_op("ttx::rope", mutates_args={}) def rope_fwd( @@ -808,6 +818,8 @@ def store_paged_kv_fake( swa_paged_prefill = swa_paged_prefill_impl swa_paged_decode = swa_paged_decode_impl swa_infer = swa_infer_impl + conformer_sliding_window_attention = conformer_sliding_window_attention_impl + conformer_chunk_attention = conformer_chunk_attention_impl swa_fwd = swa_fwd_impl swa_bwd = swa_bwd_impl group_rmsnorm = group_rmsnorm_impl @@ -867,6 +879,8 @@ def store_paged_kv_fake( top_k_sampling = top_k_sampling_impl dynamic_quant = dynamic_quant_impl lightning_indexer = lightning_indexer_impl + conformer_sliding_window_attention = conformer_sliding_window_attention_impl + conformer_chunk_attention = conformer_chunk_attention_impl group_rmsnorm = group_rmsnorm_impl embedding_nf4_dequant = embedding_nf4_dequant_impl n_gram_decode = n_gram_decode_impl diff --git a/mojo_opset/backends/ttx/kernels/npu/__init__.py b/mojo_opset/backends/ttx/kernels/npu/__init__.py index 4605cc00..a7de6cae 100755 --- a/mojo_opset/backends/ttx/kernels/npu/__init__.py +++ b/mojo_opset/backends/ttx/kernels/npu/__init__.py @@ -20,6 +20,9 @@ from .layernorm import layernorm_fwd_impl from .layernorm import layernorm_infer_impl from .lightning_indexer import lightning_indexer_impl +from .conformer_chunk_attention import conformer_chunk_attention_impl +from .conformer_chunk_attention import prepare_conformer_chunk_attention_q_block_indices +from .conformer_sliding_window_attention import conformer_sliding_window_attention_impl from .quant import dynamic_quant_impl from .rmsnorm import rmsnorm_bwd_impl from .rmsnorm import rmsnorm_fwd_impl @@ -84,6 +87,9 @@ "sdpa_fwd_impl", "sdpa_bwd_impl", "lightning_indexer_impl", + "conformer_chunk_attention_impl", + "prepare_conformer_chunk_attention_q_block_indices", + "conformer_sliding_window_attention_impl", "dynamic_quant_impl", "diffusion_attention_fwd_impl", "diffusion_attention_bwd_impl", diff --git a/mojo_opset/backends/ttx/kernels/npu/conformer_chunk_attention.py b/mojo_opset/backends/ttx/kernels/npu/conformer_chunk_attention.py new file mode 100644 index 00000000..206c3e8a --- /dev/null +++ b/mojo_opset/backends/ttx/kernels/npu/conformer_chunk_attention.py @@ -0,0 +1,301 @@ +import math +import os + +from typing import Optional + +import torch +import triton +import triton.language as tl + +from mojo_opset.backends.ttx.kernels.utils import prepare_chunk_indices + +from .utils import get_num_cores + + +@triton.jit +def _conformer_chunk_attention_kernel( + o_ptr, + q_ptr, + k_ptr, + v_ptr, + cu_q_lens_ptr, + cu_total_seq_lens_ptr, + q_block_indices_ptr, + scale, + chunk_size: tl.constexpr, + left_context_chunks: tl.constexpr, + stride_ot, + stride_oh, + stride_od, + stride_qt, + stride_qh, + stride_qd, + stride_kt, + stride_kh, + stride_kd, + stride_vt, + stride_vh, + stride_vd, + total_q_blocks, + num_heads: tl.constexpr, + head_dim: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_D: tl.constexpr, +): + tl.static_assert(head_dim <= BLOCK_D, "BLOCK_D must cover head_dim") + + pid = tl.program_id(0) + n_programs = tl.num_programs(0) + + total_tasks = total_q_blocks * num_heads + for task_id in range(pid, total_tasks, n_programs): + block_task_id = task_id // num_heads + b_id = tl.load(q_block_indices_ptr + block_task_id * 2).to(tl.int32) + q_block_id = tl.load(q_block_indices_ptr + block_task_id * 2 + 1).to(tl.int32) + h_id = task_id % num_heads + + q_start = tl.load(cu_q_lens_ptr + b_id).to(tl.int32) + q_end = tl.load(cu_q_lens_ptr + b_id + 1).to(tl.int32) + kv_start = tl.load(cu_total_seq_lens_ptr + b_id).to(tl.int32) + kv_end = tl.load(cu_total_seq_lens_ptr + b_id + 1).to(tl.int32) + q_seq_len = q_end - q_start + kv_seq_len = kv_end - kv_start + kv_computed_len = kv_seq_len - q_seq_len + + q_block_start = q_block_id * BLOCK_M + q_offs = q_block_start + tl.arange(0, BLOCK_M) + q_valid = q_offs < q_seq_len + q_block_abs_start = kv_computed_len + q_block_start + + q_block_ptr = tl.make_block_ptr( + base=q_ptr + q_start * stride_qt + h_id * stride_qh, + shape=(q_seq_len, head_dim), + strides=(stride_qt, stride_qd), + offsets=(q_block_start, 0), + block_shape=(BLOCK_M, BLOCK_D), + order=(1, 0), + ) + q = tl.load(q_block_ptr, boundary_check=(0, 1), padding_option="zero") + + m_i = tl.full((BLOCK_M,), -float("inf"), dtype=tl.float32) + l_i = tl.zeros((BLOCK_M,), dtype=tl.float32) + acc = tl.zeros((BLOCK_M, head_dim), dtype=tl.float32) + + # Compute KV window for this query block + # ctx_start = max(0, chunk_start - left_context_chunks * chunk_size) + # chunk_end = min(chunk_start + chunk_size, kv_seq_len) + q_block_abs_last_actual = tl.minimum(q_block_abs_start + BLOCK_M - 1, kv_seq_len - 1) + chunk_start_first = (q_block_abs_start // chunk_size) * chunk_size + chunk_start_last = (q_block_abs_last_actual // chunk_size) * chunk_size + + if left_context_chunks < 0: + kv_win_start = 0 + else: + kv_win_start = tl.maximum(0, chunk_start_first - left_context_chunks * chunk_size) + + kv_win_end = tl.minimum(chunk_start_last + chunk_size, kv_seq_len) + kv_block_start_id = kv_win_start // BLOCK_N + kv_block_end_id = tl.cdiv(kv_win_end, BLOCK_N) + + # Precompute fp32 query positions and chunk boundaries for mask + q_abs_f32 = (kv_computed_len + q_offs).to(tl.float32) + q_abs_i32 = kv_computed_len + q_offs + chunk_start_i32 = (q_abs_i32 // chunk_size) * chunk_size + chunk_end_f32 = tl.minimum((chunk_start_i32 + chunk_size).to(tl.float32), kv_seq_len.to(tl.float32)) + if left_context_chunks < 0: + ctx_start_f32 = tl.zeros((BLOCK_M,), dtype=tl.float32) + else: + ctx_start_f32 = tl.maximum((chunk_start_i32 - left_context_chunks * chunk_size).to(tl.float32), 0.0) + + for kv_block_id in range(kv_block_start_id, kv_block_end_id): + kv_block_start = kv_block_id * BLOCK_N + kv_offsets = kv_block_start + tl.arange(0, BLOCK_N) + kv_valid = kv_offsets < kv_seq_len + + k_t_block_ptr = tl.make_block_ptr( + base=k_ptr + kv_start * stride_kt + h_id * stride_kh, + shape=(head_dim, kv_seq_len), + strides=(stride_kd, stride_kt), + offsets=(0, kv_block_start), + block_shape=(BLOCK_D, BLOCK_N), + order=(0, 1), + ) + v_block_ptr = tl.make_block_ptr( + base=v_ptr + kv_start * stride_vt + h_id * stride_vh, + shape=(kv_seq_len, head_dim), + strides=(stride_vt, stride_vd), + offsets=(kv_block_start, 0), + block_shape=(BLOCK_N, BLOCK_D), + order=(1, 0), + ) + + k_t = tl.load(k_t_block_ptr, boundary_check=(0, 1), padding_option="zero") + v = tl.load(v_block_ptr, boundary_check=(0, 1), padding_option="zero") + + # QK^T + qk = tl.dot(q, k_t) * scale + + # Chunk mask: kv_idx ∈ [ctx_start[q], chunk_end[q]) + kv_f32 = kv_offsets.to(tl.float32) + chunk_mask = (kv_f32[None, :] >= ctx_start_f32[:, None]) & (kv_f32[None, :] < chunk_end_f32[:, None]) + + pre_mask = q_valid[:, None] & kv_valid[None, :] & chunk_mask + + qk = tl.where(pre_mask, qk, float("-inf")) + # m_candidate = tl.max(qk, axis=1) + # m_new = tl.maximum(m_i, m_candidate) + m_candidate = tl.max(qk, 1, propagate_nan=tl.PropagateNan.ALL) + m_new = tl.maximum(m_i, m_candidate, propagate_nan=tl.PropagateNan.ALL) + + if left_context_chunks < 0: + qk = qk - m_new[:, None] + m_delta = m_i - m_new + + else: + has_valid = m_candidate > float("-inf") + qk = qk - tl.where(has_valid, m_new, 0.0)[:, None] + + # alpha rescales old accumulator: exp(m_i - m_new) if m_new > m_i, else 1. + # When m_i == -inf (first valid block): alpha = 0 (no prior accumulator). + m_delta = tl.where(has_valid, m_i - m_new, 0.0) + + alpha = tl.exp(m_delta) + + qk = tl.exp(qk) + l_ij = tl.sum(qk, axis=1) + + acc = tl.dot(qk.to(k_t.dtype), v, acc * alpha[:, None]) + l_i = l_i * alpha + l_ij + + m_i = m_new + + out = acc / l_i[:, None] + # out = acc / tl.where(q_valid, l_i, 1.0)[:, None] + # out = tl.where(q_valid[:, None], out, 0.0) + o_block_ptr = tl.make_block_ptr( + base=o_ptr + q_start * stride_ot + h_id * stride_oh, + shape=(q_seq_len, head_dim), + strides=(stride_ot, stride_od), + offsets=(q_block_start, 0), + block_shape=(BLOCK_M, BLOCK_D), + order=(1, 0), + ) + tl.store(o_block_ptr, out.to(o_ptr.type.element_ty), boundary_check=(0, 1)) + + +def _select_blocks(head_dim: int, dtype: torch.dtype) -> tuple[int, int, int, bool]: + """Heuristic tile selection for Ascend 910B2C with 192 KB UB. + + Empirically verified UB-safe tile sizes (bm/bn combos tested with TRITON_DEBUG=1): + D=128: BM=128 any BN → UB overflow (~205KB needed). BM=96 all BN ≤ 64 pass. + D=96: BM=96, BN=96 passes. BM=64, BN=64 passes. + D=64: BM=128, BN=64 passes. BM=128, BN≥96 overflows. + + multibuffer=False at launch since cbuf workspace-multibuffer still doubles + NZ allocation overhead. AI = 2*BM*BN/(BM+2*BN). + Ascend 910B2C roofline knee ≈ 197 FLOP/byte. + """ + block_m_override = os.getenv("MOJO_CHUNK_ATTN_BLOCK_M") + block_n_override = os.getenv("MOJO_CHUNK_ATTN_BLOCK_N") + + if block_m_override is not None or block_n_override is not None: + block_m = int(block_m_override) if block_m_override is not None else 128 + block_n = int(block_n_override) if block_n_override is not None else 64 + return block_m, block_n, head_dim, False + + if dtype == torch.float32: + return 64, 64, head_dim, False + + if head_dim >= 128: + # D≥128: BM=96, BN=64, AI=2*96*64/(96+128)≈55 + return 128, 128, head_dim, True + elif head_dim >= 96: + # 96≤D<128: BM=64, BN=64, AI=2*64*64/(64+128)≈43 + return 64, 64, head_dim, False + else: + # D<96: BM=128, BN=64, AI=2*128*64/(128+128)=64 + return 128, 64, head_dim, False + + +def prepare_conformer_chunk_attention_q_block_indices( + cu_q_lens: torch.Tensor, + head_dim: int, + dtype: torch.dtype, +) -> torch.Tensor: + block_m, _, _, _ = _select_blocks(head_dim, dtype) + return prepare_chunk_indices(cu_q_lens, block_m).to(torch.int32) + + +def conformer_chunk_attention_impl( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + cu_q_lens: torch.Tensor, + cu_total_seq_lens: Optional[torch.Tensor], + chunk_size: int, + left_context_chunks: int, + softmax_scale: Optional[float] = None, + q_block_indices: Optional[torch.Tensor] = None, +) -> torch.Tensor: + if q.ndim != 3 or k.ndim != 3 or v.ndim != 3: + raise ValueError("q/k/v must be [T, H, D]") + if k.shape != v.shape or q.shape[1:] != k.shape[1:]: + raise ValueError(f"q/k/v shape mismatch: {q.shape}, {k.shape}, {v.shape}") + if cu_total_seq_lens is None: + cu_total_seq_lens = cu_q_lens + + _, num_heads, head_dim = q.shape + if softmax_scale is None: + softmax_scale = 1.0 / math.sqrt(head_dim) + + block_m, block_n, block_d, enable_multibuf = _select_blocks(head_dim, q.dtype) + if q_block_indices is None: + raise ValueError("q_block_indices must be prepared before conformer_chunk_attention_impl") + if q_block_indices.ndim != 2 or q_block_indices.shape[1] != 2: + raise ValueError(f"q_block_indices must have shape [num_q_blocks, 2], got {q_block_indices.shape}") + if q_block_indices.dtype != torch.int32: + raise ValueError(f"q_block_indices must be int32, got {q_block_indices.dtype}") + total_q_blocks = q_block_indices.shape[0] + + o = torch.zeros_like(q, memory_format=torch.contiguous_format) + grid = (min(total_q_blocks * num_heads, get_num_cores("cube")),) + + _conformer_chunk_attention_kernel[grid]( + o, + q, + k, + v, + cu_q_lens, + cu_total_seq_lens, + q_block_indices, + softmax_scale, + chunk_size, + left_context_chunks, + o.stride(0), + o.stride(1), + o.stride(2), + q.stride(0), + q.stride(1), + q.stride(2), + k.stride(0), + k.stride(1), + k.stride(2), + v.stride(0), + v.stride(1), + v.stride(2), + total_q_blocks, + num_heads, + head_dim, + block_m, + block_n, + block_d, + multibuffer=enable_multibuf, + enable_ubuf_saving=True, + limit_auto_multi_buffer_only_for_local_buffer=False, + limit_auto_multi_buffer_of_local_buffer="no-l0c", + set_workspace_multibuffer=4, + # tile_mix_vector_loop=4, + # tile_mix_cube_loop=4, + ) + return o diff --git a/mojo_opset/backends/ttx/kernels/npu/conformer_sliding_window_attention.py b/mojo_opset/backends/ttx/kernels/npu/conformer_sliding_window_attention.py new file mode 100644 index 00000000..449d3ac8 --- /dev/null +++ b/mojo_opset/backends/ttx/kernels/npu/conformer_sliding_window_attention.py @@ -0,0 +1,341 @@ +import math +import os + +from typing import Optional + +import torch +import triton +import triton.language as tl + +from .swa import get_aux_mask +from .utils import get_num_cores + +_AUX_MASK_SIZE = 256 + + +@triton.jit +def _load_tril_mask( + mask_ptr_tril, + mask_size, + mask_stride_m, + mask_stride_n, + M_BLOCK, + N_BLOCK, + m_start, + n_start, +): + """Load tril mask slice: n + n_start <= m + m_start (i.e. kv_col <= q_row + offset).""" + offset = min(max(n_start - m_start, -mask_size), mask_size) + mask = tl.load( + mask_ptr_tril + + tl.arange(0, M_BLOCK)[:, None] * mask_stride_m + + (offset + tl.arange(0, N_BLOCK))[None, :] * mask_stride_n + ) + return mask.to(tl.int1) + + +@triton.jit +def _load_triu_mask( + mask_ptr_triu, + mask_size, + mask_stride_m, + mask_stride_n, + M_BLOCK, + N_BLOCK, + m_start, + n_start, +): + """Load triu mask slice: n + n_start >= m + m_start (i.e. kv_col >= q_row + offset).""" + offset = min(max(n_start - m_start, -mask_size), mask_size) + mask = tl.load( + mask_ptr_triu + + tl.arange(0, M_BLOCK)[:, None] * mask_stride_m + + (offset + tl.arange(0, N_BLOCK))[None, :] * mask_stride_n + ) + return mask.to(tl.int1) + + +@triton.jit +def _conformer_sliding_window_attention_kernel( + o_ptr, + q_ptr, + k_ptr, + v_ptr, + cu_q_lens_ptr, + cu_total_seq_lens_ptr, + scale, + left_window: tl.constexpr, + right_window: tl.constexpr, + stride_ot, + stride_oh, + stride_od, + stride_qt, + stride_qh, + stride_qd, + stride_kt, + stride_kh, + stride_kd, + stride_vt, + stride_vh, + stride_vd, + aux_mask_ptr, + aux_mask_size, + aux_mask_stride_m, + aux_mask_stride_n, + aux_mask_ptr_triu, + aux_mask_ptr_tril, + batch_size: tl.constexpr, + num_heads: tl.constexpr, + head_dim: tl.constexpr, + BLOCK_M: tl.constexpr, + BLOCK_N: tl.constexpr, + BLOCK_D: tl.constexpr, + USE_MASK_LOOKUP: tl.constexpr, +): + tl.static_assert(head_dim <= BLOCK_D, "BLOCK_D must cover head_dim") + + pid = tl.program_id(0) + n_programs = tl.num_programs(0) + + prev_q_chunks = 0 + for b_id in range(batch_size): + q_start = tl.load(cu_q_lens_ptr + b_id).to(tl.int32) + q_end = tl.load(cu_q_lens_ptr + b_id + 1).to(tl.int32) + kv_start = tl.load(cu_total_seq_lens_ptr + b_id).to(tl.int32) + kv_end = tl.load(cu_total_seq_lens_ptr + b_id + 1).to(tl.int32) + q_seq_len = q_end - q_start + kv_seq_len = kv_end - kv_start + kv_computed_len = kv_seq_len - q_seq_len + + cur_q_chunks = tl.cdiv(q_seq_len, BLOCK_M) + prev_q_tasks = prev_q_chunks * num_heads + cur_q_tasks = cur_q_chunks * num_heads + prev_q_chunks += cur_q_chunks + + for q_task_id in range((prev_q_tasks + pid) % n_programs, cur_q_tasks, n_programs): + q_block_id = q_task_id // num_heads + h_id = q_task_id % num_heads + q_block_start = q_block_id * BLOCK_M + q_offs = q_block_start + tl.arange(0, BLOCK_M) + q_valid = q_offs < q_seq_len + q_block_abs_start = kv_computed_len + q_block_start + + q_block_ptr = tl.make_block_ptr( + base=q_ptr + q_start * stride_qt + h_id * stride_qh, + shape=(q_seq_len, head_dim), + strides=(stride_qt, stride_qd), + offsets=(q_block_start, 0), + block_shape=(BLOCK_M, BLOCK_D), + order=(1, 0), + ) + q = tl.load(q_block_ptr, boundary_check=(0, 1), padding_option="zero") + + m_i = tl.full((BLOCK_M,), -float("inf"), dtype=tl.float32) + l_i = tl.zeros((BLOCK_M,), dtype=tl.float32) + acc = tl.zeros((BLOCK_M, head_dim), dtype=tl.float32) + + q_block_last = tl.minimum(q_block_start + BLOCK_M - 1, q_seq_len - 1) + kv_win_start = tl.maximum(0, kv_computed_len + q_block_start - left_window) + kv_win_end = tl.minimum(kv_seq_len, kv_computed_len + q_block_last + right_window + 1) + kv_block_start_id = kv_win_start // BLOCK_N + kv_block_end_id = tl.cdiv(kv_win_end, BLOCK_N) + + # Precompute fp32 query positions once (shared across all KV blocks) + q_abs_f32 = (kv_computed_len + q_offs).to(tl.float32) + left_bound_f32 = q_abs_f32 - left_window + right_bound_f32 = q_abs_f32 + right_window + + for kv_block_id in range(kv_block_start_id, kv_block_end_id): + kv_block_start = kv_block_id * BLOCK_N + kv_offsets = kv_block_start + tl.arange(0, BLOCK_N) + kv_valid = kv_offsets < kv_seq_len + + k_t_block_ptr = tl.make_block_ptr( + base=k_ptr + kv_start * stride_kt + h_id * stride_kh, + shape=(head_dim, kv_seq_len), + strides=(stride_kd, stride_kt), + offsets=(0, kv_block_start), + block_shape=(BLOCK_D, BLOCK_N), + order=(0, 1), + ) + v_block_ptr = tl.make_block_ptr( + base=v_ptr + kv_start * stride_vt + h_id * stride_vh, + shape=(kv_seq_len, head_dim), + strides=(stride_vt, stride_vd), + offsets=(kv_block_start, 0), + block_shape=(BLOCK_N, BLOCK_D), + order=(1, 0), + ) + + k_t = tl.load(k_t_block_ptr, boundary_check=(0, 1), padding_option="zero") + tl.multibuffer(k_t, 2) + v = tl.load(v_block_ptr, boundary_check=(0, 1), padding_option="zero") + tl.multibuffer(v, 2) + + # QK^T + qk = tl.dot(q, k_t) * scale + + # Window mask: kv_offset ∈ [q_abs - left_window, q_abs + right_window] + if USE_MASK_LOOKUP: + mask_left = _load_triu_mask( + aux_mask_ptr_triu, + aux_mask_size, + aux_mask_stride_m, + aux_mask_stride_n, + BLOCK_M, + BLOCK_N, + q_block_abs_start - left_window, + kv_block_start, + ) + mask_right = _load_tril_mask( + aux_mask_ptr_tril, + aux_mask_size, + aux_mask_stride_m, + aux_mask_stride_n, + BLOCK_M, + BLOCK_N, + q_block_abs_start + right_window, + kv_block_start, + ) + win_mask = mask_left & mask_right + else: + kv_f32 = kv_offsets.to(tl.float32) + win_mask = (kv_f32[None, :] >= left_bound_f32[:, None]) & ( + kv_f32[None, :] <= right_bound_f32[:, None] + ) + + pre_mask = q_valid[:, None] & kv_valid[None, :] & win_mask + + # Online softmax: mask → row-max → rescale → exp → reweight + qk = tl.where(pre_mask, qk, float("-inf")) + m_candidate = tl.max(qk, axis=1) + m_new = tl.where(q_valid, tl.maximum(m_i, m_candidate), m_i) + qk = qk - tl.where(q_valid, m_new, 0.0)[:, None] + qk = tl.where(pre_mask, tl.exp(qk), 0.0) + + l_ij = tl.sum(qk, axis=1) + alpha = tl.where(q_valid, tl.exp(m_i - m_new), 1.0) + # Fused rescale + PV matmul + acc_update = tl.dot(qk.to(k_t.dtype), v, acc * alpha[:, None]) + acc = tl.where(q_valid[:, None], acc_update, acc) + l_i = tl.where(q_valid, l_i * alpha + l_ij, l_i) + m_i = m_new + + out = acc / tl.where(q_valid, l_i, 1.0)[:, None] + out = tl.where(q_valid[:, None], out, 0.0) + o_block_ptr = tl.make_block_ptr( + base=o_ptr + q_start * stride_ot + h_id * stride_oh, + shape=(q_seq_len, head_dim), + strides=(stride_ot, stride_od), + offsets=(q_block_start, 0), + block_shape=(BLOCK_M, BLOCK_D), + order=(1, 0), + ) + tl.store(o_block_ptr, out.to(o_ptr.type.element_ty), boundary_check=(0, 1)) + + +def _select_blocks(head_dim: int, dtype: torch.dtype) -> tuple[int, int, int, bool, bool]: + """Heuristic tile selection for Ascend 910B2C with 192 KB UB. + + UB budget with double-buffered K_t/V: + UB = BM*D*2 + 2*D*BN*2 + 2*BN*D*2 + BM*BN*4 + BM*D*4 + BM*8 + = BM*(6*D + 8) + BN*(8*D) + BM*BN*4 + + Arithmetic intensity (AI) = 2*BM*BN / (BM + 2*BN). + Roofline knee ≈ 197 FLOP/byte for Ascend 910B2C. + """ + block_m_override = os.getenv("MOJO_CONFORMER_ATTN_BLOCK_M") + block_n_override = os.getenv("MOJO_CONFORMER_ATTN_BLOCK_N") + use_mask_lookup = os.getenv("MOJO_CONFORMER_MASK_LOOKUP", "0") == "1" + + if block_m_override is not None or block_n_override is not None: + block_m = int(block_m_override) if block_m_override is not None else 128 + block_n = int(block_n_override) if block_n_override is not None else 64 + return block_m, block_n, head_dim, use_mask_lookup, True + + if dtype == torch.float32: + return 64, 64, head_dim, use_mask_lookup, False + + enable_multibuf = True + + if head_dim >= 128: + # D=128: BM=128, BN=48, UB=173056, AI≈55 + return 128, 32, head_dim, use_mask_lookup, enable_multibuf + elif head_dim >= 96: + # D=96: BM=192, BN=48, UB=185856, AI≈64 + return 64, 64, head_dim, use_mask_lookup, enable_multibuf + else: + # D<=64: BM=128, BN=128, UB=181248, AI≈85 + return 128, 128, head_dim, use_mask_lookup, enable_multibuf + + +def conformer_sliding_window_attention_impl( + q: torch.Tensor, + k: torch.Tensor, + v: torch.Tensor, + cu_q_lens: torch.Tensor, + cu_total_seq_lens: Optional[torch.Tensor], + left_window: int, + right_window: int, + softmax_scale: Optional[float] = None, +) -> torch.Tensor: + if q.ndim != 3 or k.ndim != 3 or v.ndim != 3: + raise ValueError("q/k/v must be [T, H, D]") + if k.shape != v.shape or q.shape[1:] != k.shape[1:]: + raise ValueError(f"q/k/v shape mismatch: {q.shape}, {k.shape}, {v.shape}") + if cu_total_seq_lens is None: + cu_total_seq_lens = cu_q_lens + + _, num_heads, head_dim = q.shape + if softmax_scale is None: + softmax_scale = 1.0 / math.sqrt(head_dim) + + block_m, block_n, block_d, use_mask_lookup, enable_multibuf = _select_blocks(head_dim, q.dtype) + + aux_mask_size, aux_mask = get_aux_mask() + aux_mask_stride_m = aux_mask.stride(0) + aux_mask_stride_n = aux_mask.stride(1) + aux_mask_ptr_triu = aux_mask.data_ptr() + aux_mask_size * aux_mask_stride_n + aux_mask_ptr_tril = aux_mask.data_ptr() + 3 * aux_mask_size * aux_mask_stride_n + + o = torch.zeros_like(q, memory_format=torch.contiguous_format) + grid = (get_num_cores("cube"),) + + _conformer_sliding_window_attention_kernel[grid]( + o, + q, + k, + v, + cu_q_lens, + cu_total_seq_lens, + softmax_scale, + left_window, + right_window, + o.stride(0), + o.stride(1), + o.stride(2), + q.stride(0), + q.stride(1), + q.stride(2), + k.stride(0), + k.stride(1), + k.stride(2), + v.stride(0), + v.stride(1), + v.stride(2), + aux_mask, + aux_mask_size, + aux_mask_stride_m, + aux_mask_stride_n, + aux_mask_ptr_triu, + aux_mask_ptr_tril, + cu_q_lens.shape[0] - 1, + num_heads, + head_dim, + block_m, + block_n, + block_d, + USE_MASK_LOOKUP=use_mask_lookup, + multibuffer=enable_multibuf, + ) + return o diff --git a/mojo_opset/backends/ttx/kernels/npu/triton-npu-kernel-opt/guides/tuning/optimization-log.md b/mojo_opset/backends/ttx/kernels/npu/triton-npu-kernel-opt/guides/tuning/optimization-log.md index 73d945ae..25b227be 100644 --- a/mojo_opset/backends/ttx/kernels/npu/triton-npu-kernel-opt/guides/tuning/optimization-log.md +++ b/mojo_opset/backends/ttx/kernels/npu/triton-npu-kernel-opt/guides/tuning/optimization-log.md @@ -66,7 +66,7 @@ Chronological record of optimization experiments on the ttx (Triton NPU) backend **Technique:** NZ (FRACTAL_NZ) format for B matrix in Triton **Result:** REJECTED for Triton (UB overflow / 50x slowdown). +16% for npu_quant_matmul. -**Action:** Added as OPT-12 (for native operators only). Detailed in [ascend-910b-gemm.md](ascend-910b-gemm.md). +**Action:** Added as OPT-12 (for native operators only). Detailed in [ascend-910b-gemm.md](ascend-910b-gemm.md). ### 2026-03-26 | Kernel: INT8 GEMM | SoC: Ascend 910B2C @@ -78,8 +78,40 @@ Chronological record of optimization experiments on the ttx (Triton NPU) backend **Technique:** Full M/N/K sweep (99 configs: M=1~8192, N/K=2048~8192) **Result:** Data used to build select_config() heuristic. Peak: 620T (121% QMM_ND). -**Action:** Tuning table in [ascend-910b-gemm.md](ascend-910b-gemm.md) +**Action:** Tuning table in [ascend-910b-gemm.md](ascend-910b-gemm.md) --- + +### 2026-05-08 | Kernel: Conformer Attention Varlen | SoC: Ascend 910B2C, 24 AICore + +**Technique:** Replaced dense BSHD + padding-mask path with a THD varlen encoder API. The kernel uses cumulative query/KV lengths and direct window masking by absolute query position. +**Result:** Accuracy passed for bf16/float32, cache/no-cache encoder cases, and uneven lengths. Perf profiler device latency: 64.9472 us / 173.7712 us / 108.5136 us / 23.5040 us for the four perf cases. Effective throughput by visible-window FLOPs peaked at 9.641 TFLOP/s; normalized to a 354 TFLOP/s bf16 peak, current best observed MFU is about 2.72%. +**Action:** Added `MojoConformerSlidingWindowAttention`, unified TTX varlen backend, and updated accuracy/perf tests. + +--- + +### 2026-05-09 | Kernel: Conformer Attention Varlen | SoC: Ascend 910B2C, 24 AICore + +**Technique:** OPT-MB — Enable multibuffering (`multibuffer=True` + `tl.multibuffer(k_t, 2)` + `tl.multibuffer(v, 2)`) on the persistent conformer attention kernel. Overlaps HBM→UB DMA transfers with Cube dot-product compute in the inner KV-block loop. +**Result:** Expected +15-25% throughput from DMA/compute overlap. + +**Technique:** OPT-TILE — Rebalanced tile sizes for double-buffered UB budget (192 KB). D=64: BM=128, BN=128 (AI=85, UB=181KB). D=128: BM=128, BN=48 (AI=55, UB=173KB). D=96: BM=192, BN=48 (AI=64, UB=186KB). fp32: BM=64, BN=64 (no multibuffer). +**Result:** Larger BN (from 64→128 for D=64, from 32→48 for D=128) reduces KV-loop iterations and increases AI. + +**Technique:** OPT-MASK — Precompute fp32 query positions (`q_abs_f32`, `left_bound_f32`, `right_bound_f32`) once outside the KV-block loop. Eliminates repeated i32→fp32 casts and arithmetic per KV block. +**Result:** Reduces per-KV-block vector operations. + +**Technique:** OPT-MASK-LOOKUP (optional, env-controlled) — Pre-computed auxiliary mask lookup using triu/tril slices (SWA pattern) as an alternative to inline fp32 comparisons. Controlled by `MOJO_CONFORMER_MASK_LOOKUP=1`. +**Result:** Experimental path; fp32 comparison path (default) already avoids int32 scalarization. + +**MFU Formula (from core definition):** +``` +core_FLOPs = 4 * head_dim * num_heads * sum_batch(Q_b * KV_b) // dense Q×KV matmul +MFU = core_FLOPs / (peak_bf16_TFLOPS * device_time_us * 1e-6 * 1e12) +``` +where `peak_bf16_TFLOPS ≈ 320` for Ascend 910B2C (24 AICores, Cube units). +This counts the full dense matmul FLOPs that the core reference operator performs (the kernel's window-skipping is an optimization that reduces actual FLOPs). + +**Action:** Updated `_conformer_sliding_window_attention_kernel` with multibuffering, optimized tiles, hoisted mask precomputation; added optional mask-lookup path. diff --git a/mojo_opset/backends/ttx/operators/attention.py b/mojo_opset/backends/ttx/operators/attention.py index 6a843e9a..23cf150e 100644 --- a/mojo_opset/backends/ttx/operators/attention.py +++ b/mojo_opset/backends/ttx/operators/attention.py @@ -2,17 +2,22 @@ import torch -from mojo_opset.backends.ttx.kernels import paged_attention_prefill +from mojo_opset.backends.ttx.kernels import conformer_chunk_attention +from mojo_opset.backends.ttx.kernels import conformer_sliding_window_attention from mojo_opset.backends.ttx.kernels import paged_attention_decode +from mojo_opset.backends.ttx.kernels import paged_attention_prefill +from mojo_opset.backends.ttx.kernels import prepare_conformer_chunk_attention_q_block_indices from mojo_opset.backends.ttx.kernels import sdpa_infer -from mojo_opset.backends.ttx.kernels import swa_paged_prefill -from mojo_opset.backends.ttx.kernels import swa_paged_decode from mojo_opset.backends.ttx.kernels import swa_infer -from mojo_opset.core import MojoPagedPrefillGQA +from mojo_opset.backends.ttx.kernels import swa_paged_decode +from mojo_opset.backends.ttx.kernels import swa_paged_prefill +from mojo_opset.core import MojoConformerChunkAttention +from mojo_opset.core import MojoConformerSlidingWindowAttention from mojo_opset.core import MojoPagedDecodeGQA -from mojo_opset.core import MojoSdpa -from mojo_opset.core import MojoPagedPrefillSWA from mojo_opset.core import MojoPagedDecodeSWA +from mojo_opset.core import MojoPagedPrefillGQA +from mojo_opset.core import MojoPagedPrefillSWA +from mojo_opset.core import MojoSdpa from mojo_opset.core import MojoSWA from mojo_opset.core.operators.attention import assert_paged_decode_contract from mojo_opset.core.operators.attention import assert_paged_prefill_contract @@ -130,6 +135,70 @@ def forward( ) return output + +class TTXConformerSlidingWindowAttention(MojoConformerSlidingWindowAttention): + supported_platforms_list = ["npu"] + + def forward( + self, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + cu_q_lens: torch.Tensor, + cu_total_seq_lens: Optional[torch.Tensor] = None, + ): + if cu_q_lens.dtype != torch.int32: + raise ValueError(f"cu_q_lens must be int32, got {cu_q_lens.dtype}") + if cu_total_seq_lens is not None and cu_total_seq_lens.dtype != torch.int32: + raise ValueError(f"cu_total_seq_lens must be int32, got {cu_total_seq_lens.dtype}") + return conformer_sliding_window_attention( + query, + key, + value, + cu_q_lens, + cu_total_seq_lens, + self.left_window, + self.right_window, + self.softmax_scale, + ) + + +class TTXConformerChunkAttention(MojoConformerChunkAttention): + supported_platforms_list = ["npu"] + + def forward( + self, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + cu_q_lens: torch.Tensor, + cu_total_seq_lens: Optional[torch.Tensor] = None, + *, + q_block_indices: Optional[torch.Tensor] = None, + ): + if cu_q_lens.dtype != torch.int32: + raise ValueError(f"cu_q_lens must be int32, got {cu_q_lens.dtype}") + if cu_total_seq_lens is not None and cu_total_seq_lens.dtype != torch.int32: + raise ValueError(f"cu_total_seq_lens must be int32, got {cu_total_seq_lens.dtype}") + if q_block_indices is None: + q_block_indices = prepare_conformer_chunk_attention_q_block_indices( + cu_q_lens, query.shape[-1], query.dtype + ) + elif q_block_indices.dtype != torch.int32: + raise ValueError(f"q_block_indices must be int32, got {q_block_indices.dtype}") + return conformer_chunk_attention( + query, + key, + value, + cu_q_lens, + cu_total_seq_lens, + self.chunk_size, + self.left_context_chunks, + self.softmax_scale, + q_block_indices=q_block_indices, + ) + + class TTXPagedPrefillSWA(MojoPagedPrefillSWA): supported_platforms_list = ["npu", "mlu", "ilu"] diff --git a/mojo_opset/core/__init__.py b/mojo_opset/core/__init__.py index b3534842..347ad56b 100644 --- a/mojo_opset/core/__init__.py +++ b/mojo_opset/core/__init__.py @@ -16,26 +16,28 @@ from .operators.activation import MojoSwiGLU """ attention """ +from .operators.attention import MojoConformerChunkAttention +from .operators.attention import MojoConformerSlidingWindowAttention from .operators.attention import MojoDecodeGQA from .operators.attention import MojoDecodeMLA from .operators.attention import MojoDecodeNSA from .operators.attention import MojoPagedDecodeGQA from .operators.attention import MojoPagedDecodeMLA from .operators.attention import MojoPagedDecodeNSA +from .operators.attention import MojoPagedDecodeQuantGQA +from .operators.attention import MojoPagedDecodeQuantSWA from .operators.attention import MojoPagedDecodeSWA from .operators.attention import MojoPagedPrefillGQA from .operators.attention import MojoPagedPrefillMLA from .operators.attention import MojoPagedPrefillNSA +from .operators.attention import MojoPagedPrefillQuantGQA +from .operators.attention import MojoPagedPrefillQuantSWA from .operators.attention import MojoPagedPrefillSWA from .operators.attention import MojoPrefillGQA from .operators.attention import MojoPrefillMLA from .operators.attention import MojoPrefillNSA from .operators.attention import MojoSdpa from .operators.attention import MojoSWA -from .operators.attention import MojoPagedDecodeQuantGQA -from .operators.attention import MojoPagedPrefillQuantGQA -from .operators.attention import MojoPagedDecodeQuantSWA -from .operators.attention import MojoPagedPrefillQuantSWA """ kvcache """ from .operators.kv_cache import MojoStoreMLAKVCache @@ -44,12 +46,12 @@ """ gemm """ from .operators.gemm import MojoGemm -from .operators.gemm import MojoQuantGemm from .operators.gemm import MojoGroupGemm +from .operators.gemm import MojoQuantGemm """ compute + comm """ -from .operators.compute_with_comm import MojoGemmAll2All from .operators.compute_with_comm import MojoAllGatherGemm +from .operators.compute_with_comm import MojoGemmAll2All from .operators.compute_with_comm import MojoGemmAllReduce from .operators.compute_with_comm import MojoGemmReduceScatter @@ -94,11 +96,11 @@ from .operators.normalization import MojoRMSNormQuant """ position_embedding """ -from .operators.position_embedding import MojoRelativeEmbedding from .operators.position_embedding import MojoApplyRoPE from .operators.position_embedding import MojoGridRoPE from .operators.position_embedding import MojoNormRoPE from .operators.position_embedding import MojoNormRoPEStoreKV +from .operators.position_embedding import MojoRelativeEmbedding from .operators.position_embedding import MojoRoPEStoreKV from .operators.position_embedding import MojoRotaryEmbedding @@ -112,6 +114,7 @@ """ convolution""" from .operators.convolution import MojoCausalConv1dUpdateState +from .operators.convolution import MojoConv1d """ mlp""" from .operators.mlp import MojoSwiGLUMLP @@ -151,6 +154,8 @@ "MojoDecodeNSA", "MojoPagedDecodeNSA", "MojoSdpa", + "MojoConformerSlidingWindowAttention", + "MojoConformerChunkAttention", "MojoPagedPrefillSWA", "MojoPagedDecodeSWA", "MojoSWA", @@ -221,6 +226,7 @@ "MojoTopPFilter", "MojoCausalConv1dUpdateState", + "MojoConv1d", "MojoSwiGLUMLP", diff --git a/mojo_opset/core/operators/attention.py b/mojo_opset/core/operators/attention.py index 938a5e58..a5360744 100644 --- a/mojo_opset/core/operators/attention.py +++ b/mojo_opset/core/operators/attention.py @@ -87,9 +87,9 @@ def forward( sl = total_seq_lens[i].item() if total_seq_lens is not None else S if sl <= 0: continue - q_i = query[i] # (Hq, D) - k_i = key[i, :, :sl, :] # (Hkv, sl, D) - v_i = value[i, :, :sl, :] # (Hkv, sl, D) + q_i = query[i] # (Hq, D) + k_i = key[i, :, :sl, :] # (Hkv, sl, D) + v_i = value[i, :, :sl, :] # (Hkv, sl, D) if group > 1: if self.gqa_layout == "AABB": @@ -124,7 +124,7 @@ def __init__( gqa_layout (str, default="ABAB"): GQA head grouping layout; one of {"ABAB", "AABB"}. Raises: - ValueError: If `gqa_layout` is not in {"ABAB", "AABB"} + ValueError: If `gqa_layout` is not in {"ABAB", "AABB"} Notes: This initializer stores configuration only. Actual causal masking and window enforcement @@ -509,9 +509,7 @@ def __init__( self.qk_head_dim = qk_nope_head_dim + qk_rope_head_dim self.use_attn_sink = use_attn_sink - self.kv_b_proj = torch.nn.Parameter( - torch.empty(num_heads * (qk_nope_head_dim + v_head_dim), kv_lora_rank) - ) + self.kv_b_proj = torch.nn.Parameter(torch.empty(num_heads * (qk_nope_head_dim + v_head_dim), kv_lora_rank)) if use_attn_sink: self.attn_sink = _make_attn_sink(num_heads, self.tensor_factory_kwargs) @@ -542,11 +540,9 @@ def forward( softmax_scale = 1.0 / math.sqrt(self.qk_head_dim) # Decompress → (B, S, H, qk_nope + v) - kv = (compressed_kv @ self.kv_b_proj.T).view( - B, S, H, self.qk_nope_head_dim + self.v_head_dim - ) - k_nope = kv[..., : self.qk_nope_head_dim] # (B, S, H, d_nope) - v = kv[..., self.qk_nope_head_dim :] # (B, S, H, d_v) + kv = (compressed_kv @ self.kv_b_proj.T).view(B, S, H, self.qk_nope_head_dim + self.v_head_dim) + k_nope = kv[..., : self.qk_nope_head_dim] # (B, S, H, d_nope) + v = kv[..., self.qk_nope_head_dim :] # (B, S, H, d_v) k = torch.cat([k_nope, k_pe.expand(-1, -1, H, -1)], dim=-1) # (B, S, H, d_qk) scores = torch.einsum("bhd,bshd->bhs", query, k) * softmax_scale @@ -555,9 +551,7 @@ def forward( for i in range(B): scores[i, :, total_seq_lens[i].item() :] = float("-inf") - probs = _attention_probs_with_optional_sink( - scores, query.dtype, getattr(self, "attn_sink", None) - ) + probs = _attention_probs_with_optional_sink(scores, query.dtype, getattr(self, "attn_sink", None)) return torch.einsum("bhs,bshd->bhd", probs, v) def extra_repr(self) -> str: @@ -590,9 +584,7 @@ def __init__( self.qk_head_dim = qk_nope_head_dim + qk_rope_head_dim self.use_attn_sink = use_attn_sink - self.kv_b_proj = torch.nn.Parameter( - torch.empty(num_heads * (qk_nope_head_dim + v_head_dim), kv_lora_rank) - ) + self.kv_b_proj = torch.nn.Parameter(torch.empty(num_heads * (qk_nope_head_dim + v_head_dim), kv_lora_rank)) if use_attn_sink: self.attn_sink = _make_attn_sink(num_heads, self.tensor_factory_kwargs) @@ -643,8 +635,8 @@ def forward( pe_parts.append(k_pe_cache[bid, 0, :tokens]) if not c_parts: continue - c_kv = torch.cat(c_parts, dim=0) # (sl, kv_lora_rank) - k_pe = torch.cat(pe_parts, dim=0) # (sl, qk_rope) + c_kv = torch.cat(c_parts, dim=0) # (sl, kv_lora_rank) + k_pe = torch.cat(pe_parts, dim=0) # (sl, qk_rope) # Decompress kv = (c_kv @ self.kv_b_proj.T).view(sl, H, self.qk_nope_head_dim + self.v_head_dim) @@ -653,9 +645,7 @@ def forward( k = torch.cat([k_nope, k_pe.unsqueeze(1).expand(-1, H, -1)], dim=-1) scores = torch.einsum("hd,shd->hs", query[i], k) * softmax_scale - probs = _attention_probs_with_optional_sink( - scores, query.dtype, getattr(self, "attn_sink", None) - ) + probs = _attention_probs_with_optional_sink(scores, query.dtype, getattr(self, "attn_sink", None)) outputs[i] = torch.einsum("hs,shd->hd", probs, v) return outputs @@ -672,6 +662,7 @@ def extra_repr(self) -> str: # MojoOperator.__init_subclass__ registration. # --------------------------------------------------------------------------- + def _nsa_compress_kv(k, v, compress_ratio): """Average-pool K/V in blocks of ``compress_ratio`` along sequence dim.""" S, H, D = k.shape @@ -681,31 +672,30 @@ def _nsa_compress_kv(k, v, compress_ratio): return k_t, v_t -def _nsa_select_blocks(query, comp_k, sl, softmax_scale, - compress_ratio, block_size, num_selected_blocks): +def _nsa_select_blocks(query, comp_k, sl, softmax_scale, compress_ratio, block_size, num_selected_blocks): """Select top-k blocks by compressed attention score, returning a mask.""" H, D = query.shape C = comp_k.shape[0] - + # 1. Compute softmax probabilities for compressed tokens qk = torch.einsum("hd,chd->hc", query, comp_k) * softmax_scale - qk = qk.softmax(dim=-1, dtype=torch.float32) # [H, C] - + qk = qk.softmax(dim=-1, dtype=torch.float32) # [H, C] + # 2. Aggregate into blocks of size `block_size` tokens_per_block = block_size // compress_ratio num_blocks = math.ceil(sl / block_size) - + block_score = torch.zeros(H, num_blocks, dtype=torch.float32, device=query.device) for b in range(num_blocks): start_c = b * tokens_per_block end_c = min((b + 1) * tokens_per_block, C) if start_c < C: block_score[:, b] = qk[:, start_c:end_c].sum(dim=-1) - + # 3. Select topk blocks per head num_sel = min(num_selected_blocks, num_blocks) - topk_idx = block_score.topk(num_sel, dim=-1).indices # [H, num_sel] - + topk_idx = block_score.topk(num_sel, dim=-1).indices # [H, num_sel] + # 4. Create mask mask = torch.zeros(H, sl, dtype=torch.bool, device=query.device) for h in range(H): @@ -713,7 +703,7 @@ def _nsa_select_blocks(query, comp_k, sl, softmax_scale, start = b.item() * block_size end = min(start + block_size, sl) mask[h, start:end] = True - + return mask @@ -738,8 +728,9 @@ def _nsa_gate(query, gate_proj): return torch.sigmoid(torch.einsum("...hd,hdc->...hc", query, gate_proj)) -def _nsa_init(self, num_heads, head_dim, compress_ratio, num_selected_blocks, - block_size, window_size, is_causal, **kwargs): +def _nsa_init( + self, num_heads, head_dim, compress_ratio, num_selected_blocks, block_size, window_size, is_causal, **kwargs +): MojoOperator.__init__(self, **kwargs) self.num_heads = num_heads self.head_dim = head_dim @@ -768,8 +759,13 @@ def _nsa_decode_core(self, q_i, k_i, v_i, sl, softmax_scale): H, D = q_i.shape comp_k, comp_v = _nsa_compress_kv(k_i, v_i, self.compress_ratio) sel_mask = _nsa_select_blocks( - q_i, comp_k, sl, softmax_scale, - self.compress_ratio, self.block_size, self.num_selected_blocks, + q_i, + comp_k, + sl, + softmax_scale, + self.compress_ratio, + self.block_size, + self.num_selected_blocks, ) win_k, win_v = _nsa_window_kv(k_i, v_i, sl, self.window_size) @@ -789,11 +785,20 @@ class MojoDecodeNSA(MojoOperator): and sliding window (local) — blended by a per-head sigmoid gate. """ - def __init__(self, num_heads, head_dim, compress_ratio=4, - num_selected_blocks=16, block_size=64, window_size=512, - is_causal=True, **kwargs): - _nsa_init(self, num_heads, head_dim, compress_ratio, - num_selected_blocks, block_size, window_size, is_causal, **kwargs) + def __init__( + self, + num_heads, + head_dim, + compress_ratio=4, + num_selected_blocks=16, + block_size=64, + window_size=512, + is_causal=True, + **kwargs, + ): + _nsa_init( + self, num_heads, head_dim, compress_ratio, num_selected_blocks, block_size, window_size, is_causal, **kwargs + ) def forward( self, @@ -833,11 +838,20 @@ def forward( class MojoPagedDecodeNSA(MojoOperator): """Paged NSA decode with blocked KV caches.""" - def __init__(self, num_heads, head_dim, compress_ratio=4, - num_selected_blocks=16, block_size=64, window_size=512, - is_causal=True, **kwargs): - _nsa_init(self, num_heads, head_dim, compress_ratio, - num_selected_blocks, block_size, window_size, is_causal, **kwargs) + def __init__( + self, + num_heads, + head_dim, + compress_ratio=4, + num_selected_blocks=16, + block_size=64, + window_size=512, + is_causal=True, + **kwargs, + ): + _nsa_init( + self, num_heads, head_dim, compress_ratio, num_selected_blocks, block_size, window_size, is_causal, **kwargs + ) def forward( self, @@ -914,9 +928,7 @@ def __init__( self.is_causal = is_causal self.use_attn_sink = use_attn_sink - self.kv_b_proj = torch.nn.Parameter( - torch.empty(num_heads * (qk_nope_head_dim + v_head_dim), kv_lora_rank) - ) + self.kv_b_proj = torch.nn.Parameter(torch.empty(num_heads * (qk_nope_head_dim + v_head_dim), kv_lora_rank)) if use_attn_sink: self.attn_sink = _make_attn_sink(num_heads, self.tensor_factory_kwargs) @@ -956,9 +968,9 @@ def forward( for i in range(batch_size): s = cu_q_lens[i].item() e = cu_q_lens[i + 1].item() - q_i = query[s:e] # (L, H, d_qk) - k_i = k_all[s:e] # (L, H, d_qk) - v_i = v_all[s:e] # (L, H, d_v) + q_i = query[s:e] # (L, H, d_qk) + k_i = k_all[s:e] # (L, H, d_qk) + v_i = v_all[s:e] # (L, H, d_v) scores = torch.einsum("thd,shd->ths", q_i, k_i) * softmax_scale @@ -967,9 +979,7 @@ def forward( causal_mask = torch.tril(torch.ones(L, L, device=query.device, dtype=torch.bool)) scores.masked_fill_(~causal_mask.unsqueeze(1), float("-inf")) - probs = _attention_probs_with_optional_sink( - scores, query.dtype, getattr(self, "attn_sink", None) - ) + probs = _attention_probs_with_optional_sink(scores, query.dtype, getattr(self, "attn_sink", None)) outputs[s:e] = torch.einsum("ths,shd->thd", probs, v_i) return outputs @@ -1007,9 +1017,7 @@ def __init__( self.is_causal = is_causal self.use_attn_sink = use_attn_sink - self.kv_b_proj = torch.nn.Parameter( - torch.empty(num_heads * (qk_nope_head_dim + v_head_dim), kv_lora_rank) - ) + self.kv_b_proj = torch.nn.Parameter(torch.empty(num_heads * (qk_nope_head_dim + v_head_dim), kv_lora_rank)) if use_attn_sink: self.attn_sink = _make_attn_sink(num_heads, self.tensor_factory_kwargs) @@ -1087,14 +1095,10 @@ def forward( if self.is_causal: q_len = qe - qs - causal_mask = torch.ones(q_len, kv_len, device=query.device, dtype=torch.bool).tril( - kv_len - q_len - ) + causal_mask = torch.ones(q_len, kv_len, device=query.device, dtype=torch.bool).tril(kv_len - q_len) scores.masked_fill_(~causal_mask.unsqueeze(1), float("-inf")) - probs = _attention_probs_with_optional_sink( - scores, query.dtype, getattr(self, "attn_sink", None) - ) + probs = _attention_probs_with_optional_sink(scores, query.dtype, getattr(self, "attn_sink", None)) outputs[qs:qe] = torch.einsum("ths,shd->thd", probs, v) return outputs @@ -1111,11 +1115,20 @@ def extra_repr(self) -> str: class MojoPrefillNSA(MojoOperator): """Non-paged NSA prefill — variable-length packed sequences.""" - def __init__(self, num_heads, head_dim, compress_ratio=4, - num_selected_blocks=16, block_size=64, window_size=512, - is_causal=True, **kwargs): - _nsa_init(self, num_heads, head_dim, compress_ratio, - num_selected_blocks, block_size, window_size, is_causal, **kwargs) + def __init__( + self, + num_heads, + head_dim, + compress_ratio=4, + num_selected_blocks=16, + block_size=64, + window_size=512, + is_causal=True, + **kwargs, + ): + _nsa_init( + self, num_heads, head_dim, compress_ratio, num_selected_blocks, block_size, window_size, is_causal, **kwargs + ) def forward( self, @@ -1153,12 +1166,17 @@ def forward( ck, cv = _nsa_compress_kv(k_ctx, v_ctx, cr) if t_sl >= cr else (k_ctx, v_ctx) sel_mask = _nsa_select_blocks( - q_seq[t], ck, t_sl, softmax_scale, - cr, self.block_size, self.num_selected_blocks, + q_seq[t], + ck, + t_sl, + softmax_scale, + cr, + self.block_size, + self.num_selected_blocks, ) win_k, win_v = _nsa_window_kv(k_ctx, v_ctx, t_sl, self.window_size) - q_t = q_seq[t:t + 1] + q_t = q_seq[t : t + 1] out_comp = _nsa_attend(q_t, ck, cv, softmax_scale).squeeze(0) out_sel = _nsa_attend(q_t, k_ctx, v_ctx, softmax_scale, mask=sel_mask).squeeze(0) out_win = _nsa_attend(q_t, win_k, win_v, softmax_scale).squeeze(0) @@ -1174,11 +1192,20 @@ def forward( class MojoPagedPrefillNSA(MojoOperator): """Paged NSA prefill with blocked KV caches.""" - def __init__(self, num_heads, head_dim, compress_ratio=4, - num_selected_blocks=16, block_size=64, window_size=512, - is_causal=True, **kwargs): - _nsa_init(self, num_heads, head_dim, compress_ratio, - num_selected_blocks, block_size, window_size, is_causal, **kwargs) + def __init__( + self, + num_heads, + head_dim, + compress_ratio=4, + num_selected_blocks=16, + block_size=64, + window_size=512, + is_causal=True, + **kwargs, + ): + _nsa_init( + self, num_heads, head_dim, compress_ratio, num_selected_blocks, block_size, window_size, is_causal, **kwargs + ) def forward( self, @@ -1243,12 +1270,17 @@ def forward( ck, cv = _nsa_compress_kv(k_ctx, v_ctx, cr) if t_kv >= cr else (k_ctx, v_ctx) sel_mask = _nsa_select_blocks( - q_seq[t_idx], ck, t_kv, softmax_scale, - cr, self.block_size, self.num_selected_blocks, + q_seq[t_idx], + ck, + t_kv, + softmax_scale, + cr, + self.block_size, + self.num_selected_blocks, ) win_k, win_v = _nsa_window_kv(k_ctx, v_ctx, t_kv, self.window_size) - q_t = q_seq[t_idx:t_idx + 1] + q_t = q_seq[t_idx : t_idx + 1] out_comp = _nsa_attend(q_t, ck, cv, softmax_scale).squeeze(0) out_sel = _nsa_attend(q_t, k_ctx, v_ctx, softmax_scale, mask=sel_mask).squeeze(0) out_win = _nsa_attend(q_t, win_k, win_v, softmax_scale).squeeze(0) @@ -1312,6 +1344,186 @@ def extra_repr(self) -> str: return f"{self.scale=}, {self.enable_gqa=}".replace("self.", "") +class MojoConformerSlidingWindowAttention(MojoOperator): + """Varlen Conformer encoder attention with per-query left/right visibility windows. + + Contract: + - `query`, `key`, `value` use THD layout: ``[total_tokens, heads, head_dim]``. + - `cu_q_lens` and `cu_total_seq_lens` delimit per-sequence query and KV spans. + - if `cu_total_seq_lens` is None, KV spans are the same as query spans. + - for each query, visible keys are limited to ``[q_abs - left_window, q_abs + right_window]``. + """ + + def __init__( + self, + left_window: int, + right_window: int = 0, + softmax_scale: Optional[float] = None, + ): + super().__init__() + if left_window < 0: + raise ValueError(f"left_window must be >= 0, got {left_window}") + if right_window < 0: + raise ValueError(f"right_window must be >= 0, got {right_window}") + self.left_window = left_window + self.right_window = right_window + self.softmax_scale = softmax_scale + + def forward( + self, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + cu_q_lens: torch.Tensor, + cu_total_seq_lens: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + if query.ndim != 3 or key.ndim != 3 or value.ndim != 3: + raise ValueError("query/key/value must be 3D THD tensors") + if key.shape != value.shape: + raise ValueError(f"key/value must share the same shape, got {key.shape}, {value.shape}") + if cu_q_lens.dtype != torch.int32: + raise ValueError(f"cu_q_lens must be int32, got {cu_q_lens.dtype}") + if cu_total_seq_lens is not None and cu_total_seq_lens.dtype != torch.int32: + raise ValueError(f"cu_total_seq_lens must be int32, got {cu_total_seq_lens.dtype}") + + total_q_tokens, num_heads, head_dim = query.shape + + if cu_total_seq_lens is None: + cu_total_seq_lens = cu_q_lens + + if self.softmax_scale is not None: + softmax_scale = self.softmax_scale + else: + softmax_scale = 1.0 / math.sqrt(head_dim) + + output = torch.zeros_like(query) + batch_size = cu_q_lens.shape[0] - 1 + + for b in range(batch_size): + q_start, q_end = cu_q_lens[b].item(), cu_q_lens[b + 1].item() + kv_start, kv_end = cu_total_seq_lens[b].item(), cu_total_seq_lens[b + 1].item() + q_len = q_end - q_start + kv_len = kv_end - kv_start + if q_len <= 0 or kv_len <= 0: + continue + if kv_len < q_len: + raise ValueError(f"KV length must be >= query length for conformer attention, got {kv_len} vs {q_len}") + + q = query[q_start:q_end].permute(1, 0, 2) + k_t = key[kv_start:kv_end].permute(1, 2, 0) + scores = torch.bmm(q, k_t).float() * softmax_scale + + q_abs_idx = torch.arange(kv_len - q_len, kv_len, device=query.device) + kv_idx = torch.arange(kv_len, device=query.device) + window_mask = (q_abs_idx[:, None] - self.left_window <= kv_idx[None, :]) & ( + kv_idx[None, :] <= q_abs_idx[:, None] + self.right_window + ) + scores = torch.where(window_mask.unsqueeze(0), scores, float("-inf")) + + probs = torch.softmax(scores, dim=-1, dtype=torch.float32).to(value.dtype) + v = value[kv_start:kv_end].permute(1, 0, 2) + output[q_start:q_end] = torch.bmm(probs, v).permute(1, 0, 2).to(output.dtype) + + return output + + def extra_repr(self) -> str: + return f"{self.left_window=}, {self.right_window=}, {self.softmax_scale=}".replace("self.", "") + + +class MojoConformerChunkAttention(MojoOperator): + """Varlen duplex Conformer encoder attention with chunk-based visibility. + + Contract: + - `query`, `key`, `value` use THD layout: ``[total_tokens, heads, head_dim]``. + - `cu_q_lens` and `cu_total_seq_lens` delimit per-sequence query and KV spans. + - if `cu_total_seq_lens` is None, KV spans are the same as query spans. + - visibility matches the duplex audio modeling mask: + queries are grouped into chunks of size ``chunk_size`` and may attend to + keys in ``[ctx_start, chunk_end)`` where ``ctx_start`` is derived from + ``left_context_chunks`` and chunk boundaries. + - ``left_context_chunks < 0`` means unlimited left context. + """ + + def __init__( + self, + chunk_size: int, + left_context_chunks: int = -1, + softmax_scale: Optional[float] = None, + ): + super().__init__() + if chunk_size <= 0: + raise ValueError(f"chunk_size must be > 0, got {chunk_size}") + if left_context_chunks < -1: + raise ValueError(f"left_context_chunks must be >= -1, got {left_context_chunks}") + self.chunk_size = chunk_size + self.left_context_chunks = left_context_chunks + self.softmax_scale = softmax_scale + + def forward( + self, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + cu_q_lens: torch.Tensor, + cu_total_seq_lens: Optional[torch.Tensor] = None, + *, + q_block_indices: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + if query.ndim != 3 or key.ndim != 3 or value.ndim != 3: + raise ValueError("query/key/value must be 3D THD tensors") + if key.shape != value.shape: + raise ValueError(f"key/value must share the same shape, got {key.shape}, {value.shape}") + if cu_q_lens.dtype != torch.int32: + raise ValueError(f"cu_q_lens must be int32, got {cu_q_lens.dtype}") + if cu_total_seq_lens is not None and cu_total_seq_lens.dtype != torch.int32: + raise ValueError(f"cu_total_seq_lens must be int32, got {cu_total_seq_lens.dtype}") + + _, _, head_dim = query.shape + + if cu_total_seq_lens is None: + cu_total_seq_lens = cu_q_lens + + softmax_scale = self.softmax_scale if self.softmax_scale is not None else 1.0 / math.sqrt(head_dim) + left_context_tokens = self.left_context_chunks * self.chunk_size + + output = torch.zeros_like(query) + batch_size = cu_q_lens.shape[0] - 1 + + for b in range(batch_size): + q_start, q_end = cu_q_lens[b].item(), cu_q_lens[b + 1].item() + kv_start, kv_end = cu_total_seq_lens[b].item(), cu_total_seq_lens[b + 1].item() + q_len = q_end - q_start + kv_len = kv_end - kv_start + if q_len <= 0 or kv_len <= 0: + continue + if kv_len < q_len: + raise ValueError(f"KV length must be >= query length for conformer attention, got {kv_len} vs {q_len}") + + q = query[q_start:q_end].permute(1, 0, 2) + k_t = key[kv_start:kv_end].permute(1, 2, 0) + scores = torch.bmm(q, k_t).float() * softmax_scale + + q_abs_idx = torch.arange(kv_len - q_len, kv_len, device=query.device) + kv_idx = torch.arange(kv_len, device=query.device) + chunk_start = torch.div(q_abs_idx, self.chunk_size, rounding_mode="floor") * self.chunk_size + chunk_end = torch.clamp(chunk_start + self.chunk_size, max=kv_len) + if self.left_context_chunks < 0: + ctx_start = torch.zeros_like(chunk_start) + else: + ctx_start = torch.clamp(chunk_start - left_context_tokens, min=0) + chunk_mask = (ctx_start[:, None] <= kv_idx[None, :]) & (kv_idx[None, :] < chunk_end[:, None]) + scores = torch.where(chunk_mask.unsqueeze(0), scores, float("-inf")) + + probs = torch.softmax(scores, dim=-1, dtype=torch.float32).to(value.dtype) + v = value[kv_start:kv_end].permute(1, 0, 2) + output[q_start:q_end] = torch.bmm(probs, v).permute(1, 0, 2).to(output.dtype) + + return output + + def extra_repr(self) -> str: + return f"{self.chunk_size=}, {self.left_context_chunks=}, {self.softmax_scale=}".replace("self.", "") + + def _generate_window_mask( q_seq_len: int, kv_seq_len: int, @@ -1338,6 +1550,7 @@ def _generate_window_mask( return mask + class MojoPagedPrefillSWA(MojoOperator): def __init__( self, @@ -1382,7 +1595,9 @@ def forward( if softmax_scale is None: softmax_scale = 1.0 / (head_dim**0.5) - total_seq_lens = _seq_lens_from_cu(cu_q_lens) if cu_total_seq_lens is None else _seq_lens_from_cu(cu_total_seq_lens) + total_seq_lens = ( + _seq_lens_from_cu(cu_q_lens) if cu_total_seq_lens is None else _seq_lens_from_cu(cu_total_seq_lens) + ) o = torch.empty_like(query) bsz = cu_q_lens.shape[0] - 1 @@ -1487,7 +1702,7 @@ def forward( o = torch.zeros_like(query) for i in range(bsz): - q_i = query[i].unsqueeze(1) # -> [n_q_heads, 1, head_dim] + q_i = query[i].unsqueeze(1) # -> [n_q_heads, 1, head_dim] kv_seq_len = total_seq_lens[i].item() if kv_seq_len <= 0: @@ -1615,7 +1830,9 @@ def forward( l_i = torch.sum(p_i, dim=-1, keepdim=True) # -> [n_q_heads, q_seq_len, 1] p_i = p_i.to(value.dtype) - v_i = value[cu_total_seq_lens[i] : cu_total_seq_lens[i + 1]].permute(1, 0, 2) # -> [n_kv_heads, kv_seq_len, head_dim] + v_i = value[cu_total_seq_lens[i] : cu_total_seq_lens[i + 1]].permute( + 1, 0, 2 + ) # -> [n_kv_heads, kv_seq_len, head_dim] if n_q_heads != n_kv_heads: if self.gqa_interleave: v_i = v_i.repeat((n_q_heads // n_kv_heads, 1, 1)) @@ -1632,13 +1849,14 @@ def _dynamic_quantize(tensor, qmax, qmin, quant_dtype): amax = tensor.abs().amax(dim=-1, keepdim=True).clamp(min=1e-12) scale = amax / qmax scale = torch.where(scale < 1e-6, 1.0, scale) - + tensor_scaled = tensor / scale tensor_quant = tensor_scaled.round().clamp(qmin, qmax).to(quant_dtype) - + scale = scale.view(*tensor.shape[:-1], 1) return tensor_quant, scale + class MojoPagedPrefillQuantGQA(MojoOperator): def __init__( self, @@ -1670,7 +1888,9 @@ def __init__( assert self.query_dtype in (torch.bfloat16, torch.int8), f"Unsupported query dtype {self.query_dtype}" if self.query_dtype == torch.int8: raise NotImplementedError("Quantized query is not implemented") - assert self.context_dtype == torch.int8, f"Quant attention support int8 context only, but got {self.context_dtype}" + assert self.context_dtype == torch.int8, ( + f"Quant attention support int8 context only, but got {self.context_dtype}" + ) assert self.compute_dtype in (torch.bfloat16, torch.int8), f"Unsupported compute dtype {self.compute_dtype}" if self.compute_dtype == torch.int8: bits = 8 @@ -1727,10 +1947,14 @@ def forward( """ assert_paged_prefill_contract(cu_q_lens, block_tables, cu_total_seq_lens) if self.query_dtype == torch.int8: - assert query_scale is not None and query.dtype == self.query_dtype, "query_scale must be provided for quantized query" + assert query_scale is not None and query.dtype == self.query_dtype, ( + "query_scale must be provided for quantized query" + ) else: - assert query_scale is None and query.dtype == self.query_dtype, "query_scale must be None for non-quantized query" - + assert query_scale is None and query.dtype == self.query_dtype, ( + "query_scale must be None for non-quantized query" + ) + total_q_tokens, num_q_heads, head_dim = query.shape _, num_kv_heads, page_size, _ = key_cache.shape if softmax_scale is None: @@ -1757,14 +1981,18 @@ def forward( q_seq_len = q_lens[i].item() start_loc = cu_q_lens[i].item() end_loc = cu_q_lens[i + 1].item() - q = query[start_loc:end_loc].permute(1, 0, 2) # [n_q_heads, q_seq_len, head_dim] + q = query[start_loc:end_loc].permute(1, 0, 2) # [n_q_heads, q_seq_len, head_dim] kv_seq_len = total_seq_lens[i].item() kv_blocks = (kv_seq_len + page_size - 1) // page_size - k_unpadded = key_cache[block_tables[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] - k_unpadded = k_unpadded.permute(1, 0, 2, 3).reshape(num_kv_heads, kv_blocks * page_size, head_dim)[:, :kv_seq_len] - v_unpadded = value_cache[block_tables[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] - v_unpadded = v_unpadded.permute(1, 0, 2, 3).reshape(num_kv_heads, kv_blocks * page_size, head_dim)[:, :kv_seq_len] + k_unpadded = key_cache[block_tables[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] + k_unpadded = k_unpadded.permute(1, 0, 2, 3).reshape(num_kv_heads, kv_blocks * page_size, head_dim)[ + :, :kv_seq_len + ] + v_unpadded = value_cache[block_tables[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] + v_unpadded = v_unpadded.permute(1, 0, 2, 3).reshape(num_kv_heads, kv_blocks * page_size, head_dim)[ + :, :kv_seq_len + ] if num_q_heads != num_kv_heads: if self.gqa_layout == "AABB": @@ -1778,7 +2006,9 @@ def forward( v_expanded = v_unpadded if self.compute_dtype == torch.int8: - q_quant, q_scale = _dynamic_quantize(q * key_scale.unsqueeze(1), self.qmax, self.qmin, self.compute_dtype) + q_quant, q_scale = _dynamic_quantize( + q * key_scale.unsqueeze(1), self.qmax, self.qmin, self.compute_dtype + ) attn_scores = torch.matmul(q_quant.float(), k_expanded.mT.float()) * q_scale * softmax_scale else: k_expanded_scaled = k_expanded.float() * key_scale.unsqueeze(1).float() @@ -1798,8 +2028,14 @@ def forward( attn_probs = torch.softmax(attn_scores, dim=-1, dtype=torch.float32).to(query.dtype) if self.compute_dtype == torch.int8: - attn_probs_quant, attn_probs_scale = _dynamic_quantize(attn_probs, self.qmax, self.qmin, self.compute_dtype) - o = torch.matmul(attn_probs_quant.float(), v_expanded.float()) * attn_probs_scale * value_scale.unsqueeze(1) + attn_probs_quant, attn_probs_scale = _dynamic_quantize( + attn_probs, self.qmax, self.qmin, self.compute_dtype + ) + o = ( + torch.matmul(attn_probs_quant.float(), v_expanded.float()) + * attn_probs_scale + * value_scale.unsqueeze(1) + ) else: v_expanded_scaled = v_expanded.float() * value_scale.unsqueeze(1).float() o = torch.matmul(attn_probs.float(), v_expanded_scaled) @@ -1849,7 +2085,9 @@ def __init__( assert self.query_dtype in (torch.bfloat16, torch.int8), f"Unsupported query dtype {self.query_dtype}" if self.query_dtype == torch.int8: raise NotImplementedError("Quantized query is not implemented") - assert self.context_dtype == torch.int8, f"Quant attention support int8 context only, but got {self.context_dtype}" + assert self.context_dtype == torch.int8, ( + f"Quant attention support int8 context only, but got {self.context_dtype}" + ) assert self.compute_dtype in (torch.bfloat16, torch.int8), f"Unsupported compute dtype {self.compute_dtype}" if self.compute_dtype == torch.int8: bits = 8 @@ -1899,10 +2137,13 @@ def forward( """ assert_paged_decode_contract(block_tables, total_seq_lens) if self.query_dtype == torch.int8: - assert query_scale is not None and query.dtype == self.query_dtype, "query_scale must be provided for quantized query" + assert query_scale is not None and query.dtype == self.query_dtype, ( + "query_scale must be provided for quantized query" + ) else: - assert query_scale is None and query.dtype == self.query_dtype, "query_scale must be None for non-quantized query" - + assert query_scale is None and query.dtype == self.query_dtype, ( + "query_scale must be None for non-quantized query" + ) batch_size, num_q_heads, head_dim = query.shape _, num_kv_heads, page_size, head_dim = key_cache.shape @@ -1927,13 +2168,17 @@ def forward( # skip padded batches continue - q = query[i].unsqueeze(1) # [n_q_heads, 1, head_dim] + q = query[i].unsqueeze(1) # [n_q_heads, 1, head_dim] kv_blocks = (seq_len + page_size - 1) // page_size - k_unpadded = key_cache[block_tables[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] - k_unpadded = k_unpadded.permute(1, 0, 2, 3).reshape(num_kv_heads, kv_blocks * page_size, head_dim)[:, :seq_len] - v_unpadded = value_cache[block_tables[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] - v_unpadded = v_unpadded.permute(1, 0, 2, 3).reshape(num_kv_heads, kv_blocks * page_size, head_dim)[:, :seq_len] + k_unpadded = key_cache[block_tables[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] + k_unpadded = k_unpadded.permute(1, 0, 2, 3).reshape(num_kv_heads, kv_blocks * page_size, head_dim)[ + :, :seq_len + ] + v_unpadded = value_cache[block_tables[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] + v_unpadded = v_unpadded.permute(1, 0, 2, 3).reshape(num_kv_heads, kv_blocks * page_size, head_dim)[ + :, :seq_len + ] if num_q_heads != num_kv_heads: if self.gqa_layout == "AABB": @@ -1947,7 +2192,9 @@ def forward( v_expanded = v_unpadded if self.compute_dtype == torch.int8: - q_quant, q_scale = _dynamic_quantize(q * key_scale.unsqueeze(1), self.qmax, self.qmin, self.compute_dtype) + q_quant, q_scale = _dynamic_quantize( + q * key_scale.unsqueeze(1), self.qmax, self.qmin, self.compute_dtype + ) attn_scores = torch.matmul(q_quant.float(), k_expanded.mT.float()) * q_scale * softmax_scale else: k_expanded_scaled = k_expanded.float() * key_scale.unsqueeze(1).float() @@ -1963,8 +2210,14 @@ def forward( attn_probs = torch.softmax(attn_scores, dim=-1, dtype=torch.float32).to(query.dtype) if self.compute_dtype == torch.int8: - attn_probs_quant, attn_probs_scale = _dynamic_quantize(attn_probs, self.qmax, self.qmin, self.compute_dtype) - o = torch.matmul(attn_probs_quant.float(), v_expanded.float()) * attn_probs_scale * value_scale.unsqueeze(1) + attn_probs_quant, attn_probs_scale = _dynamic_quantize( + attn_probs, self.qmax, self.qmin, self.compute_dtype + ) + o = ( + torch.matmul(attn_probs_quant.float(), v_expanded.float()) + * attn_probs_scale + * value_scale.unsqueeze(1) + ) else: v_expanded_scaled = v_expanded.float() * value_scale.unsqueeze(1).float() o = torch.matmul(attn_probs.float(), v_expanded_scaled) @@ -2012,7 +2265,9 @@ def __init__( assert self.query_dtype in (torch.bfloat16, torch.int8), f"Unsupported query dtype {self.query_dtype}" if self.query_dtype == torch.int8: raise NotImplementedError("Quantized query is not implemented") - assert self.context_dtype == torch.int8, f"Quant attention support int8 context only, but got {self.context_dtype}" + assert self.context_dtype == torch.int8, ( + f"Quant attention support int8 context only, but got {self.context_dtype}" + ) assert self.compute_dtype in (torch.bfloat16, torch.int8), f"Unsupported compute dtype {self.compute_dtype}" if self.compute_dtype == torch.int8: bits = 8 @@ -2024,9 +2279,9 @@ def forward( query: torch.Tensor, # [total_q_len, n_q_heads, head_dim] query_scale: Optional[torch.Tensor], # [total_q_len, n_q_heads, 1] key_cache: torch.Tensor, # [n_pages, n_kv_heads, page_size, head_dim] - key_scale: torch.Tensor, # [n_kv_heads, head_dim] + key_scale: torch.Tensor, # [n_kv_heads, head_dim] value_cache: torch.Tensor, # [n_pages, n_kv_heads, page_size, head_dim] - value_scale: torch.Tensor, # [n_kv_heads, head_dim] + value_scale: torch.Tensor, # [n_kv_heads, head_dim] cu_q_lens: torch.Tensor, # [bsz + 1] block_table: torch.Tensor, # [bsz, max_num_blocks] softmax_scale: Optional[float] = None, @@ -2063,27 +2318,29 @@ def forward( assert_paged_prefill_contract(cu_q_lens, block_table, cu_total_seq_lens) if self.query_dtype == torch.int8: - assert query_scale is not None and query.dtype == self.query_dtype, "query_scale must be provided for quantized query" + assert query_scale is not None and query.dtype == self.query_dtype, ( + "query_scale must be provided for quantized query" + ) else: - assert query_scale is None and query.dtype == self.query_dtype, "query_scale must be None for non-quantized query" - + assert query_scale is None and query.dtype == self.query_dtype, ( + "query_scale must be None for non-quantized query" + ) + total_q_len, n_q_heads, head_dim = query.shape _, n_kv_heads, page_size, _ = key_cache.shape if softmax_scale is None: softmax_scale = 1.0 / (head_dim**0.5) - seqlens_kv = ( - _seq_lens_from_cu(cu_q_lens) if cu_total_seq_lens is None else _seq_lens_from_cu(cu_total_seq_lens) - ) + seqlens_kv = _seq_lens_from_cu(cu_q_lens) if cu_total_seq_lens is None else _seq_lens_from_cu(cu_total_seq_lens) if n_q_heads != n_kv_heads: if self.gqa_interleave: - key_scale = key_scale.repeat((n_q_heads // n_kv_heads, 1)) # -> [n_q_heads, head_dim] - value_scale = value_scale.repeat((n_q_heads // n_kv_heads, 1)) # -> [n_q_heads, head_dim] - else: + key_scale = key_scale.repeat((n_q_heads // n_kv_heads, 1)) # -> [n_q_heads, head_dim] + value_scale = value_scale.repeat((n_q_heads // n_kv_heads, 1)) # -> [n_q_heads, head_dim] + else: key_scale = key_scale.repeat_interleave(n_q_heads // n_kv_heads, dim=0) # -> [n_q_heads, head_dim] value_scale = value_scale.repeat_interleave(n_q_heads // n_kv_heads, dim=0) # -> [n_q_heads, head_dim] - + o = torch.empty_like(query) bsz = cu_q_lens.shape[0] - 1 for i in range(bsz): @@ -2096,7 +2353,7 @@ def forward( kv_seq_len = seqlens_kv[i].item() kv_blocks = (kv_seq_len + page_size - 1) // page_size - k_i = key_cache[block_table[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] + k_i = key_cache[block_table[i, :kv_blocks]] # [kv_blocks, n_kv_heads, page_size, head_dim] k_i = k_i.permute(1, 0, 2, 3).reshape(n_kv_heads, kv_blocks * page_size, head_dim)[:, :kv_seq_len] k_i_T = k_i.permute(0, 2, 1) # -> [n_kv_heads, head_dim, kv_seq_len] if n_q_heads != n_kv_heads: @@ -2106,10 +2363,14 @@ def forward( k_i_T = k_i_T.repeat_interleave( n_q_heads // n_kv_heads, dim=0 ) # -> [n_q_heads, head_dim, kv_seq_len] - + if self.compute_dtype == torch.int8: - q_i_quant, q_i_scale = _dynamic_quantize(q_i * key_scale.unsqueeze(1), self.qmax, self.qmin, self.compute_dtype) - s_i = torch.bmm(q_i_quant.float(), k_i_T.float()) * q_i_scale * softmax_scale # -> [n_q_heads, q_seq_len, kv_seq_len] + q_i_quant, q_i_scale = _dynamic_quantize( + q_i * key_scale.unsqueeze(1), self.qmax, self.qmin, self.compute_dtype + ) + s_i = ( + torch.bmm(q_i_quant.float(), k_i_T.float()) * q_i_scale * softmax_scale + ) # -> [n_q_heads, q_seq_len, kv_seq_len] else: k_i_T = k_i_T.float() * key_scale.unsqueeze(-1).float() s_i = torch.bmm(q_i.float(), k_i_T.float()) * softmax_scale @@ -2138,16 +2399,19 @@ def forward( v_i = v_i.repeat_interleave(n_q_heads // n_kv_heads, dim=0) # -> [n_q_heads, kv_seq_len, head_dim] if self.compute_dtype == torch.int8: p_i_quant, p_i_scale = _dynamic_quantize(p_i, self.qmax, self.qmin, self.compute_dtype) - o_i = torch.bmm(p_i_quant.float(), v_i.float()) * p_i_scale * value_scale.unsqueeze(1) # -> [n_q_heads, q_seq_len, head_dim] + o_i = ( + torch.bmm(p_i_quant.float(), v_i.float()) * p_i_scale * value_scale.unsqueeze(1) + ) # -> [n_q_heads, q_seq_len, head_dim] else: v_i = v_i.float() * value_scale.unsqueeze(1).float() - o_i = torch.bmm(p_i.float(), v_i.float()) # -> [n_q_heads, q_seq_len, head_dim] - + o_i = torch.bmm(p_i.float(), v_i.float()) # -> [n_q_heads, q_seq_len, head_dim] + o_i = o_i / l_i o_i = o_i.permute(1, 0, 2) # -> [q_seq_len, n_q_heads, head_dim] o[cu_q_lens[i] : cu_q_lens[i + 1]] = o_i.to(o.dtype) return o + class MojoPagedDecodeQuantSWA(MojoOperator): def __init__( self, @@ -2185,7 +2449,9 @@ def __init__( assert self.query_dtype in (torch.bfloat16, torch.int8), f"Unsupported query dtype {self.query_dtype}" if self.query_dtype == torch.int8: raise NotImplementedError("Quantized query is not implemented") - assert self.context_dtype == torch.int8, f"Quant attention support int8 context only, but got {self.context_dtype}" + assert self.context_dtype == torch.int8, ( + f"Quant attention support int8 context only, but got {self.context_dtype}" + ) assert self.compute_dtype in (torch.bfloat16, torch.int8), f"Unsupported compute dtype {self.compute_dtype}" if self.compute_dtype == torch.int8: bits = 8 @@ -2197,9 +2463,9 @@ def forward( query: torch.Tensor, # [bsz, n_q_heads, head_dim] query_scale: Optional[torch.Tensor], # [bsz, n_q_heads, 1] key_cache: torch.Tensor, # [n_pages, n_kv_heads, page_size, head_dim] - key_scale: torch.Tensor, # [n_kv_heads, head_dim] + key_scale: torch.Tensor, # [n_kv_heads, head_dim] value_cache: torch.Tensor, # [n_pages, n_kv_heads, page_size, head_dim] - value_scale: torch.Tensor, # [n_kv_heads, head_dim] + value_scale: torch.Tensor, # [n_kv_heads, head_dim] total_seq_lens: torch.Tensor, # [bsz] block_table: torch.Tensor, # [bsz, max_num_blocks] softmax_scale: Optional[float] = None, @@ -2229,10 +2495,14 @@ def forward( assert_paged_decode_contract(block_table, total_seq_lens) if self.query_dtype == torch.int8: - assert query_scale is not None and query.dtype == self.query_dtype, "query_scale must be provided for quantized query" + assert query_scale is not None and query.dtype == self.query_dtype, ( + "query_scale must be provided for quantized query" + ) else: - assert query_scale is None and query.dtype == self.query_dtype, "query_scale must be None for non-quantized query" - + assert query_scale is None and query.dtype == self.query_dtype, ( + "query_scale must be None for non-quantized query" + ) + bsz, n_q_heads, head_dim = query.shape _, n_kv_heads, page_size, _ = key_cache.shape if softmax_scale is None: @@ -2240,16 +2510,15 @@ def forward( if n_q_heads != n_kv_heads: if self.gqa_interleave: - key_scale = key_scale.repeat((n_q_heads // n_kv_heads, 1)) # -> [n_q_heads, head_dim] - value_scale = value_scale.repeat((n_q_heads // n_kv_heads, 1)) # -> [n_q_heads, head_dim] - else: + key_scale = key_scale.repeat((n_q_heads // n_kv_heads, 1)) # -> [n_q_heads, head_dim] + value_scale = value_scale.repeat((n_q_heads // n_kv_heads, 1)) # -> [n_q_heads, head_dim] + else: key_scale = key_scale.repeat_interleave(n_q_heads // n_kv_heads, dim=0) # -> [n_q_heads, head_dim] value_scale = value_scale.repeat_interleave(n_q_heads // n_kv_heads, dim=0) # -> [n_q_heads, head_dim] - o = torch.zeros_like(query) for i in range(bsz): - q_i = query[i].unsqueeze(1) # -> [n_q_heads, 1, head_dim] + q_i = query[i].unsqueeze(1) # -> [n_q_heads, 1, head_dim] kv_seq_len = total_seq_lens[i].item() if kv_seq_len == 0: # skip padded tokens @@ -2267,8 +2536,12 @@ def forward( ) # -> [n_q_heads, head_dim, kv_seq_len] if self.compute_dtype == torch.int8: - q_i_quant, q_i_scale = _dynamic_quantize(q_i * key_scale.unsqueeze(1), self.qmax, self.qmin, self.compute_dtype) - s_i = torch.bmm(q_i_quant.float(), k_i_T.float()) * q_i_scale * softmax_scale # -> [n_q_heads, 1, kv_seq_len] + q_i_quant, q_i_scale = _dynamic_quantize( + q_i * key_scale.unsqueeze(1), self.qmax, self.qmin, self.compute_dtype + ) + s_i = ( + torch.bmm(q_i_quant.float(), k_i_T.float()) * q_i_scale * softmax_scale + ) # -> [n_q_heads, 1, kv_seq_len] else: k_i_T = k_i_T.float() * key_scale.unsqueeze(-1).float() s_i = torch.bmm(q_i.float(), k_i_T.float()) * softmax_scale @@ -2297,13 +2570,14 @@ def forward( v_i = v_i.repeat_interleave(n_q_heads // n_kv_heads, dim=0) # -> [n_q_heads, kv_seq_len, head_dim] if self.compute_dtype == torch.int8: p_i_quant, p_i_scale = _dynamic_quantize(p_i, self.qmax, self.qmin, self.compute_dtype) - o_i = torch.bmm(p_i_quant.float(), v_i.float()) * p_i_scale * value_scale.unsqueeze(1) # -> [n_q_heads, 1, head_dim] + o_i = ( + torch.bmm(p_i_quant.float(), v_i.float()) * p_i_scale * value_scale.unsqueeze(1) + ) # -> [n_q_heads, 1, head_dim] else: v_i = v_i.float() * value_scale.unsqueeze(1).float() - o_i = torch.bmm(p_i.float(), v_i.float()) # -> [n_q_heads, 1, head_dim] + o_i = torch.bmm(p_i.float(), v_i.float()) # -> [n_q_heads, 1, head_dim] o_i = o_i / l_i o_i = o_i.squeeze(1) # -> [n_q_heads, head_dim] o[i] = o_i.to(o.dtype) return o - diff --git a/mojo_opset/core/operators/convolution.py b/mojo_opset/core/operators/convolution.py index 46cd99cf..c5b2c139 100644 --- a/mojo_opset/core/operators/convolution.py +++ b/mojo_opset/core/operators/convolution.py @@ -40,3 +40,108 @@ def forward( out = F.silu(out) out = out.to(hidden_states.dtype) return out + + +class MojoConv1d(MojoOperator): + """Functional ``conv1d`` core op over ``[batch, channels, seq_len]`` inputs. + + This operator follows ``torch.nn.functional.conv1d`` semantics as closely + as practical for a core op: + - ``hidden_states`` uses ``[batch, in_channels, seq_len]``. + - ``weight`` uses the native ``conv1d`` layout + ``[out_channels, in_channels / groups, kernel_size]``. + - ``bias`` is optional with shape ``[out_channels]``. + - ``stride``, ``padding``, ``dilation``, and ``groups`` are operator + hyper-parameters carried on the module instance. + + Defaults are chosen to match the current USM Conformer depthwise module: + - ``stride=1`` + - ``padding="same"`` + - ``dilation=1`` + - ``groups=None`` which means "resolve to ``in_channels`` at runtime", + i.e. depthwise by default for the current use case. + + Notes about the intentionally chosen boundary: + - This op is still functional: weights and bias are passed to + ``forward`` instead of being owned as ``nn.Parameter``. + - ``padding_mode`` is not modeled; this op always follows the zero-pad + behavior of ``F.conv1d``. + - ``transposed`` convolution is out of scope. + - Fused activation / residual / causal state-update behavior remains in + separate operators. + - Backend-specific packed layouts are out of scope for the core op. + """ + + def __init__( + self, + stride: int = 1, + padding: int | str = "same", + dilation: int = 1, + groups: Optional[int] = None, + ): + super().__init__() + self.stride = stride + self.padding = padding + self.dilation = dilation + self.groups = groups + + def forward( + self, + hidden_states: torch.Tensor, + weight: torch.Tensor, + bias: Optional[torch.Tensor] = None, + ) -> torch.Tensor: + """Apply ``conv1d`` using native PyTorch weight layout. + + Args: + hidden_states: Input tensor in ``[batch, in_channels, seq_len]``. + weight: Native conv1d weight tensor in + ``[out_channels, in_channels / groups, kernel_size]``. + bias: Optional bias tensor in ``[out_channels]``. + + Returns: + Tensor in ``[batch, out_channels, output_seq_len]``. + + Raises: + ValueError: If inputs violate the conv1d contract. + """ + + if hidden_states.ndim != 3: + raise ValueError(f"hidden_states must be [B, C_in, T], got {hidden_states.shape}") + if weight.ndim != 3: + raise ValueError(f"weight must be [C_out, C_in/groups, K], got {weight.shape}") + + _, in_channels, _ = hidden_states.shape + out_channels, weight_in_channels, _ = weight.shape + groups = in_channels if self.groups is None else self.groups + + if groups <= 0: + raise ValueError(f"groups must be positive, got {groups}") + if in_channels % groups != 0: + raise ValueError(f"in_channels={in_channels} must be divisible by groups={groups}") + expected_weight_in_channels = in_channels // groups + if weight_in_channels != expected_weight_in_channels: + raise ValueError( + "weight second dimension must equal in_channels / groups, got " + f"{weight_in_channels} vs expected {expected_weight_in_channels}" + ) + if bias is not None: + if bias.ndim != 1 or bias.shape[0] != out_channels: + raise ValueError(f"bias must be [C_out] with C_out={out_channels}, got {bias.shape}") + + output = F.conv1d( + hidden_states.to(weight.dtype), + weight, + bias, + stride=self.stride, + padding=self.padding, + dilation=self.dilation, + groups=groups, + ) + return output.to(hidden_states.dtype) + + def extra_repr(self) -> str: + return ( + f"stride={self.stride}, padding={self.padding}, " + f"dilation={self.dilation}, groups={self.groups}" + ) diff --git a/mojo_opset/tests/accuracy/operators/test_attention.py b/mojo_opset/tests/accuracy/operators/test_attention.py index 04b3dc77..eb7849ba 100644 --- a/mojo_opset/tests/accuracy/operators/test_attention.py +++ b/mojo_opset/tests/accuracy/operators/test_attention.py @@ -1,28 +1,33 @@ import functools import math + from typing import Optional import pytest import torch +from mojo_opset import MojoConformerChunkAttention +from mojo_opset import MojoConformerSlidingWindowAttention from mojo_opset import MojoDecodeGQA from mojo_opset import MojoDecodeMLA from mojo_opset import MojoDecodeNSA from mojo_opset import MojoPagedDecodeGQA from mojo_opset import MojoPagedDecodeMLA from mojo_opset import MojoPagedDecodeNSA +from mojo_opset import MojoPagedDecodeSWA from mojo_opset import MojoPagedPrefillGQA from mojo_opset import MojoPagedPrefillMLA from mojo_opset import MojoPagedPrefillNSA +from mojo_opset import MojoPagedPrefillSWA from mojo_opset import MojoPrefillGQA from mojo_opset import MojoPrefillMLA from mojo_opset import MojoPrefillNSA from mojo_opset import MojoSdpa -from mojo_opset import MojoPagedPrefillSWA -from mojo_opset import MojoPagedDecodeSWA from mojo_opset import MojoSWA +from mojo_opset.backends.ttx.kernels import prepare_conformer_chunk_attention_q_block_indices from mojo_opset.tests.utils import auto_switch_platform from mojo_opset.tests.utils import bypass_not_implemented +from mojo_opset.utils.platform import get_torch_device def generate_paged_decode_data( @@ -44,7 +49,9 @@ def generate_paged_decode_data( max_total_seq_len = total_seq_lens.max().item() max_num_blocks_per_seq = (max_total_seq_len + block_size - 1) // block_size - total_blocks_needed = int(torch.div(total_seq_lens + block_size - 1, block_size, rounding_mode="floor").sum().item()) + total_blocks_needed = int( + torch.div(total_seq_lens + block_size - 1, block_size, rounding_mode="floor").sum().item() + ) if total_blocks_needed == 0: total_blocks_needed = batch_size * max_num_blocks_per_seq @@ -77,7 +84,7 @@ def generate_paged_decode_data( (8, 16, 4, 96, 1024, 128, torch.bfloat16, "M_BF16_PADDIM"), (8, 8, 1, 128, 8192, 1024, torch.bfloat16, "M_BF16_LONG"), (8, 8, 1, 128, 2048, 1024, torch.bfloat16, "M_BF16_BIGPAGE"), - (8, 8, 1, 128, 0, 1024, torch.bfloat16, "M_BF16_PADSEQ") + (8, 8, 1, 128, 0, 1024, torch.bfloat16, "M_BF16_PADSEQ"), ] @@ -222,7 +229,7 @@ def generate_paged_prefill_data( (2, 16, 4, 96, 1024, 0, 128, torch.bfloat16, "M_BF16_PADDIM"), (2, 8, 1, 128, 4096, 8192, 128, torch.bfloat16, "M_BF16_WITH_CACHE"), (2, 8, 1, 128, 1024, 2048, 1024, torch.bfloat16, "M_BF16_BIGPAGE"), - (2, 8, 1, 128, 0, 0, 1024, torch.bfloat16, "M_BF16_PADSEQ") + (2, 8, 1, 128, 0, 0, 1024, torch.bfloat16, "M_BF16_PADSEQ"), ] @@ -259,10 +266,7 @@ def test_paged_prefill_gqa( max_q_lens: int, max_total_seq_lens: int, ): - paged_prefill_attn = MojoPagedPrefillGQA( - is_causal=True, - gqa_layout=gqa_layout - ) + paged_prefill_attn = MojoPagedPrefillGQA(is_causal=True, gqa_layout=gqa_layout) paged_prefill_attn_ref = MojoPagedPrefillGQA._registry.get("torch")( is_causal=True, @@ -325,9 +329,7 @@ def test_paged_prefill_gqa_bucket_padded_varlen(gqa_layout: str): ) if type(paged_prefill_attn_ref) is type(paged_prefill_attn): - raise NotImplementedError( - f"both operands resolve to the same implementation, skipping comparison." - ) + raise NotImplementedError("both operands resolve to the same implementation, skipping comparison.") softmax_scale = 1.0 / math.sqrt(head_dim) out_ref = paged_prefill_attn_ref( @@ -399,7 +401,16 @@ def generate_diffusion_attn_test_data( @pytest.mark.parametrize( "bsz, q_head_num, kv_head_num, head_dim, seq_length, block_size", - [(1, 5, 1, 128, 2048, 32,)], + [ + ( + 1, + 5, + 1, + 128, + 2048, + 32, + ) + ], ) @bypass_not_implemented def test_sdpa( @@ -413,12 +424,8 @@ def test_sdpa( query, key, value, blockwise_diffusion_attn_mask, enable_gqa = generate_diffusion_attn_test_data( bsz, q_head_num, kv_head_num, head_dim, seq_length, block_size ) - diffusion_attn_ref = MojoSdpa._registry.get("torch")( - scale=1.0 / math.sqrt(query.shape[-1]), enable_gqa=enable_gqa - ) - diffusion_attn = MojoSdpa( - scale=1.0 / math.sqrt(query.shape[-1]), enable_gqa=enable_gqa - ) + diffusion_attn_ref = MojoSdpa._registry.get("torch")(scale=1.0 / math.sqrt(query.shape[-1]), enable_gqa=enable_gqa) + diffusion_attn = MojoSdpa(scale=1.0 / math.sqrt(query.shape[-1]), enable_gqa=enable_gqa) diffusion_attn_ref.forward_diff_with(diffusion_attn, query, key, value, blockwise_diffusion_attn_mask) @@ -426,6 +433,7 @@ def test_sdpa( # MojoDecodeGQA (non-paged) # =========================================================================== + @pytest.mark.parametrize( "B, Hq, Hkv, D, S", [(4, 16, 4, 128, 256), (2, 8, 1, 64, 512)], @@ -441,15 +449,22 @@ def test_decode_gqa(B, Hq, Hkv, D, S, gqa_layout): op = MojoDecodeGQA(gqa_layout=gqa_layout) op_ref = MojoDecodeGQA._registry.get("torch")(gqa_layout=gqa_layout) op.forward_diff_with( - op_ref, query, key, value, total_seq_lens, + op_ref, + query, + key, + value, + total_seq_lens, softmax_scale=1.0 / math.sqrt(D), - atol=1e-2, rtol=1e-2, + atol=1e-2, + rtol=1e-2, ) + # =========================================================================== # MojoDecodeMLA # =========================================================================== + @pytest.mark.parametrize( "B, H, d_nope, d_rope, d_v, d_c, S", [(4, 16, 96, 32, 128, 64, 256)], @@ -469,8 +484,13 @@ def test_decode_mla(B, H, d_nope, d_rope, d_v, d_c, S): op_ref.kv_b_proj.copy_(w) op.forward_diff_with( - op_ref, query, compressed_kv, k_pe, total_seq_lens, - atol=1e-2, rtol=1e-2, + op_ref, + query, + compressed_kv, + k_pe, + total_seq_lens, + atol=1e-2, + rtol=1e-2, ) @@ -478,6 +498,7 @@ def test_decode_mla(B, H, d_nope, d_rope, d_v, d_c, S): # MojoPrefillMLA # =========================================================================== + @pytest.mark.parametrize( "H, d_nope, d_rope, d_v, d_c", [(8, 64, 32, 64, 32)], @@ -500,8 +521,13 @@ def test_prefill_mla(H, d_nope, d_rope, d_v, d_c): op_ref.kv_b_proj.copy_(w) op.forward_diff_with( - op_ref, query, compressed_kv, k_pe, cu, - atol=1e-2, rtol=1e-2, + op_ref, + query, + compressed_kv, + k_pe, + cu, + atol=1e-2, + rtol=1e-2, ) @@ -554,6 +580,7 @@ def test_decode_mla_attn_sink_reference(): # MojoDecodeNSA # =========================================================================== + @pytest.mark.parametrize( "B, H, D, S", [(2, 8, 64, 256)], @@ -573,8 +600,13 @@ def test_decode_nsa(B, H, D, S): op_ref.gate_proj.copy_(g) op.forward_diff_with( - op_ref, query, key, value, total_seq_lens, - atol=1e-2, rtol=1e-2, + op_ref, + query, + key, + value, + total_seq_lens, + atol=1e-2, + rtol=1e-2, ) @@ -582,6 +614,7 @@ def test_decode_nsa(B, H, D, S): # MojoPrefillGQA (non-paged) # =========================================================================== + @pytest.mark.parametrize( "B, Hq, Hkv, D, S", [(2, 16, 4, 128, 64), (1, 8, 1, 64, 128)], @@ -600,9 +633,14 @@ def test_prefill_gqa(B, Hq, Hkv, D, S, gqa_layout): op = MojoPrefillGQA(is_causal=True, gqa_layout=gqa_layout) op_ref = MojoPrefillGQA._registry.get("torch")(is_causal=True, gqa_layout=gqa_layout) op.forward_diff_with( - op_ref, query, key, value, cu, + op_ref, + query, + key, + value, + cu, softmax_scale=1.0 / math.sqrt(D), - atol=2e-2, rtol=2e-2, + atol=2e-2, + rtol=2e-2, ) @@ -610,8 +648,10 @@ def test_prefill_gqa(B, Hq, Hkv, D, S, gqa_layout): # MojoPagedDecodeMLA # =========================================================================== -def _generate_paged_mla_decode_data(batch_size, num_heads, d_nope, d_rope, d_v, - kv_lora_rank, max_seq_len, block_size, dtype): + +def _generate_paged_mla_decode_data( + batch_size, num_heads, d_nope, d_rope, d_v, kv_lora_rank, max_seq_len, block_size, dtype +): query = torch.randn(batch_size, num_heads, d_nope + d_rope, dtype=dtype) if max_seq_len > 0: total_seq_lens = torch.randint(max_seq_len // 2, max_seq_len, (batch_size,), dtype=torch.int32).clamp(min=1) @@ -629,7 +669,7 @@ def _generate_paged_mla_decode_data(batch_size, num_heads, d_nope, d_rope, d_v, off = 0 for i in range(batch_size): n = (total_seq_lens[i].item() + block_size - 1) // block_size - block_tables[i, :n] = free[off:off + n] + block_tables[i, :n] = free[off : off + n] off += n return query, ckv_cache, kpe_cache, total_seq_lens, block_tables @@ -646,7 +686,15 @@ def _generate_paged_mla_decode_data(batch_size, num_heads, d_nope, d_rope, d_v, @bypass_not_implemented def test_paged_decode_mla(B, H, d_nope, d_rope, d_v, d_c, S, blk): query, ckv_cache, kpe_cache, total_seq_lens, bt = _generate_paged_mla_decode_data( - B, H, d_nope, d_rope, d_v, d_c, S, blk, torch.bfloat16, + B, + H, + d_nope, + d_rope, + d_v, + d_c, + S, + blk, + torch.bfloat16, ) op = MojoPagedDecodeMLA(H, d_nope, d_rope, d_v, d_c) op_ref = MojoPagedDecodeMLA._registry.get("torch")(H, d_nope, d_rope, d_v, d_c) @@ -656,8 +704,14 @@ def test_paged_decode_mla(B, H, d_nope, d_rope, d_v, d_c, S, blk): op_ref.kv_b_proj.copy_(w) op.forward_diff_with( - op_ref, query, ckv_cache, kpe_cache, total_seq_lens, bt, - atol=1e-2, rtol=1e-2, + op_ref, + query, + ckv_cache, + kpe_cache, + total_seq_lens, + bt, + atol=1e-2, + rtol=1e-2, ) @@ -665,8 +719,10 @@ def test_paged_decode_mla(B, H, d_nope, d_rope, d_v, d_c, S, blk): # MojoPagedPrefillMLA # =========================================================================== -def _generate_paged_mla_prefill_data(batch_size, num_heads, d_nope, d_rope, d_v, - kv_lora_rank, max_q_len, block_size, dtype): + +def _generate_paged_mla_prefill_data( + batch_size, num_heads, d_nope, d_rope, d_v, kv_lora_rank, max_q_len, block_size, dtype +): if max_q_len > 0: q_lens = torch.randint(max_q_len // 2, max_q_len, (batch_size,), dtype=torch.int32).clamp(min=1) else: @@ -690,7 +746,7 @@ def _generate_paged_mla_prefill_data(batch_size, num_heads, d_nope, d_rope, d_v, for i in range(batch_size): kl = kv_lens[i].item() nb = (kl + block_size - 1) // block_size - blocks = free[off:off + nb] + blocks = free[off : off + nb] block_tables[i, :nb] = blocks off += nb @@ -716,7 +772,15 @@ def _generate_paged_mla_prefill_data(batch_size, num_heads, d_nope, d_rope, d_v, @bypass_not_implemented def test_paged_prefill_mla(B, H, d_nope, d_rope, d_v, d_c, max_q, blk): query, ckv_cache, kpe_cache, cu, bt = _generate_paged_mla_prefill_data( - B, H, d_nope, d_rope, d_v, d_c, max_q, blk, torch.bfloat16, + B, + H, + d_nope, + d_rope, + d_v, + d_c, + max_q, + blk, + torch.bfloat16, ) op = MojoPagedPrefillMLA(H, d_nope, d_rope, d_v, d_c, is_causal=True) op_ref = MojoPagedPrefillMLA._registry.get("torch")(H, d_nope, d_rope, d_v, d_c, is_causal=True) @@ -726,8 +790,14 @@ def test_paged_prefill_mla(B, H, d_nope, d_rope, d_v, d_c, max_q, blk): op_ref.kv_b_proj.copy_(w) op.forward_diff_with( - op_ref, query, ckv_cache, kpe_cache, cu, bt, - atol=1e-2, rtol=1e-2, + op_ref, + query, + ckv_cache, + kpe_cache, + cu, + bt, + atol=1e-2, + rtol=1e-2, ) @@ -735,6 +805,7 @@ def test_paged_prefill_mla(B, H, d_nope, d_rope, d_v, d_c, max_q, blk): # MojoPagedDecodeNSA # =========================================================================== + @pytest.mark.parametrize( "B, H, D, S, blk", [(2, 8, 64, 256, 64)], @@ -742,8 +813,13 @@ def test_paged_prefill_mla(B, H, d_nope, d_rope, d_v, d_c, max_q, blk): @bypass_not_implemented def test_paged_decode_nsa(B, H, D, S, blk): query, k_cache, v_cache, total_seq_lens, bt, _ = generate_paged_decode_data( - batch_size=B, num_q_heads=H, num_kv_heads=H, - head_dim=D, max_seq_len=S, block_size=blk, dtype=torch.bfloat16, + batch_size=B, + num_q_heads=H, + num_kv_heads=H, + head_dim=D, + max_seq_len=S, + block_size=blk, + dtype=torch.bfloat16, ) cr, nsb, ws = 4, 4, 64 op = MojoPagedDecodeNSA(H, D, compress_ratio=cr, num_selected_blocks=nsb, window_size=ws) @@ -754,8 +830,14 @@ def test_paged_decode_nsa(B, H, D, S, blk): op_ref.gate_proj.copy_(g) op.forward_diff_with( - op_ref, query, k_cache, v_cache, total_seq_lens, bt, - atol=1e-2, rtol=1e-2, + op_ref, + query, + k_cache, + v_cache, + total_seq_lens, + bt, + atol=1e-2, + rtol=1e-2, ) @@ -763,6 +845,7 @@ def test_paged_decode_nsa(B, H, D, S, blk): # MojoPrefillNSA (non-paged) — small total_seq_lens to keep runtime manageable # =========================================================================== + @pytest.mark.parametrize( "H, D", [(4, 64)], @@ -779,15 +862,22 @@ def test_prefill_nsa(H, D): cr, nsb, ws = 4, 2, 16 op = MojoPrefillNSA(H, D, compress_ratio=cr, num_selected_blocks=nsb, window_size=ws, is_causal=True) - op_ref = MojoPrefillNSA._registry.get("torch")(H, D, compress_ratio=cr, num_selected_blocks=nsb, window_size=ws, is_causal=True) + op_ref = MojoPrefillNSA._registry.get("torch")( + H, D, compress_ratio=cr, num_selected_blocks=nsb, window_size=ws, is_causal=True + ) with torch.no_grad(): g = torch.randn_like(op.gate_proj) op.gate_proj.copy_(g) op_ref.gate_proj.copy_(g) op.forward_diff_with( - op_ref, query, key, value, cu, - atol=1e-2, rtol=1e-2, + op_ref, + query, + key, + value, + cu, + atol=1e-2, + rtol=1e-2, ) @@ -795,6 +885,7 @@ def test_prefill_nsa(H, D): # MojoPagedPrefillNSA — small total_seq_lens to keep runtime manageable # =========================================================================== + @pytest.mark.parametrize( "H, D, blk", [(4, 64, 32)], @@ -803,25 +894,37 @@ def test_prefill_nsa(H, D): def test_paged_prefill_nsa(H, D, blk): B = 2 query, k_cache, v_cache, cu, bt, _, _, _ = generate_paged_prefill_data( - batch_size=B, num_q_heads=H, num_kv_heads=H, - head_dim=D, max_q_len=32, max_kv_computed_len=0, - block_size=blk, dtype=torch.bfloat16, + batch_size=B, + num_q_heads=H, + num_kv_heads=H, + head_dim=D, + max_q_len=32, + max_kv_computed_len=0, + block_size=blk, + dtype=torch.bfloat16, ) cr, nsb, ws = 4, 2, 16 op = MojoPagedPrefillNSA(H, D, compress_ratio=cr, num_selected_blocks=nsb, window_size=ws, is_causal=True) - op_ref = MojoPagedPrefillNSA._registry.get("torch")(H, D, compress_ratio=cr, num_selected_blocks=nsb, window_size=ws, is_causal=True) + op_ref = MojoPagedPrefillNSA._registry.get("torch")( + H, D, compress_ratio=cr, num_selected_blocks=nsb, window_size=ws, is_causal=True + ) with torch.no_grad(): g = torch.randn_like(op.gate_proj) op.gate_proj.copy_(g) op_ref.gate_proj.copy_(g) op.forward_diff_with( - op_ref, query, k_cache, v_cache, cu, bt, - atol=1e-2, rtol=1e-2, + op_ref, + query, + k_cache, + v_cache, + cu, + bt, + atol=1e-2, + rtol=1e-2, ) - # =========================================================================== # MojoSWA # =========================================================================== @@ -842,25 +945,30 @@ def test_paged_prefill_nsa(H, D, blk): "query, k_cache, v_cache, cu_q_lens, block_tables, cu_total_seq_lens", [ pytest.param( - *(lambda d: (d[0], d[1], d[2], d[3], d[4], d[5]))(generate_paged_prefill_data( - batch_size=B, - num_q_heads=Q_H, - num_kv_heads=KV_H, - head_dim=D, - max_q_len=Q_LEN, - max_kv_computed_len=KV_COMPUTED_LEN, - block_size=BLK_S, - dtype=dtype, - )), + *(lambda d: (d[0], d[1], d[2], d[3], d[4], d[5]))( + generate_paged_prefill_data( + batch_size=B, + num_q_heads=Q_H, + num_kv_heads=KV_H, + head_dim=D, + max_q_len=Q_LEN, + max_kv_computed_len=KV_COMPUTED_LEN, + block_size=BLK_S, + dtype=dtype, + ) + ), id=ID, ) for B, Q_H, KV_H, D, Q_LEN, KV_COMPUTED_LEN, BLK_S, dtype, ID in test_configs_swa_prefill ], ) -@pytest.mark.parametrize("gqa_layout, global_window, local_window", [ - ("ABAB", 4, 255), - ("AABB", 4, 1023), -]) +@pytest.mark.parametrize( + "gqa_layout, global_window, local_window", + [ + ("ABAB", 4, 255), + ("AABB", 4, 1023), + ], +) @auto_switch_platform() @bypass_not_implemented def test_paged_prefill_swa( @@ -916,6 +1024,7 @@ def test_paged_prefill_swa( (2, 24, 8, 128, 2048, 1024, torch.bfloat16, "M_BF16_GROUP2"), ] + @pytest.mark.parametrize( "query, k_cache, v_cache, total_seq_lens, block_tables, max_total_seq_len", [ @@ -934,10 +1043,13 @@ def test_paged_prefill_swa( for B, Q_H, KV_H, D, S_LEN, BLK_S, dtype, ID in test_configs_swa_decode ], ) -@pytest.mark.parametrize("gqa_layout, global_window, local_window", [ - ("ABAB", 4, 255), - ("AABB", 4, 1023), -]) +@pytest.mark.parametrize( + "gqa_layout, global_window, local_window", + [ + ("ABAB", 4, 255), + ("AABB", 4, 1023), + ], +) @auto_switch_platform() @bypass_not_implemented def test_paged_decode_swa( @@ -983,6 +1095,202 @@ def test_paged_decode_swa( ) +def generate_conformer_varlen_attention_data( + q_lens: list[int], + cache_lens: list[int], + num_heads: int, + head_dim: int, + dtype: torch.dtype, +): + cu_q_lens = torch.tensor([0] + torch.cumsum(torch.tensor(q_lens, dtype=torch.int32), 0).tolist(), dtype=torch.int32) + kv_lens = [q_len + cache_len for q_len, cache_len in zip(q_lens, cache_lens)] + cu_total_seq_lens = torch.tensor( + [0] + torch.cumsum(torch.tensor(kv_lens, dtype=torch.int32), 0).tolist(), + dtype=torch.int32, + ) + query = torch.randn(cu_q_lens[-1].item(), num_heads, head_dim, dtype=dtype) + key = torch.randn(cu_total_seq_lens[-1].item(), num_heads, head_dim, dtype=dtype) + value = torch.randn_like(key) + return query, key, value, cu_q_lens, cu_total_seq_lens + + +def conformer_chunk_attention_golden( + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + cu_q_lens: torch.Tensor, + cu_total_seq_lens: torch.Tensor, + chunk_size: int, + left_context_chunks: int, + softmax_scale: Optional[float] = None, +) -> torch.Tensor: + _, _, head_dim = query.shape + scale = softmax_scale if softmax_scale is not None else 1.0 / math.sqrt(head_dim) + left_context_tokens = left_context_chunks * chunk_size + output = torch.zeros_like(query) + batch_size = cu_q_lens.shape[0] - 1 + + for b in range(batch_size): + q_start, q_end = cu_q_lens[b].item(), cu_q_lens[b + 1].item() + kv_start, kv_end = cu_total_seq_lens[b].item(), cu_total_seq_lens[b + 1].item() + q_len = q_end - q_start + kv_len = kv_end - kv_start + if q_len <= 0 or kv_len <= 0: + continue + + q = query[q_start:q_end].permute(1, 0, 2) + k_t = key[kv_start:kv_end].permute(1, 2, 0) + scores = torch.bmm(q, k_t).float() * scale + + q_abs_idx = torch.arange(kv_len - q_len, kv_len, device=query.device) + kv_idx = torch.arange(kv_len, device=query.device) + chunk_start = torch.div(q_abs_idx, chunk_size, rounding_mode="floor") * chunk_size + chunk_end = torch.clamp(chunk_start + chunk_size, max=kv_len) + if left_context_chunks < 0: + ctx_start = torch.zeros_like(chunk_start) + else: + ctx_start = torch.clamp(chunk_start - left_context_tokens, min=0) + chunk_mask = (ctx_start[:, None] <= kv_idx[None, :]) & (kv_idx[None, :] < chunk_end[:, None]) + scores = torch.where(chunk_mask.unsqueeze(0), scores, float("-inf")) + + probs = torch.softmax(scores, dim=-1, dtype=torch.float32).to(value.dtype) + v = value[kv_start:kv_end].permute(1, 0, 2) + output[q_start:q_end] = torch.bmm(probs, v).permute(1, 0, 2).to(output.dtype) + + return output + + +@pytest.mark.parametrize( + "q_lens, cache_lens, num_heads, head_dim, left_window, right_window, dtype", + [ + ([5, 7], [0, 0], 2, 96, 3, 2, torch.bfloat16), + ([1, 17, 9], [4, 0, 8], 4, 96, 8, 0, torch.bfloat16), + ([3], [2], 2, 128, 2, 1, torch.float32), + ([33, 11], [5, 13], 8, 96, 16, 4, torch.bfloat16), + ], +) +@auto_switch_platform() +@bypass_not_implemented +def test_conformer_sliding_window_attention( + q_lens: list[int], + cache_lens: list[int], + num_heads: int, + head_dim: int, + left_window: int, + right_window: int, + dtype: torch.dtype, +): + device = get_torch_device() + query, key, value, cu_q_lens, cu_total_seq_lens = generate_conformer_varlen_attention_data( + q_lens, + cache_lens, + num_heads, + head_dim, + dtype, + ) + query = query.to(device) + key = key.to(device) + value = value.to(device) + cu_q_lens = cu_q_lens.to(device) + cu_total_seq_lens = cu_total_seq_lens.to(device) + + op = MojoConformerSlidingWindowAttention(left_window=left_window, right_window=right_window) + op_ref = MojoConformerSlidingWindowAttention._registry.get("torch")( + left_window=left_window, right_window=right_window + ) + atol = 2e-2 if dtype != torch.float32 else 1e-5 + rtol = 2e-2 if dtype != torch.float32 else 1e-6 + op.forward_diff_with( + op_ref, + query, + key, + value, + cu_q_lens, + cu_total_seq_lens, + atol=atol, + rtol=rtol, + ) + + +@pytest.mark.parametrize( + "q_lens, cache_lens, num_heads, head_dim, chunk_size, left_context_chunks, dtype", + [ + ([662, 182], [0, 0], 2, 128, 4, -1, torch.bfloat16), + ([8, 172, 933], [4, 0, 8], 4, 128, 8, 1, torch.bfloat16), + ([872], [2], 2, 128, 2, 0, torch.float32), + ([33, 11], [5, 13], 8, 128, 8, 2, torch.bfloat16), + ([314, 272], [5, 3], 4, 128, 8, -1, torch.bfloat16), + ([478, 198], [2, 8], 4, 128, 32, -1, torch.bfloat16), + ([2300, 1100], [0, 0], 2, 128, 128, -1, torch.bfloat16), + ([249, 153], [0, 0], 4, 128, 8, 8, torch.bfloat16), + ([167, 263], [0, 0], 4, 128, 16, 0, torch.bfloat16), + ([121, 414], [0, 0], 2, 128, 32, 0, torch.float32), + ([347, 371], [0, 0], 4, 128, 16, 4, torch.bfloat16), + ([7, 11, 1443], [3, 5, 0], 8, 128, 8, -1, torch.bfloat16), + ([127, 61, 843], [0, 0, 0], 8, 128, 32, 2, torch.bfloat16), + ([257, 193], [0, 0], 4, 128, 64, -1, torch.bfloat16), + ([131, 927, 733], [31, 13, 7], 8, 96, 16, 0, torch.bfloat16), + ([211, 1769, 1451], [0, 0, 101], 8, 128, 32, -1, torch.bfloat16), + ([63, 47, 31, 17], [0, 0, 0, 0], 12, 64, 8, -1, torch.bfloat16), + ([101, 89], [0, 0], 16, 96, 16, 3, torch.bfloat16), + ([7, 13], [0, 0], 2, 64, 256, -1, torch.float32), + ([3, 5, 7], [0, 0, 2], 4, 96, 128, 0, torch.bfloat16), + ], +) +@auto_switch_platform() +@bypass_not_implemented +def test_conformer_chunk_attention( + q_lens: list[int], + cache_lens: list[int], + num_heads: int, + head_dim: int, + chunk_size: int, + left_context_chunks: int, + dtype: torch.dtype, +): + device = get_torch_device() + query, key, value, cu_q_lens, cu_total_seq_lens = generate_conformer_varlen_attention_data( + q_lens, + cache_lens, + num_heads, + head_dim, + dtype, + ) + query = query.to(device) + key = key.to(device) + value = value.to(device) + cu_q_lens = cu_q_lens.to(device) + cu_total_seq_lens = cu_total_seq_lens.to(device) + + op = MojoConformerChunkAttention(chunk_size=chunk_size, left_context_chunks=left_context_chunks) + op_ref = MojoConformerChunkAttention._registry.get("torch")( + chunk_size=chunk_size, left_context_chunks=left_context_chunks + ) + if type(op) is type(op_ref): + raise NotImplementedError("both operands resolve to the same implementation, skipping comparison.") + + q_block_indices = prepare_conformer_chunk_attention_q_block_indices(cu_q_lens, head_dim, dtype) + atol = 2e-2 if dtype != torch.float32 else 1e-5 + rtol = 2e-2 if dtype != torch.float32 else 1e-6 + + out_ref = op_ref( + query, + key, + value, + cu_q_lens, + cu_total_seq_lens, + ) + out = op( + query, + key, + value, + cu_q_lens, + cu_total_seq_lens, + q_block_indices=q_block_indices, + ) + torch.testing.assert_close(out.to(torch.float32), out_ref.to(torch.float32), atol=atol, rtol=rtol) + + def generate_sdpa_data( batch_size: int, num_q_heads: int, @@ -1011,7 +1319,6 @@ def generate_sdpa_data( key = torch.randn(total_kv_tokens, num_kv_heads, head_dim, dtype=dtype) value = torch.randn(total_kv_tokens, num_kv_heads, head_dim, dtype=dtype) - return query, key, value, cu_q_lens, cu_total_seq_lens @@ -1021,6 +1328,7 @@ def generate_sdpa_data( (2, 8, 1, 128, 1024, 2048, torch.bfloat16, "M_BF16_WITH_CACHE"), ] + @pytest.mark.parametrize( "query, key, value, cu_q_lens, cu_total_seq_lens", [ @@ -1039,10 +1347,13 @@ def generate_sdpa_data( for B, Q_H, KV_H, D, Q_LEN, KV_COMPUTED_LEN, dtype, ID in test_configs_swa_infer ], ) -@pytest.mark.parametrize("gqa_layout, global_window, local_window", [ - ("ABAB", 4, 255), - ("AABB", 4, 1023), -]) +@pytest.mark.parametrize( + "gqa_layout, global_window, local_window", + [ + ("ABAB", 4, 255), + ("AABB", 4, 1023), + ], +) @auto_switch_platform() @bypass_not_implemented def test_swa_infer( diff --git a/mojo_opset/tests/accuracy/operators/test_convolution.py b/mojo_opset/tests/accuracy/operators/test_convolution.py index 553dc186..ca507f78 100644 --- a/mojo_opset/tests/accuracy/operators/test_convolution.py +++ b/mojo_opset/tests/accuracy/operators/test_convolution.py @@ -5,6 +5,7 @@ from mojo_opset.tests.utils import bypass_not_implemented from mojo_opset import MojoCausalConv1dUpdateState +from mojo_opset import MojoConv1d @pytest.mark.parametrize( @@ -33,3 +34,47 @@ def test_causal_conv1d_update_state(B, T, D, W, act): assert_close(out, out_ref) assert_close(conv_state, conv_state_ref) + + +@pytest.mark.parametrize( + "B, C_in, C_out, T, K, stride, padding, dilation, groups, use_bias, dtype", + [ + (2, 8, 8, 32, 3, 1, "same", 1, None, True, torch.float32), + (2, 16, 24, 17, 5, 1, 2, 1, 1, False, torch.float32), + (1, 32, 32, 64, 7, 1, "same", 1, None, True, torch.float16), + (3, 12, 18, 19, 4, 1, 3, 1, 3, True, torch.bfloat16), + ], +) +@bypass_not_implemented +def test_conv1d(B, C_in, C_out, T, K, stride, padding, dilation, groups, use_bias, dtype): + resolved_groups = C_in if groups is None else groups + hidden_states = torch.randn(B, C_in, T, dtype=dtype) + weight = torch.randn(C_out, C_in // resolved_groups, K, dtype=dtype) + bias = torch.randn(C_out, dtype=dtype) if use_bias else None + + op = MojoConv1d(stride=stride, padding=padding, dilation=dilation, groups=groups) + op_ref = MojoConv1d._registry.get("torch")(stride=stride, padding=padding, dilation=dilation, groups=groups) + + out = op(hidden_states, weight, bias) + out_ref = op_ref(hidden_states, weight, bias) + + assert_close(out, out_ref) + + +def test_conv1d_matches_torch_conv1d(): + hidden_states = torch.randn(2, 4, 11, dtype=torch.float32) + weight = torch.randn(4, 1, 5, dtype=torch.float32) + bias = torch.randn(4, dtype=torch.float32) + + op = MojoConv1d._registry.get("torch")(stride=1, padding="same", dilation=1, groups=None) + out = op(hidden_states, weight, bias) + ref = torch.nn.functional.conv1d( + hidden_states, + weight, + bias, + stride=1, + padding="same", + dilation=1, + groups=hidden_states.shape[1], + ) + assert_close(out, ref) diff --git a/mojo_opset/tests/perf/benchmark.md b/mojo_opset/tests/perf/benchmark.md index 7d391089..0cad2b13 100644 --- a/mojo_opset/tests/perf/benchmark.md +++ b/mojo_opset/tests/perf/benchmark.md @@ -1,5 +1,34 @@ | Timestamp | Op Name | Parameters | Device Latency (us) | Host Latency (ms) | | ------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------- | +| 2026-05-12 15:10:55 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(896, 8, 64), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(896, 8, 64), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(896, 8, 64), dtype=torch.bfloat16, device=npu:0) | 113.0336 us | 0.4565 ms | +| 2026-05-12 15:11:07 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0) | 5595.4080 us | 5.8686 ms | +| 2026-05-12 15:11:19 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(896, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0) | 1819.3872 us | 2.0693 ms | +| 2026-05-12 15:11:29 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(5,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(5,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(363, 4, 64), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(243, 4, 64), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(363, 4, 64), dtype=torch.bfloat16, device=npu:0) | 20.5616 us | 0.2670 ms | +| 2026-05-12 15:11:39 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(576, 8, 64), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(576, 8, 64), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(576, 8, 64), dtype=torch.bfloat16, device=npu:0) | 52.7776 us | 0.3361 ms | +| 2026-05-12 15:11:44 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(896, 4, 64), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(896, 4, 64), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(896, 4, 64), dtype=torch.bfloat16, device=npu:0) | 71.1280 us | 0.3787 ms | +| 2026-05-12 15:11:53 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(768, 8, 96), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(768, 8, 96), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(768, 8, 96), dtype=torch.bfloat16, device=npu:0) | 58.6992 us | 0.3259 ms | +| 2026-05-12 15:12:03 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(448, 8, 96), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(448, 8, 96), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(448, 8, 96), dtype=torch.bfloat16, device=npu:0) | 29.4304 us | 0.3021 ms | +| 2026-05-12 15:12:15 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1152, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(1152, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1152, 8, 128), dtype=torch.bfloat16, device=npu:0) | 1396.4032 us | 1.6490 ms | +| 2026-05-12 15:12:25 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(2304, 8, 64), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(2304, 8, 64), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(2304, 8, 64), dtype=torch.bfloat16, device=npu:0) | 365.8480 us | 0.6224 ms | +| 2026-05-12 15:12:35 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(5,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(5,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1920, 4, 96), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(1920, 4, 96), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1920, 4, 96), dtype=torch.bfloat16, device=npu:0) | 164.6960 us | 0.3984 ms | +| 2026-05-12 15:12:47 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(5,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(5,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1760, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(1280, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1760, 8, 128), dtype=torch.bfloat16, device=npu:0) | 1149.5104 us | 1.4079 ms | +| 2026-05-12 15:12:57 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(896, 16, 64), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(896, 16, 64), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(896, 16, 64), dtype=torch.bfloat16, device=npu:0) | 112.8864 us | 0.4559 ms | +| 2026-05-12 15:13:07 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(992, 12, 96), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(768, 12, 96), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(992, 12, 96), dtype=torch.bfloat16, device=npu:0) | 83.7264 us | 0.3671 ms | +| 2026-05-12 20:02:06 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(5504, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(5504, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(5504, 8, 128), dtype=torch.bfloat16, device=npu:0) | 3619.3168 us | 3.8710 ms | +| 2026-05-12 20:03:27 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1024, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(1024, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1024, 8, 128), dtype=torch.bfloat16, device=npu:0) | 135.4672 us | 0.3848 ms | +| 2026-05-12 21:19:42 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(8192, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(8192, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(8192, 8, 128), dtype=torch.bfloat16, device=npu:0) | 4422.1584 us | 4.6779 ms | +| 2026-05-13 10:44:37 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(4096, 1, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(4096, 1, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(4096, 1, 128), dtype=torch.bfloat16, device=npu:0) | 425.2784 us | 0.7056 ms | +| 2026-05-13 10:44:47 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(6,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(6,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1291, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(1291, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1291, 8, 128), dtype=torch.bfloat16, device=npu:0) | 159.0864 us | 0.4086 ms | +| 2026-05-13 11:09:31 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(4096, 1, 128), dtype=torch.bfloat16, device=npu:0)
q_block_indices: Tensor(shape=(43, 2), dtype=torch.int32, device=npu:0)
query: Tensor(shape=(4096, 1, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(4096, 1, 128), dtype=torch.bfloat16, device=npu:0) | 425.4448 us | 0.6950 ms | +| 2026-05-13 11:12:19 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(8192, 8, 128), dtype=torch.bfloat16, device=npu:0)
q_block_indices: Tensor(shape=(86, 2), dtype=torch.int32, device=npu:0)
query: Tensor(shape=(8192, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(8192, 8, 128), dtype=torch.bfloat16, device=npu:0) | 8739.1136 us | 9.0725 ms | +| 2026-05-13 11:52:53 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(8192, 1, 128), dtype=torch.bfloat16, device=npu:0)
q_block_indices: Tensor(shape=(86, 2), dtype=torch.int32, device=npu:0)
query: Tensor(shape=(8192, 1, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(8192, 1, 128), dtype=torch.bfloat16, device=npu:0) | 1125.2064 us | 1.2892 ms | +| 2026-05-13 11:53:06 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(6,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(6,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1291, 8, 128), dtype=torch.bfloat16, device=npu:0)
q_block_indices: Tensor(shape=(16, 2), dtype=torch.int32, device=npu:0)
query: Tensor(shape=(1291, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1291, 8, 128), dtype=torch.bfloat16, device=npu:0) | 137.8464 us | 0.2839 ms | +| 2026-05-13 15:30:11 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(2,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(8192, 1, 128), dtype=torch.bfloat16, device=npu:0)
q_block_indices: Tensor(shape=(64, 2), dtype=torch.int32, device=npu:0)
query: Tensor(shape=(8192, 1, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(8192, 1, 128), dtype=torch.bfloat16, device=npu:0) | 470.3040 us | 0.6117 ms | +| 2026-05-13 15:30:24 | TTXConformerChunkAttention | cu_q_lens: Tensor(shape=(6,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(6,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1291, 8, 128), dtype=torch.bfloat16, device=npu:0)
q_block_indices: Tensor(shape=(12, 2), dtype=torch.int32, device=npu:0)
query: Tensor(shape=(1291, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1291, 8, 128), dtype=torch.bfloat16, device=npu:0) | 72.8688 us | 0.2708 ms | +| 2026-05-08 20:30:08 | TTXConformerSlidingWindowAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(896, 8, 64), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(896, 8, 64), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(896, 8, 64), dtype=torch.bfloat16, device=npu:0) | 64.1904 us | 0.3512 ms | +| 2026-05-08 20:30:17 | TTXConformerSlidingWindowAttention | cu_q_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(3,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0) | 171.8704 us | 0.3918 ms | +| 2026-05-08 20:30:27 | TTXConformerSlidingWindowAttention | cu_q_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(4,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(896, 8, 128), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(1792, 8, 128), dtype=torch.bfloat16, device=npu:0) | 106.4800 us | 0.3336 ms | +| 2026-05-08 20:30:37 | TTXConformerSlidingWindowAttention | cu_q_lens: Tensor(shape=(5,), dtype=torch.int32, device=npu:0)
cu_total_seq_lens: Tensor(shape=(5,), dtype=torch.int32, device=npu:0)
key: Tensor(shape=(363, 4, 64), dtype=torch.bfloat16, device=npu:0)
query: Tensor(shape=(243, 4, 64), dtype=torch.bfloat16, device=npu:0)
value: Tensor(shape=(363, 4, 64), dtype=torch.bfloat16, device=npu:0) | 23.1568 us | 0.2657 ms | | 2026-01-22 14:25:55 | TTXGelu | x: Tensor(shape=(128, 128), dtype=torch.float32, device=npu:0) | 5.1760 us | 0.2676 ms | | 2026-01-22 15:16:14 | TTXGroupGemm | group_list: Tensor(shape=(8,), dtype=torch.int64, device=npu:0) input: Tensor(shape=(20480, 4096), dtype=torch.float16, device=npu:0) | 2381.0754 us | 2.5413 ms | | 2026-01-22 15:16:20 | TTXGroupGemm | group_list: Tensor(shape=(8,), dtype=torch.int64, device=npu:0) input: Tensor(shape=(20480, 4096), dtype=torch.bfloat16, device=npu:0) | 2352.0550 us | 2.5800 ms | @@ -84,6 +113,7 @@ | 2026-02-04 17:20:51 | TorchGroupGemm | group_list: Tensor(shape=(8,), dtype=torch.int64, device=npu:0)
input: Tensor(shape=(20480, 4096), dtype=torch.float16, device=npu:0) | 3266.7656 us | 3.0046 ms | | 2026-02-04 17:20:55 | TorchGroupGemm | group_list: Tensor(shape=(8,), dtype=torch.int64, device=npu:0)
input: Tensor(shape=(20480, 4096), dtype=torch.bfloat16, device=npu:0) | 3195.7736 us | 2.9804 ms | | 2026-03-27 18:01:47 | TorchJoinProbRejectSampling | draft_probs: Tensor(shape=(15, 3), dtype=torch.float32, device=npu:0)
draft_tokens: Tensor(shape=(15, 3), dtype=torch.int64, device=npu:0)
target_logits: Tensor(shape=(15, 4, 155136), dtype=torch.float32, device=npu:0) | 145.2254 us | 0.1935 ms | +| 2026-05-13 15:30:16 | TorchNpuCausalFusionAttention | atten_mask: Tensor(shape=(8192, 8192), dtype=torch.bool, device=npu:0)
key_fa: Tensor(shape=(1, 1, 8192, 128), dtype=torch.bfloat16, device=npu:0)
query_fa: Tensor(shape=(1, 1, 8192, 128), dtype=torch.bfloat16, device=npu:0)
scale: 0.08838834764831843
value_fa: Tensor(shape=(1, 1, 8192, 128), dtype=torch.bfloat16, device=npu:0) | 374.3536 us | 0.4506 ms | | 2026-03-18 19:13:37 | TorchNpuGroupQuantMatmulReduceSum | x1: Tensor(shape=(8, 512, 128), dtype=torch.int8, device=npu:0)
x1_scale: Tensor(shape=(8, 512), dtype=torch.float32, device=npu:0)
x2: Tensor(shape=(8, 128, 256), dtype=torch.int8, device=npu:0)
x2_scale: Tensor(shape=(256,), dtype=torch.bfloat16, device=npu:0) | 29.2568 us | 0.1599 ms | | 2026-03-18 19:13:42 | TorchNpuGroupQuantMatmulReduceSum | x1: Tensor(shape=(4, 1024, 128), dtype=torch.int8, device=npu:0)
x1_scale: Tensor(shape=(4, 1024), dtype=torch.float32, device=npu:0)
x2: Tensor(shape=(4, 128, 512), dtype=torch.int8, device=npu:0)
x2_scale: Tensor(shape=(512,), dtype=torch.bfloat16, device=npu:0) | 30.7440 us | 0.1567 ms | | 2026-03-18 19:13:37 | TorchNpuQuantBatchGemmReduceSum | x1: Tensor(shape=(8, 512, 128), dtype=torch.int8, device=npu:0)
x1_scale: Tensor(shape=(8, 512), dtype=torch.float32, device=npu:0)
x2_scale: Tensor(shape=(256,), dtype=torch.bfloat16, device=npu:0) | 29.2568 us | 0.1599 ms | diff --git a/mojo_opset/tests/perf/test_attention.py b/mojo_opset/tests/perf/test_attention.py index 178bea3f..e72c95d5 100644 --- a/mojo_opset/tests/perf/test_attention.py +++ b/mojo_opset/tests/perf/test_attention.py @@ -5,14 +5,18 @@ import pytest import torch +from mojo_opset import MojoConformerChunkAttention +from mojo_opset import MojoConformerSlidingWindowAttention from mojo_opset import MojoPagedDecodeGQA +from mojo_opset import MojoPagedDecodeSWA from mojo_opset import MojoPagedPrefillGQA -from mojo_opset import MojoSdpa from mojo_opset import MojoPagedPrefillSWA -from mojo_opset import MojoPagedDecodeSWA +from mojo_opset import MojoSdpa from mojo_opset import MojoSWA +from mojo_opset.backends.ttx.kernels import prepare_conformer_chunk_attention_q_block_indices from mojo_opset.tests.utils import auto_switch_platform from mojo_opset.tests.utils import bypass_not_implemented +from mojo_opset.utils.platform import get_torch_device def generate_paged_decode_data( @@ -29,7 +33,9 @@ def generate_paged_decode_data( total_seq_lens = torch.randint(1, max_seq_len, (batch_size,), dtype=torch.int32) max_num_blocks_per_seq = (total_seq_lens.max().item() + block_size - 1) // block_size - total_blocks_needed = int(torch.div(total_seq_lens + block_size - 1, block_size, rounding_mode="floor").sum().item()) + total_blocks_needed = int( + torch.div(total_seq_lens + block_size - 1, block_size, rounding_mode="floor").sum().item() + ) if total_blocks_needed == 0: total_blocks_needed = batch_size * max_num_blocks_per_seq @@ -179,8 +185,10 @@ def generate_paged_prefill_data( k_cache[physical_block_id, :, :tokens_in_block, :] = k_slice v_cache[physical_block_id, :, :tokens_in_block, :] = v_slice - cu_total_seq_lens = None if kv_cache_lens is None else torch.cat( - [torch.tensor([0], dtype=torch.int32), torch.cumsum(kv_lens, 0).to(torch.int32)] + cu_total_seq_lens = ( + None + if kv_cache_lens is None + else torch.cat([torch.tensor([0], dtype=torch.int32), torch.cumsum(kv_lens, 0).to(torch.int32)]) ) return query, k_cache, v_cache, cu_q_lens, block_tables, cu_total_seq_lens @@ -244,6 +252,114 @@ def test_paged_prefill_gqa( ) +def _make_cu_lens(lengths: list[int], device: torch.device | str) -> torch.Tensor: + return torch.tensor( + [0] + torch.cumsum(torch.tensor(lengths, dtype=torch.int32), 0).tolist(), dtype=torch.int32, device=device + ) + + +class TorchNpuCausalFusionAttention: + def __call__( + self, + query: torch.Tensor, + key: torch.Tensor, + value: torch.Tensor, + atten_mask: torch.Tensor, + scale: float, + ) -> torch.Tensor: + import torch_npu + + return torch_npu.npu_fusion_attention( + query, + key, + value, + query.shape[1], + input_layout="BNSD", + padding_mask=None, + atten_mask=atten_mask, + scale=scale, + keep_prob=1.0, + pre_tockens=65535, + next_tockens=65535, + sparse_mode=0, + )[0] + + +@pytest.mark.parametrize( + "q_lens, cache_lens, num_heads, head_dim, left_window, right_window, dtype", + [ + ([512, 384], [0, 0], 8, 64, 128, 0, torch.bfloat16), + ([1024, 768], [0, 0], 8, 128, 256, 8, torch.bfloat16), + ([512, 256, 128], [512, 256, 128], 8, 128, 256, 0, torch.bfloat16), + ([128, 65, 33, 17], [64, 32, 16, 8], 4, 64, 64, 4, torch.bfloat16), + ], +) +@auto_switch_platform(set_perf=True) +@bypass_not_implemented +def test_conformer_sliding_window_attention( + q_lens: list[int], + cache_lens: list[int], + num_heads: int, + head_dim: int, + left_window: int, + right_window: int, + dtype: torch.dtype, +): + device = get_torch_device() + kv_lens = [q_len + cache_len for q_len, cache_len in zip(q_lens, cache_lens)] + cu_q_lens = _make_cu_lens(q_lens, device) + cu_total_seq_lens = _make_cu_lens(kv_lens, device) + query = torch.randn(cu_q_lens[-1].item(), num_heads, head_dim, dtype=dtype, device=device) + key = torch.randn(cu_total_seq_lens[-1].item(), num_heads, head_dim, dtype=dtype, device=device) + value = torch.randn_like(key) + + op = MojoConformerSlidingWindowAttention(left_window=left_window, right_window=right_window) + perf(lambda: op(query, key, value, cu_q_lens, cu_total_seq_lens)) # noqa: F821 + + +@pytest.mark.parametrize( + "q_lens, cache_lens, num_heads, head_dim, chunk_size, left_context_chunks, dtype", + [ + ([8192], [0], 1, 128, 1, -1, torch.bfloat16), + ([102, 768, 334, 5, 82], [0, 0, 0, 0, 0], 8, 128, 8, -1, torch.bfloat16), + ], +) +@auto_switch_platform(set_perf=True) +@bypass_not_implemented +def test_conformer_chunk_attention( + q_lens: list[int], + cache_lens: list[int], + num_heads: int, + head_dim: int, + chunk_size: int, + left_context_chunks: int, + dtype: torch.dtype, +): + device = get_torch_device() + kv_lens = [q_len + cache_len for q_len, cache_len in zip(q_lens, cache_lens)] + cu_q_lens = _make_cu_lens(q_lens, device) + cu_total_seq_lens = _make_cu_lens(kv_lens, device) + query = torch.randn(cu_q_lens[-1].item(), num_heads, head_dim, dtype=dtype, device=device) + key = torch.randn(cu_total_seq_lens[-1].item(), num_heads, head_dim, dtype=dtype, device=device) + value = torch.randn_like(key) + + op = MojoConformerChunkAttention(chunk_size=chunk_size, left_context_chunks=left_context_chunks) + q_block_indices = prepare_conformer_chunk_attention_q_block_indices(cu_q_lens, head_dim, dtype) + perf(lambda: op(query, key, value, cu_q_lens, cu_total_seq_lens, q_block_indices=q_block_indices)) # noqa: F821 + + if chunk_size == 1 and left_context_chunks < 0 and len(q_lens) == 1 and all(cache_len == 0 for cache_len in cache_lens): + query_fa = query.transpose(0, 1).unsqueeze(0).contiguous() + key_fa = key.transpose(0, 1).unsqueeze(0).contiguous() + value_fa = value.transpose(0, 1).unsqueeze(0).contiguous() + atten_mask = torch.triu( + torch.ones((q_lens[0], q_lens[0]), dtype=torch.bool, device=device), + diagonal=1, + ) + scale = 1.0 / math.sqrt(head_dim) + torch_npu_causal_fa = TorchNpuCausalFusionAttention() + perf(lambda: torch_npu_causal_fa(query_fa, key_fa, value_fa, atten_mask, scale)) # noqa: F821 + + def generate_test_data( bsz: int, q_head_num: int, @@ -280,9 +396,7 @@ def test_sdpa( blockwise_diffusion_attn_mask: torch.Tensor, enable_gqa: bool, ): - diffusion_attn = MojoSdpa( - scale=1.0 / math.sqrt(query.shape[-1]), enable_gqa=enable_gqa - ) + diffusion_attn = MojoSdpa(scale=1.0 / math.sqrt(query.shape[-1]), enable_gqa=enable_gqa) perf(lambda: diffusion_attn(query, key, value, blockwise_diffusion_attn_mask)) # noqa: F821 @@ -312,9 +426,12 @@ def test_sdpa( for B, Q_H, KV_H, D, Q_LEN, KV_COMPUTED_LEN, BLK_S, dtype, ID in test_configs_swa_prefill ], ) -@pytest.mark.parametrize("gqa_layout, global_window, local_window", [ - ("AABB", 4, 1023), -]) +@pytest.mark.parametrize( + "gqa_layout, global_window, local_window", + [ + ("AABB", 4, 1023), + ], +) @auto_switch_platform(set_perf=True) @bypass_not_implemented def test_paged_prefill_swa( @@ -351,8 +468,6 @@ def test_paged_prefill_swa( ) - - test_configs_swa_decode = [ (8, 16, 4, 128, 1024, 32, torch.bfloat16, "M_BF16"), (8, 16, 4, 96, 1024, 128, torch.bfloat16, "M_BF16_PADDIM"), @@ -378,9 +493,12 @@ def test_paged_prefill_swa( for B, Q_H, KV_H, D, S_LEN, BLK_S, dtype, ID in test_configs_swa_decode ], ) -@pytest.mark.parametrize("gqa_layout, global_window, local_window", [ - ("AABB", 4, 1023), -]) +@pytest.mark.parametrize( + "gqa_layout, global_window, local_window", + [ + ("AABB", 4, 1023), + ], +) @auto_switch_platform(set_perf=True) @bypass_not_implemented def test_paged_decode_swa( @@ -445,6 +563,7 @@ def generate_sdpa_data( return query, key, value, cu_q_lens, cu_total_seq_lens + test_configs_swa_infer = [ (2, 16, 4, 128, 1024, 0, torch.bfloat16, "M_BF16"), (2, 16, 4, 96, 1024, 0, torch.bfloat16, "M_BF16_PADDIM"), @@ -470,9 +589,12 @@ def generate_sdpa_data( for B, Q_H, KV_H, D, Q_LEN, KV_COMPUTED_LEN, dtype, ID in test_configs_swa_infer ], ) -@pytest.mark.parametrize("gqa_layout, global_window, local_window", [ - ("AABB", 4, 1023), -]) +@pytest.mark.parametrize( + "gqa_layout, global_window, local_window", + [ + ("AABB", 4, 1023), + ], +) @auto_switch_platform(set_perf=True) @bypass_not_implemented def test_swa_infer( diff --git a/mojo_opset/tests/perf/test_convolution.py b/mojo_opset/tests/perf/test_convolution.py index 3e1145a6..347ab6c7 100644 --- a/mojo_opset/tests/perf/test_convolution.py +++ b/mojo_opset/tests/perf/test_convolution.py @@ -3,6 +3,7 @@ import torch from mojo_opset import MojoCausalConv1dUpdateState +from mojo_opset import MojoConv1d from mojo_opset.tests.utils import auto_switch_platform from mojo_opset.tests.utils import bypass_not_implemented @@ -36,3 +37,32 @@ def test_causal_conv1d_update_state(hidden_states, conv_state, weight, bias, act conv_state_ref = conv_state.clone() perf(lambda: causal_conv1d(hidden_states, conv_state, weight, bias, activation)) # noqa: F821 perf(lambda: causal_conv1d_ref(hidden_states, conv_state_ref, weight, bias, activation)) # noqa: F821 + + +@pytest.mark.parametrize( + "hidden_states, weight, bias, stride, padding, dilation, groups", + [ + ( + torch.randn(B, C_in, T, dtype=dtype), + torch.randn(C_out, C_in // resolved_groups, K, dtype=dtype), + torch.randn(C_out, dtype=dtype) if use_bias else None, + stride, + padding, + dilation, + groups, + ) + for (B, C_in, C_out, T, K, stride, padding, dilation, groups, use_bias, dtype) in [ + (2, 256, 256, 512, 5, 1, "same", 1, None, True, torch.float16), + (2, 512, 768, 1024, 7, 1, 3, 1, 1, True, torch.bfloat16), + (1, 128, 128, 2048, 31, 1, "same", 1, None, False, torch.bfloat16), + ] + for resolved_groups in [C_in if groups is None else groups] + ], +) +@auto_switch_platform(set_perf=True) +@bypass_not_implemented +def test_conv1d(hidden_states, weight, bias, stride, padding, dilation, groups): + op = MojoConv1d(stride=stride, padding=padding, dilation=dilation, groups=groups) + op_ref = MojoConv1d._registry.get("torch")(stride=stride, padding=padding, dilation=dilation, groups=groups) + perf(lambda: op(hidden_states, weight, bias)) # noqa: F821 + perf(lambda: op_ref(hidden_states, weight, bias)) # noqa: F821