Download build/torch-neuron/nki_kernels/attention_inner.py from aws-neuron/vit-encoder-neuron-kernels: direct link, hf CLI and curl.
- Browser
- Download file 58.7 kB
-
https://huggingface.co/kernels/aws-neuron/vit-encoder-neuron-kernels/resolve/v1/build/torch-neuron/nki_kernels/attention_inner.py
- Command line
-
hf download hf://aws-neuron/vit-encoder-neuron-kernels@v1/build/torch-neuron/nki_kernels/attention_inner.py
-
curl -L -o attention_inner.py https://huggingface.co/kernels/aws-neuron/vit-encoder-neuron-kernels/resolve/v1/build/torch-neuron/nki_kernels/attention_inner.py
58.7 kB
| # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"). | |
| # You may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # | |
| # MODIFIED from the AWS Neuron SDK's nkilib ``core/attention/attention_cte.py`` | |
| # (Apache-2.0, Copyright Amazon.com, Inc. or its affiliates). Modifications: | |
| # 1. Four engine-placement edits (two memsets -> GpSimd, the tp_out eviction | |
| # tensor_copy -> Scalar, the list-branch write-back tensor_copy -> GpSimd), | |
| # measured worth ~1.19x end-to-end in the V-JEPA2 encoder. | |
| # 2. Code paths unreachable for this model's configuration removed (causal/SWA | |
| # masks, prefix caching, flash sectioning at seq<=10240, sinks, fp8, | |
| # sequence packing, CP) -- the executed instruction stream is unchanged. | |
| # The original supports the general case; use nkilib for anything but this | |
| # non-causal, single-section, SBUF-KV configuration. | |
| """K2 inner attention: the vendor ``_attention_cte`` context-encoding kernel, FLATTENED to | |
| exactly the branches the 4-kernel V-JEPA2 encoder reaches, with the four h8/h10 engine pins | |
| applied in place. Self-contained: imports only ``nki``, ``nkilib``, ``torch`` (none here) -- | |
| NO imports from ``experiment/`` or the repo-root incumbent modules. | |
| WHAT THIS COMPUTES | |
| ================== | |
| ``Output = softmax(scale * Q @ K^T) @ V`` for ONE head, called 8x per core by the K2 outer | |
| kernel (``attention_block.py``). The pipeline, in order, per 128-token Q group: | |
| load_q -> QK^T (MM1) + row-max -> update running max -> exp (softmax numerator) | |
| -> P@V (MM2) -> write-back (reciprocal-normalise, evict to the caller's SBUF O). | |
| Group software-pipelining overlaps three groups at a time (grp_i does PV+writeback, grp_i+1 | |
| does exp, grp_i+2 does load-Q + QK/max), which is the vendor's Level-4 loop structure. | |
| PSUM BANK PLAN (this config, tp_out=True, single 2048 section) | |
| -------------------------------------------------------------- | |
| * MM1 (QK^T) tiles: banks {0,1,2,3} (``address=(0, (k_tile_idx % 4) * PSUM_BANK_SIZE)``). | |
| * MM2 (P@V) tiles: banks {4,5,6,7} (``address=(0, (4 + large_tile_idx % 4) * ...)``). | |
| * tp reciprocal / correction transposes: bank {3 or 7} (``((grp%2)*4 + 3) * ...``). | |
| The producer (K2 outer) owns banks 5/6/7 during its phase; the attention region runs after the | |
| producer's high-water mark, so there is no live overlap. | |
| RING / MULTI-BUFFER DEPTHS (from the dp/dp2/dp4 fork lineage; kept verbatim) | |
| --------------------------------------------------------------------------- | |
| * ``exp_sb`` group-axis depth 2 (dp2): exp(g+1) can run while transpose(g) still drains. | |
| * ``exp_tp_sb`` group-axis depth 8 (dp4): P^T transposes issue as soon as their exp output | |
| exists, spreading DMA traffic out of the P.V collision window. | |
| THE FOUR ENGINE PINS (h8/h10), and WHY each exists (one line each, from PERF_INVENTORY) | |
| --------------------------------------------------------------------------------------- | |
| 1. ``_scale_reciprocal_write_back_impl`` tp_out PSUM->SBUF evict ``tensor_copy`` -> SCALAR | |
| (h8 site 1): the un-pinned copy defaults to Vector, K2's binding engine; Scalar/Vector are | |
| the only legal engines (PSUM source excludes GpSimd) and Scalar is the exercised majority. | |
| 2. ``_write_back_o_impl`` tp_out list-branch ``tensor_copy`` -> GPSIMD (h8 site 3): SBUF->SBUF | |
| fp32->bf16 CAST, all three engines legal, GpSimd is the idle engine so the copy sheds off | |
| Vector. | |
| 3. ``_qk_and_max_impl`` ``memset`` -> GPSIMD (h10 :2876): initialises mm1_partial_max, first | |
| consumer is several ops downstream, so a slower idle engine is safe and sheds Vector. | |
| 4. ``_fused_qkmax_and_pv_impl`` ``memset`` -> GPSIMD (h10 :3294): same rationale, 2688/core. | |
| The vendor ``_exp_impl`` memset (dp4 :2993) stays UNPINNED (engine.unknown / Vector) on purpose | |
| -- pinning it would fork ``_exp_impl`` and re-home the softmax-exp source strings; it MUST stay | |
| unpinned to reproduce the shipping instruction stream. | |
| DEAD PATHS REMOVED (§4 of PERF_INVENTORY) -- guard that made each unreachable for this call: | |
| ``causal_mask=False, use_sbuf_kv=True, seqlen_k=2048<10240, bs=1, tp_out=True, fuse_v_proj=False, | |
| no prior/APC/SWA/CP/seqpack/sink, cache_softmax=False``. See per-function docstrings. | |
| The allocator usage (``nkilib`` ``ModularAllocator``) is kept EXACTLY as the vendor uses it. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from typing import Any, List, Optional | |
| import nki | |
| import nki.isa as nisa | |
| import nki.language as nl | |
| from nki.isa import engine, reduce_cmd | |
| from nki.language.opcode import maximum as _maximum | |
| from nkilib.core.utils.allocator import align_to | |
| from nkilib.core.utils.kernel_assert import assert_shape, kernel_assert | |
| from nkilib.core.utils.kernel_helpers import ( | |
| PSUM_BANK_SIZE, | |
| div_ceil, | |
| get_verified_program_sharding_info, | |
| is_sbuf_tensor, | |
| ) | |
| from nkilib.core.utils.logging import get_logger | |
| from nkilib.core.utils.modular_allocator import ModularAllocator | |
| from nkilib.core.utils.stream_shuffle_broadcast import stream_shuffle_broadcast | |
| logger = get_logger("attention_inner") | |
| _FLOAT32_MIN = -3.4028235e38 # used for initialization and masking | |
| # Tested-range constraints (checked in _attention_cte). | |
| _MAX_BS = 512 | |
| _MAX_SEQLEN = 131072 | |
| _MAX_BS_TIMES_SEQLEN_QK = 32.0 * 36864 * 36864 | |
| _MAX_HEAD_DIM = 128 | |
| _MIN_GLOBAL_CP_DEGREE = 1 | |
| _MAX_GLOBAL_CP_DEGREE = 32 | |
| # Tile size / threshold constants used by the LIVE path. | |
| _Q_GRP_SZ = 128 | |
| _V_TILE_SZ = 128 # V tile size for loading and MM2 operations | |
| import os as _os | |
| _WB_FUSED = _os.environ.get("VIT_ENC_WB_FUSED", "0") == "1" # fused normalise+cast write-back: OFF (measured +6.6% at N=2048) | |
| _MM1_WIDE = _os.environ.get("VIT_ENC_MM1_WIDE", "1") == "1" # set 0 to restore the v1.3 4-bank MM1 layout | |
| _MM1_BANKS = (0, 1, 2, 5, 6) # MM1 rotation; bank 4 = P.V (single large tile), 3/7 = reciprocal transposes | |
| _MM1_WIDE_MAX_KTILES = 2 # only widen when <= this many key tiles per group: at 4 key tiles (N=2048) the | |
| # 5-bank rotation collides with P.V/producer banks and measured +7% slower | |
| _K_TILE_SZ = 512 # K tile size for loading and MM1+masking operations | |
| _EXP_TILE_SZ = 512 # Tile size for exp instructions (must equal _K_TILE_SZ) | |
| _LARGE_TILE_SZ = 2048 # Larger tile size for allocations/pipelining (4 x 512 tiles) | |
| _FLASH_ATTENTION_THRESHOLD = 10 * 1024 # Use flash attention above this K/V length | |
| _FLASH_ATTENTION_SECTION_LENGTH = 8 * 1024 # Section size when using flash attention | |
| # ========================================================================================== | |
| # Buffer / config dataclasses (kept verbatim -- the four required by the reached path). | |
| # ========================================================================================== | |
| class AttnConfig(nl.NKIObject): | |
| """High-level attention configuration set at kernel entry point.""" | |
| seqlen_q: int = None | |
| seqlen_k_active: int = None | |
| seqlen_k_prior: int = None | |
| d: int = None | |
| tp_q: bool = None | |
| tp_k: bool = None | |
| tp_out: bool = None | |
| is_prefix_caching: bool = None | |
| causal_mask: bool = None | |
| use_swa: bool = None | |
| sliding_window: int = None | |
| use_cp: bool = None | |
| global_cp_deg: int = None | |
| cp_strided_q_slicing: bool = None | |
| cp_striped_input: bool = None | |
| scale: float = None | |
| cache_softmax: bool = None | |
| skip_output_normalization: bool = None | |
| dtype: Any = None | |
| softmax_dtype: Any = None | |
| mm_out_dtype: Any = None | |
| is_sequence_packed: bool = None | |
| class AttnInternalBuffers(nl.NKIObject): | |
| """Container for all SBUF and PSUM tensor buffers used in attention computation.""" | |
| q_sb = None | |
| k_sb = None | |
| v_sb = None | |
| k_sb_prior = None | |
| v_sb_prior = None | |
| k_scale_sb = None | |
| mm1_psum = None | |
| mm1_copy_sb = None | |
| mm1_affine_select_output = None | |
| mm1_masked = None | |
| mm1_partial_max = None | |
| mm1_section_max = None | |
| mm1_running_max = None | |
| prev_mm1_running_max = None | |
| flash_attn_correction_factor = None | |
| exp_sb = None | |
| exp_partial_sum = None | |
| exp_section_sum = None | |
| exp_tp_sb = None | |
| exp_running_sum = None | |
| prev_exp_running_sum = None | |
| exp_sum_reciprocal = None | |
| mm2_psum = None | |
| mm2_sb = None | |
| mm2_prev_output = None | |
| mm2_accum_flash_attn = None | |
| mm2_final = None | |
| tp_flash_attn_correction_factor_psum = None | |
| tp_flash_attn_correction_factor_sb = None | |
| tp_exp_sum_reciprocal_psum = None | |
| tp_exp_sum_reciprocal_sb = None | |
| mm2_prev_output_scaled = None | |
| zero_bias_tensor = None | |
| sink_sb = None | |
| range_sel_lbs = None | |
| range_sel_ubs = None | |
| range_sel_lbs_prior = None | |
| range_sel_ubs_prior = None | |
| k_offset_sb_u32 = None | |
| class SectionParams(nl.NKIObject): | |
| section_idx = None | |
| section_offset = None | |
| section_offset_active = None | |
| next_section_offset_active = None | |
| section_contains_prefix = None | |
| next_section_contains_prefix = None | |
| kv_section_idx = None | |
| class AttnTileParams(nl.NKIObject): | |
| """Tile and buffer sizing parameters computed during implementation.""" | |
| seqlen_k_active_updated: int = None | |
| seqlen_k_prior_padded: int = None | |
| is_causal: bool = None | |
| sb_p: int = None | |
| num_grps: int = None | |
| num_q_grps_per_load: int = None | |
| can_pack_q_load: bool = None | |
| num_large_tiles_per_section: int = None | |
| num_k_tiles_per_section: int = None | |
| num_v_tiles_per_section: int = None | |
| exp_inst_elems: int = None | |
| num_exp_insts_per_large_tile: int = None | |
| num_tps_in_mm2_grp: int = None | |
| mm2_grp_sz: int = None | |
| use_swa_optimized_allocation: bool = None | |
| dynamic_sel_mask: bool = None | |
| section_len: int = None | |
| num_sections: int = None | |
| # ========================================================================================== | |
| # Entry: _attention_cte. Dead paths removed: prefix caching / APC / sink / seqpack / CP / SWA / | |
| # fused-v-proj / seqlen-q sharding remainder / cache_softmax. This call ALWAYS takes | |
| # use_sbuf_kv=True, causal_mask=False, tp_out=True, single-shard (num_shard=1), bs=1. | |
| # ========================================================================================== | |
| def _attention_cte( | |
| q: nl.ndarray, | |
| k: Optional[nl.ndarray] = None, | |
| v: Optional[nl.ndarray] = None, | |
| scale: float = 1.0, | |
| causal_mask: bool = True, | |
| tp_q: bool = True, | |
| tp_k: bool = False, | |
| tp_out: bool = False, | |
| cache_softmax: bool = False, | |
| softmax_dtype=nl.float32, | |
| mm_out_dtype=nl.float32, | |
| k_cache_sbuf: Optional[List[nl.ndarray]] = None, | |
| v_cache_sbuf: Optional[List[nl.ndarray]] = None, | |
| out_o_hbm=None, | |
| init_sbuf_addr: int = 0, | |
| seqlen_k_valid: int = None, | |
| ): | |
| """One head of context-encoding attention (SBUF-KV path). | |
| EDIT: ``seqlen_k_valid`` overrides the active key length (default len(k_cache)*512) | |
| so keys at positions >= seqlen_k_valid (token padding) are skipped by every loop bound. | |
| LIVE contract for the K2 encoder: ``k=v=None`` (K/V handed in via ``k_cache_sbuf`` / | |
| ``v_cache_sbuf``), ``causal_mask=False``, ``tp_q=False, tp_k=False, tp_out=True``, | |
| ``cache_softmax=False``, ``out_o_hbm`` is a LIST of SBUF slices into the caller's O tile. | |
| DEAD (removed): prefix caching (k_prior/v_prior/prior_used_len), sink, sliding_window, CP | |
| (cp_offset/global_cp_deg/striped/strided), sequence packing (bound_min/bound_max), | |
| skip_output_normalization, fp8 k_scale, and the fused v-projection (h_hbm/wv_t/bv). | |
| """ | |
| is_prefix_caching = False | |
| use_sbuf_kv = (k_cache_sbuf is not None and len(k_cache_sbuf) > 0) or ( | |
| v_cache_sbuf is not None and len(v_cache_sbuf) > 0 | |
| ) | |
| kernel_assert( | |
| k_cache_sbuf is not None and len(k_cache_sbuf) > 0, "k_cache_sbuf must be provided when using sbuf kv" | |
| ) | |
| kernel_assert( | |
| v_cache_sbuf is not None and len(v_cache_sbuf) > 0, "v_cache_sbuf must be provided when using sbuf kv" | |
| ) | |
| seqlen_q, seqlen_k_active, seqlen_k_prior, d, out_shape, softmax_shape = _check_input_and_return_shape( | |
| q, | |
| k, | |
| v, | |
| is_prefix_caching, | |
| None, | |
| None, | |
| None, | |
| tp_q, | |
| tp_k, | |
| tp_out, | |
| cache_softmax, | |
| k_cache_sbuf, | |
| v_cache_sbuf, | |
| None, | |
| None, | |
| skip_v_check=False, | |
| ) | |
| if seqlen_k_valid is not None: | |
| kernel_assert(seqlen_k_valid <= seqlen_k_active, "seqlen_k_valid exceeds K cache length") | |
| seqlen_k_active = seqlen_k_valid | |
| # Use out_o_hbm if provided (LIVE: it is a list of SBUF slices). | |
| if out_o_hbm is not None: | |
| result = out_o_hbm | |
| else: | |
| result = nl.ndarray(shape=out_shape, dtype=q.dtype, buffer=nl.shared_hbm) | |
| bs = q.shape[0] | |
| bs_kv = 1 # sbuf-kv: batch_size_kv is per-batch tiles | |
| kernel_assert(bs > 0, f"Batch size must be positive, got {bs}") | |
| kernel_assert(bs <= _MAX_BS, f"attention_cte kernel is not tested for batch size above {_MAX_BS}, got {bs}.") | |
| kernel_assert(bs_kv > 0, f"Batch size must be positive, got {bs_kv}") | |
| kernel_assert(bs % bs_kv == 0, f"Q batch size must be a multiple of KV batch size, got {bs=}, {bs_kv=}") | |
| seqlen_k_total = seqlen_k_active | |
| kernel_assert(seqlen_q <= _MAX_SEQLEN, f"seqlen above {_MAX_SEQLEN}, got {seqlen_q=}.") | |
| kernel_assert(seqlen_k_total <= _MAX_SEQLEN, f"seqlen above {_MAX_SEQLEN}, got {seqlen_k_total=}.") | |
| bs_seqlen_qk_product = float(bs * seqlen_q) * seqlen_k_total | |
| if bs_seqlen_qk_product > _MAX_BS_TIMES_SEQLEN_QK: | |
| logger.warn( | |
| f"attention_cte kernel is not tested for batch size x seqlen_q x seqlen_k above " | |
| f"{_MAX_BS_TIMES_SEQLEN_QK}, got {bs_seqlen_qk_product=}.", | |
| ) | |
| kernel_assert(d > 0, f"d must be > 0, got {d=}.") | |
| kernel_assert(d <= _MAX_HEAD_DIM, f"we do not support head_dim > {_MAX_HEAD_DIM}, got head dim {d}") | |
| kernel_assert( | |
| (str(mm_out_dtype) == str(nl.float32)) | |
| or (str(mm_out_dtype) == str(nl.bfloat16) and (nisa.get_nc_version() >= nisa.nc_version.gen4)), | |
| f"mm_out_dtype (psum) should be in [float32, bfloat16], got {mm_out_dtype} in {nisa.get_nc_version()}.", | |
| ) | |
| ac = AttnConfig( | |
| seqlen_q=seqlen_q, | |
| seqlen_k_active=seqlen_k_active, | |
| seqlen_k_prior=seqlen_k_prior, | |
| d=d, | |
| tp_q=tp_q, | |
| tp_k=tp_k, | |
| tp_out=tp_out, | |
| is_prefix_caching=is_prefix_caching, | |
| causal_mask=causal_mask, | |
| use_swa=False, | |
| sliding_window=0, | |
| use_cp=False, | |
| global_cp_deg=None, | |
| cp_strided_q_slicing=False, | |
| cp_striped_input=False, | |
| scale=scale, | |
| cache_softmax=cache_softmax, | |
| skip_output_normalization=False, | |
| dtype=q.dtype, | |
| softmax_dtype=softmax_dtype, | |
| mm_out_dtype=mm_out_dtype, | |
| is_sequence_packed=False, | |
| ) | |
| # Sharding info. use_sbuf_kv disables internal sharding (caller already sharded), so | |
| # num_shard=1, shard_id=0, num_bs_per_shard=bs, has_remainder=False. | |
| get_verified_program_sharding_info("attention_cte", max_sharding=2) | |
| num_shard = 1 | |
| shard_id = 0 | |
| num_bs_per_shard = bs // num_shard | |
| bs_offset = shard_id * num_bs_per_shard | |
| for batch_idx in range(num_bs_per_shard): | |
| # use_sbuf_kv: bs is LOCAL batch size and b is the LOCAL index. | |
| batch_id = batch_idx | |
| kv_batch_id = _q_to_kv_batch_id(batch_id, bs, bs_kv) | |
| _attention_cte_impl( | |
| q, | |
| k, | |
| v, | |
| None, | |
| None, | |
| None, | |
| result, | |
| batch_id, | |
| kv_batch_id, | |
| ac, | |
| k_cache_sbuf=k_cache_sbuf, | |
| v_cache_sbuf=v_cache_sbuf, | |
| init_sbuf_addr=init_sbuf_addr, | |
| ) | |
| # has_remainder is False (num_shard==1); the seqlen-q sharding path is DEAD. | |
| return result | |
| def _attention_cte_impl( | |
| q, | |
| k_active, | |
| v_active, | |
| k_prior, | |
| v_prior, | |
| prior_used_len, | |
| o, | |
| batch_id, | |
| batch_id_kv, | |
| ac: AttnConfig, | |
| k_cache_sbuf: Optional[List[nl.ndarray]] = None, | |
| v_cache_sbuf: Optional[List[nl.ndarray]] = None, | |
| init_sbuf_addr: int = 0, | |
| ): | |
| """Single-batch attention body. LIVE path only: single section (num_sections==1), pipelined | |
| group loop (num_grps=16 > 1), SBUF-KV (no K/V HBM load), tp_out=True. | |
| DEAD (removed): seqlen-q sharding, sink, SWA memset, sections>1 flash accumulation, K/V HBM | |
| load (_load_k_tile / _load_v_tile), SBUF-prior storage, cache_softmax intermediate writes, | |
| the non-pipelined (shard_seqlen_q_length<=1) branch. | |
| """ | |
| # Not seqlen-sharded: start at group 0, cover all groups. | |
| atp = _compute_tile_parameters(ac, is_seqlen_sharded=False) | |
| shard_seqlen_q_start = 0 | |
| shard_seqlen_q_length = atp.num_grps | |
| allocator = ModularAllocator(initial_address=init_sbuf_addr) | |
| bufs = AttnInternalBuffers() | |
| bufs.k_scale_sb = None | |
| bufs.zero_bias_tensor = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, 1), dtype=nl.float32) | |
| nisa.memset(bufs.zero_bias_tensor, 0.0) | |
| # _setup_range_select_bounds is a NO-OP here: dynamic_sel_mask=False and not prefix caching. | |
| _setup_range_select_bounds(ac, atp, bufs, allocator, None, None, None, None, batch_id=batch_id) | |
| bufs.mm1_running_max = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32) | |
| bufs.exp_running_sum = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32) | |
| bufs.exp_sum_reciprocal = allocator.alloc_sbuf_tensor(shape=(atp.sb_p, atp.num_grps), dtype=nl.float32) | |
| sbuf_addr_outer = allocator.get_current_address() | |
| # Single section (num_sections == 1). | |
| for section_idx in range(atp.num_sections): | |
| section_offset = atp.section_len * section_idx | |
| section_offset_active, section_contains_prefix = _compute_section_offset_active( | |
| section_offset, ac.is_prefix_caching, atp.seqlen_k_prior_padded | |
| ) | |
| next_section_offset = atp.section_len * (section_idx + 1) | |
| next_section_offset_active, next_section_contains_prefix = _compute_section_offset_active( | |
| next_section_offset, ac.is_prefix_caching, atp.seqlen_k_prior_padded | |
| ) | |
| sp = SectionParams( | |
| section_idx=section_idx, | |
| section_offset=section_offset, | |
| section_offset_active=section_offset_active, | |
| next_section_offset_active=next_section_offset_active, | |
| section_contains_prefix=section_contains_prefix, | |
| next_section_contains_prefix=next_section_contains_prefix, | |
| ) | |
| allocator.set_current_address(sbuf_addr_outer) | |
| _allocate_attention_buffers(allocator, ac, atp, bufs, sink=None, | |
| k_cache_sbuf=k_cache_sbuf, v_cache_sbuf=v_cache_sbuf) | |
| sbuf_addr = allocator.get_current_address() | |
| # SBUF-KV: K and V are already resident, so the HBM K/V load block is skipped entirely. | |
| shard_seqlen_q_end = shard_seqlen_q_start + shard_seqlen_q_length | |
| if shard_seqlen_q_length == 1: | |
| # v1.5: one Q group (N <= 128 tokens) -- no pipelining (the pipelined prologue below assumes >= 2 | |
| # groups and indexed a non-existent second Q tile, so N=128 failed to compile in <= v1.4). | |
| _load_q_impl(shard_seqlen_q_start, ac, atp, sp, bufs, q, batch_id, sbuf_addr) | |
| _qk_and_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, batch_id) | |
| _update_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None) | |
| _exp_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None) | |
| _pv_impl(shard_seqlen_q_start, ac, atp, sp, bufs) | |
| _write_back_impl(shard_seqlen_q_start, ac, atp, sp, bufs, o, batch_id) | |
| else: | |
| # Software pipelining: >= 2 Q groups. | |
| _load_q_impl(shard_seqlen_q_start, ac, atp, sp, bufs, q, batch_id, sbuf_addr) | |
| _qk_and_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, batch_id) | |
| _update_max_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None) | |
| _exp_impl(shard_seqlen_q_start, ac, atp, sp, bufs, None) | |
| _load_q_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, q, batch_id, sbuf_addr) | |
| _qk_and_max_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, batch_id) | |
| _update_max_impl(shard_seqlen_q_start + 1, ac, atp, sp, bufs, None) | |
| for grp_i in range(shard_seqlen_q_start, shard_seqlen_q_end - 2): | |
| # grp_i: PV, write_back ; grp_i+1: EXP ; grp_i+2: load Q, QK+Max. | |
| _load_q_impl(grp_i + 2, ac, atp, sp, bufs, q, batch_id, sbuf_addr) | |
| _exp_impl(grp_i + 1, ac, atp, sp, bufs, None) | |
| _fused_qkmax_and_pv_impl(grp_i, ac, atp, sp, bufs, batch_id) | |
| _write_back_impl(grp_i, ac, atp, sp, bufs, o, batch_id) | |
| _update_max_impl(grp_i + 2, ac, atp, sp, bufs, None) | |
| _pv_impl(shard_seqlen_q_end - 2, ac, atp, sp, bufs) | |
| _write_back_impl(shard_seqlen_q_end - 2, ac, atp, sp, bufs, o, batch_id) | |
| _exp_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs, None) | |
| _pv_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs) | |
| _write_back_impl(shard_seqlen_q_end - 1, ac, atp, sp, bufs, o, batch_id) | |
| # cache_softmax intermediate writes (out_neg_max / out_sum_recip) are DEAD (cache_softmax=False). | |
| def _compute_tile_parameters(ac: AttnConfig, is_seqlen_sharded: bool) -> AttnTileParams: | |
| """Tile parameters for the LIVE config. Dead: SWA/prefix/CP scale checks and seqlen updates, | |
| flash sectioning (total_seqlen_k=2048 <= 10240 -> single section).""" | |
| atp = AttnTileParams() | |
| # LIVE: causal_mask=False, no CP/SWA/seqpack -> is_causal=False, dynamic_sel_mask=False. | |
| atp.is_causal = ac.causal_mask | |
| atp.dynamic_sel_mask = False | |
| atp.seqlen_k_active_updated = ac.seqlen_k_active | |
| atp.use_swa_optimized_allocation = False | |
| atp.sb_p = nl.tile_size.pmax | |
| kernel_assert(_Q_GRP_SZ == atp.sb_p, f"expect Q group size == pmax, got {_Q_GRP_SZ=}, {atp.sb_p=}") | |
| kernel_assert(_V_TILE_SZ == atp.sb_p, f"expect V tile size == pmax, got {_V_TILE_SZ=}, {atp.sb_p=}") | |
| atp.num_grps = div_ceil(ac.seqlen_q, atp.sb_p) | |
| atp.can_pack_q_load = not is_seqlen_sharded | |
| num_q_grps_per_load_dtype = 4 if ac.dtype == nl.float32 else 8 | |
| atp.num_q_grps_per_load = min(num_q_grps_per_load_dtype if atp.can_pack_q_load else 1, atp.num_grps) | |
| kernel_assert(atp.num_q_grps_per_load > 0, f"num_q_grps_per_load must be positive, got {atp.num_q_grps_per_load}.") | |
| atp.seqlen_k_prior_padded = None # not prefix caching | |
| total_seqlen_k = atp.seqlen_k_active_updated | |
| # total_seqlen_k = 2048 <= 10240 -> no flash attention, single section. | |
| use_flash_attn = total_seqlen_k > _FLASH_ATTENTION_THRESHOLD | |
| if use_flash_attn: | |
| atp.section_len = min(total_seqlen_k, _FLASH_ATTENTION_SECTION_LENGTH) | |
| else: | |
| atp.section_len = total_seqlen_k | |
| kernel_assert(atp.section_len > 0, f"section_len must be positive, got {atp.section_len}") | |
| atp.num_sections = div_ceil(total_seqlen_k, atp.section_len) | |
| if not use_flash_attn: | |
| kernel_assert(atp.num_sections == 1, "Logic fault, must only have 1 section if not using flash_attn") | |
| atp.num_large_tiles_per_section = div_ceil(atp.section_len, _LARGE_TILE_SZ) | |
| atp.num_k_tiles_per_section = div_ceil(atp.section_len, _K_TILE_SZ) | |
| atp.num_v_tiles_per_section = div_ceil(atp.section_len, _V_TILE_SZ) | |
| atp.exp_inst_elems = _EXP_TILE_SZ | |
| atp.num_exp_insts_per_large_tile = _LARGE_TILE_SZ // atp.exp_inst_elems | |
| atp.num_tps_in_mm2_grp = _K_TILE_SZ // atp.sb_p | |
| atp.mm2_grp_sz = _K_TILE_SZ | |
| return atp | |
| def _setup_range_select_bounds( | |
| ac: AttnConfig, | |
| atp: AttnTileParams, | |
| bufs: AttnInternalBuffers, | |
| allocator: ModularAllocator, | |
| cp_offset: Any, | |
| prior_used_len: Any, | |
| bound_min: Any, | |
| bound_max: Any, | |
| batch_id: int = 0, | |
| ) -> tuple: | |
| """NO-OP for this config: only CP / SWA / prefix-caching / sequence-packing populate range | |
| bounds, and all are off (``dynamic_sel_mask=False``, ``is_prefix_caching=False``). The entire | |
| vendor body was gated by ``if atp.dynamic_sel_mask:`` / ``if ac.is_prefix_caching:`` / SWA, | |
| so it emits no instructions here and is removed.""" | |
| return | |
| def _allocate_attention_buffers( | |
| allocator: ModularAllocator, | |
| ac: AttnConfig, | |
| atp: AttnTileParams, | |
| bufs: AttnInternalBuffers, | |
| sink: Any, | |
| k_cache_sbuf: Optional[List[nl.ndarray]] = None, | |
| v_cache_sbuf: Optional[List[nl.ndarray]] = None, | |
| ): | |
| """Allocate SBUF/PSUM buffers. LIVE: tp_out=True (so all tp_out branches taken), K/V come | |
| from k_cache_sbuf/v_cache_sbuf, sink=None, dynamic_sel_mask=False (so the static-mask | |
| mm1_copy_sb / mm1_affine_select_output buffers ARE allocated -- kept). The tp_out=False | |
| branches and the fused-v-proj branch are DEAD and removed.""" | |
| mm2_p, mm2_n = atp.sb_p, ac.d | |
| # K/V provided in SBUF by the caller. | |
| bufs.k_sb = k_cache_sbuf | |
| bufs.v_sb = v_cache_sbuf | |
| bufs.q_sb = allocator.alloc_sbuf_tensor( | |
| shape=(ac.d, atp.sb_p * atp.num_q_grps_per_load), | |
| dtype=nl.bfloat16, | |
| block_dim=[div_ceil(atp.num_grps, atp.num_q_grps_per_load)], | |
| num_free_tiles=[2], | |
| align_to=32, | |
| ) | |
| bufs.flash_attn_correction_factor = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| mm1_partial_max_n_elts = atp.num_k_tiles_per_section # sink is None | |
| bufs.mm1_partial_max = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, mm1_partial_max_n_elts), dtype=nl.float32, | |
| block_dim=[atp.num_grps], num_free_tiles=[2], align_to=4, | |
| ) | |
| bufs.mm1_section_max = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| n_final_reduce_sum_elts = div_ceil(atp.section_len, atp.exp_inst_elems) # sink is None | |
| bufs.exp_partial_sum = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, n_final_reduce_sum_elts), dtype=nl.float32, | |
| block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| bufs.exp_section_sum = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| bufs.prev_mm1_running_max = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| bufs.prev_exp_running_sum = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, 1), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| # tp_out=True everywhere below. | |
| bufs.mm2_prev_output = allocator.alloc_sbuf_tensor( | |
| shape=(ac.d, atp.sb_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| bufs.mm2_accum_flash_attn = allocator.alloc_sbuf_tensor( | |
| shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| bufs.mm2_prev_output_scaled = allocator.alloc_sbuf_tensor( | |
| shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| bufs.tp_flash_attn_correction_factor_psum = [] | |
| bufs.tp_exp_sum_reciprocal_psum = [] | |
| for grp_idx in range(atp.num_grps): | |
| tp_flash_attn_correction_factor_psum_tile = nl.ndarray( | |
| (ac.d, atp.sb_p), dtype=nl.float32, buffer=nl.psum, | |
| address=(0, ((grp_idx % 2) * 4 + 3) * PSUM_BANK_SIZE), | |
| ) | |
| bufs.tp_flash_attn_correction_factor_psum.append(tp_flash_attn_correction_factor_psum_tile) | |
| tp_exp_sum_reciprocal_psum_tile = nl.ndarray( | |
| (ac.d, atp.sb_p), dtype=nl.float32, buffer=nl.psum, | |
| address=(0, ((grp_idx % 2) * 4 + 3) * PSUM_BANK_SIZE), | |
| ) | |
| bufs.tp_exp_sum_reciprocal_psum.append(tp_exp_sum_reciprocal_psum_tile) | |
| bufs.tp_flash_attn_correction_factor_sb = allocator.alloc_sbuf_tensor( | |
| shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[4], | |
| ) | |
| bufs.tp_exp_sum_reciprocal_sb = allocator.alloc_sbuf_tensor( | |
| shape=(ac.d, atp.sb_p), dtype=nl.float32, block_dim=[atp.num_grps], num_free_tiles=[4], | |
| ) | |
| bufs.mm2_final = allocator.alloc_sbuf_tensor( | |
| shape=(ac.d, atp.sb_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| bufs.mm2_sb = allocator.alloc_sbuf_tensor( | |
| shape=(mm2_n, mm2_p), dtype=ac.mm_out_dtype, block_dim=[atp.num_grps], num_free_tiles=[2], | |
| ) | |
| bufs.mm1_masked = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, _LARGE_TILE_SZ), dtype=nl.float32, | |
| block_dim=[atp.num_grps, atp.num_large_tiles_per_section], | |
| num_free_tiles=[2, atp.num_large_tiles_per_section], | |
| ) | |
| bufs.exp_sb = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, _LARGE_TILE_SZ), dtype=nl.bfloat16, | |
| block_dim=[atp.num_grps, atp.num_large_tiles_per_section], | |
| # DP2: group-axis depth 2 so exp(g+1) runs while transpose(g) still drains exp_sb(g). | |
| num_free_tiles=[2, atp.num_large_tiles_per_section], | |
| ) | |
| # MM1 PSUM tiles: banks {0,1,2,3}. | |
| bufs.mm1_psum = [] | |
| for grp_idx in range(atp.num_grps): | |
| grp_row = [] | |
| for large_tile_idx in range(atp.num_large_tiles_per_section): | |
| tile_row = [] | |
| for k_tile_idx in range(4): | |
| mm1_psum_tile = nl.ndarray( | |
| (atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype, buffer=nl.psum, | |
| # v1.4: rotate MM1 over 5 banks (0,1,2,5,6) across consecutive Q groups instead | |
| # of reusing banks 0-3 per group, so a QK matmul does not wait for the Vector row-max evict | |
| # of the tile 4 back. Bank 4 = P.V, 3/7 = reciprocal transposes. Banks 5/6 are also the | |
| # producer's projection banks; the tile framework orders reuse by address, so overlap | |
| # with a later image's producer stays correct (it may serialise). | |
| address=(0, (_MM1_BANKS[(grp_idx * 4 + k_tile_idx) % len(_MM1_BANKS)] | |
| if (_MM1_WIDE and atp.num_k_tiles_per_section <= _MM1_WIDE_MAX_KTILES) | |
| else (k_tile_idx % 4)) * PSUM_BANK_SIZE), | |
| ) | |
| tile_row.append(mm1_psum_tile) | |
| grp_row.append(tile_row) | |
| bufs.mm1_psum.append(grp_row) | |
| # dynamic_sel_mask=False -> the static-mask staging buffers are allocated (no-mask branch | |
| # does not read them, but the buffers exist in the vendor's non-dynamic allocation). | |
| bufs.mm1_copy_sb = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype, | |
| block_dim=[atp.num_grps, atp.num_large_tiles_per_section, 4], num_free_tiles=[1, 1, 2], | |
| ) | |
| bufs.mm1_affine_select_output = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, nl.tile_size.psum_fmax), dtype=ac.mm_out_dtype, | |
| block_dim=[atp.num_grps, atp.num_large_tiles_per_section, 4], num_free_tiles=[1, 1, 2], | |
| ) | |
| bufs.exp_tp_sb = allocator.alloc_sbuf_tensor( | |
| shape=(atp.sb_p, atp.mm2_grp_sz), dtype=nl.bfloat16, | |
| block_dim=[atp.num_grps, atp.num_large_tiles_per_section, atp.num_tps_in_mm2_grp], | |
| # DP4: 8 group-blocks resident so P^T transposes issue as soon as their exp exists. | |
| num_free_tiles=[8, atp.num_large_tiles_per_section, atp.num_tps_in_mm2_grp], | |
| align_to=32, | |
| ) | |
| # MM2 PSUM tiles: banks {4,5,6,7}. tp_out=True shape (mm2_n, mm2_p). | |
| bufs.mm2_psum = [] | |
| for grp_idx in range(atp.num_grps): | |
| grp_row = [] | |
| for large_tile_idx in range(atp.num_large_tiles_per_section): | |
| mm2_psum_tile = nl.ndarray( | |
| (mm2_n, mm2_p), dtype=ac.mm_out_dtype, buffer=nl.psum, | |
| address=(0, ((4 + (large_tile_idx % 4)) * PSUM_BANK_SIZE)), | |
| ) | |
| grp_row.append(mm2_psum_tile) | |
| bufs.mm2_psum.append(grp_row) | |
| def _q_to_kv_batch_id(batch_id: int, bs: int, bs_kv: int) -> int: | |
| """Map Q batch id to KV batch id for native GQA support. bs_kv=1 here -> always 0.""" | |
| return batch_id // (bs // bs_kv) | |
| def _check_input_and_return_shape( | |
| q, | |
| k, | |
| v, | |
| is_prefix_caching, | |
| k_prior, | |
| v_prior, | |
| prior_used_len, | |
| tp_q, | |
| tp_k, | |
| tp_out, | |
| cache_softmax, | |
| k_cache_sbuf=None, | |
| v_cache_sbuf=None, | |
| k_prior_sbuf=None, | |
| v_prior_sbuf=None, | |
| skip_v_check=False, | |
| ) -> tuple: | |
| """Validate shapes and return (seqlen_q, seqlen_k, seqlen_k_prior, d, out_shape, | |
| cache_softmax_shape). LIVE: use_sbuf_kv=True, tp_q=False, tp_out=True, not prefix caching, | |
| cache_softmax=False.""" | |
| # SBUF-KV path. | |
| kernel_assert(k_cache_sbuf is not None and len(k_cache_sbuf) > 0, "k_cache_sbuf must be provided") | |
| kernel_assert(v_cache_sbuf is not None and len(v_cache_sbuf) > 0, "v_cache_sbuf must be provided") | |
| if tp_q: | |
| batch_size, seqlen_q, d = q.shape | |
| else: | |
| batch_size, d, seqlen_q = q.shape | |
| d_from_k = k_cache_sbuf[0].shape[0] | |
| kernel_assert(d == d_from_k, f"Head dimension mismatch: q has d={d}, but k_cache_sbuf has d={d_from_k}") | |
| seqlen_k = len(k_cache_sbuf) * _K_TILE_SZ | |
| batch_size_kv = 1 | |
| seqlen_k_prior = None # not prefix caching | |
| out_seqlen = seqlen_q | |
| if tp_out: | |
| out_shape = (batch_size, d, out_seqlen) | |
| else: | |
| out_shape = (batch_size, out_seqlen, d) | |
| cache_softmax_tile_size = 128 | |
| padded_seq_grps = div_ceil(out_seqlen, cache_softmax_tile_size) | |
| cache_softmax_shape = [batch_size, cache_softmax_tile_size, padded_seq_grps] | |
| return (seqlen_q, seqlen_k, seqlen_k_prior, d, out_shape, cache_softmax_shape) | |
| def _load_q_tile(q, out, tp_q, batch_id, grp_i, seqlen_offset, load_dtype, sbuf_addr, grps_per_load=1) -> int: | |
| """Load a Q tile HBM->SBUF. LIVE: tp_q=False (q is [bs, d, seqlen]), no transpose; the whole | |
| ``if tp_q:`` transpose sub-tree (dma_transpose / nc_transpose paths) is DEAD and removed.""" | |
| kernel_assert(str(load_dtype) == str(out[0].dtype), "Conflicting dtype") | |
| _, d, seqlen = q.shape | |
| num_f = min(seqlen - seqlen_offset, _Q_GRP_SZ * grps_per_load) | |
| out_dst_pat = out[grp_i // grps_per_load].ap(pattern=[[_Q_GRP_SZ * grps_per_load, d], [1, num_f]], offset=0) | |
| q_src_pat = q.ap(pattern=[[seqlen, d], [1, num_f]], offset=batch_id * d * seqlen + seqlen_offset) | |
| nisa.dma_copy(dst=out_dst_pat, src=q_src_pat) | |
| def _get_kv_tile_apc( | |
| is_prefix_caching, | |
| k_active, | |
| k_prior, | |
| seqlen_active, | |
| seqlen_prior, | |
| seqlen_offset, | |
| load_offset_active, | |
| ) -> tuple: | |
| """Determine which KV tensor (active or prior) to use. LIVE: is_prefix_caching=False, so this | |
| returns the trivial active-tile mapping; the prefix-caching branch is DEAD and removed.""" | |
| return k_active, seqlen_active, seqlen_offset, load_offset_active | |
| def _compute_section_offset_active(k_section_offset, is_prefix_caching, seqlen_k_prior_padded): | |
| """Active-K offset for a section. LIVE: is_prefix_caching=False -> pass-through.""" | |
| section_contains_prefix = False | |
| k_section_offset_active = k_section_offset | |
| return k_section_offset_active, section_contains_prefix | |
| def _load_q_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, | |
| bufs: AttnInternalBuffers, q, batch_id, sbuf_addr): | |
| """Load a Q group if compute is needed. LIVE: is_causal=False -> has_any_compute is always | |
| True; k_scale_sb is None (fp8 dequant path DEAD).""" | |
| if grp_i % atp.num_q_grps_per_load == 0: | |
| # is_causal=False -> has_any_compute_pred is True. | |
| q_seqlen_offset = grp_i * _Q_GRP_SZ | |
| _load_q_tile( | |
| q, bufs.q_sb, ac.tp_q, batch_id, grp_i, q_seqlen_offset, | |
| bufs.q_sb[0].dtype, sbuf_addr, grps_per_load=atp.num_q_grps_per_load, | |
| ) | |
| # k_scale_sb is None -> the delayed-fp8 Q scaling is DEAD. | |
| def _qk_and_max_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, | |
| bufs: AttnInternalBuffers, batch_id: int = 0): | |
| """MM1 (QK^T) + row-max for a Q group. LIVE: is_causal=False -> always compute. | |
| h10 EDIT (site :2876): the ``mm1_partial_max`` init memset is pinned to GpSimd -- it sheds | |
| Vector (K2's binding engine) and its first consumer is several ops downstream, so a slower | |
| idle engine is safe.""" | |
| # is_causal=False -> has_any_compute_pred is True. | |
| nisa.memset(bufs.mm1_partial_max[grp_i], value=_FLOAT32_MIN, engine=engine.gpsimd) | |
| for large_tile_idx in range(atp.num_large_tiles_per_section): | |
| _qk_and_max_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs, batch_id) | |
| def _update_max_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, | |
| bufs: AttnInternalBuffers, sink): | |
| """Update running max. LIVE: is_causal=False (always compute), sink=None, num_sections==1 so | |
| the reduce writes straight into mm1_running_max (max-direct promotion) and the flash | |
| multi-section correction is DEAD.""" | |
| # is_causal=False -> has_any_compute_pred is True. | |
| # num_sections==1: the reduce writes straight into mm1_running_max (max-direct); the [128,1] | |
| # copy the vendor once had here is gone (3,072 Vector instructions removed upstream). | |
| _max_dst = bufs.mm1_running_max[:, grp_i] | |
| _n_exec = min(div_ceil(atp.seqlen_k_active_updated, _K_TILE_SZ), _LARGE_TILE_SZ // _K_TILE_SZ, | |
| atp.num_k_tiles_per_section) | |
| if atp.num_large_tiles_per_section == 1 and _n_exec >= 2: | |
| # v1.3: the chained row-max left the whole row max in the last executed k-tile's column. | |
| nisa.tensor_scalar(dst=_max_dst, data=bufs.mm1_partial_max[grp_i][:, _n_exec - 1:_n_exec], | |
| op0=nl.multiply, operand0=-1.0) | |
| else: | |
| nisa.tensor_reduce(_max_dst, nl.maximum, bufs.mm1_partial_max[grp_i], 1, negate=True) | |
| # num_sections==1: no running-max copy / flash correction factor. sink block DEAD (sink=None). | |
| def _exp_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, | |
| bufs: AttnInternalBuffers, sink): | |
| """Softmax exp (Scalar ACTIVATE) + P^T dma_transposes. LIVE: is_causal=False, use_swa=False, | |
| not prefix caching -> every k-tile is emitted (emit_idxs is full), wide_exp=False, sink=None. | |
| The ``memset`` on ``exp_partial_sum`` stays UNPINNED (engine.unknown / Vector) ON PURPOSE | |
| (vendor :2993): pinning it would fork this 280-line function and re-home the softmax-exp and | |
| dma_transpose source strings, which MUST keep their ``attention_inner`` sites stable and | |
| match the shipping stream. The ACCUM FORK (reset on first tile, accumulate, drain once) and | |
| the reduce_cmd discipline are preserved verbatim.""" | |
| # is_causal=False -> has_any_compute_pred is True. | |
| q_seqlen_offset = grp_i * atp.sb_p | |
| # v1.2: with one large tile only the last emitted column is ever read (see _write_back_impl), | |
| # and it is written by the chained exp-sum, so the zero-fill is skipped. | |
| if atp.num_large_tiles_per_section > 1: | |
| nisa.memset(bufs.exp_partial_sum[grp_i][...], value=0.0) # UNPINNED on purpose (:2993). | |
| for large_tile_idx in range(atp.num_large_tiles_per_section): | |
| kernel_assert(atp.exp_inst_elems == 512, "Internal validation failed.") | |
| # ACCUM FORK pre-scan: is_causal=False and not prefix caching -> every tile emits, so | |
| # emit_idxs is [0, 1, 2, 3]. reset on the first, accumulate, drain once. | |
| emit_idxs = [] | |
| for scan_idx in range(atp.num_exp_insts_per_large_tile): | |
| scan_prior, scan_seqlen_k, scan_k_start, _ = _get_kv_tile_apc( | |
| ac.is_prefix_caching, | |
| False, | |
| True, | |
| atp.seqlen_k_active_updated, | |
| ac.seqlen_k_prior, | |
| sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + scan_idx * atp.exp_inst_elems, | |
| None, | |
| ) | |
| scan_mask = True # is_causal=False, use_swa=False | |
| if scan_mask and scan_seqlen_k > scan_k_start: | |
| emit_idxs.append(scan_idx) | |
| first_emit = emit_idxs[0] if emit_idxs else -1 | |
| last_emit = emit_idxs[-1] if emit_idxs else -1 | |
| for exp_tile_idx in range(atp.num_exp_insts_per_large_tile): | |
| is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc( | |
| ac.is_prefix_caching, | |
| False, | |
| True, | |
| atp.seqlen_k_active_updated, | |
| ac.seqlen_k_prior, | |
| sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + exp_tile_idx * atp.exp_inst_elems, | |
| None, | |
| ) | |
| num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ) | |
| num_f = min(seqlen_k - k_start_pos, atp.exp_inst_elems) | |
| exp_sel_mask = True # is_causal=False, use_swa=False | |
| if exp_sel_mask and seqlen_k > k_start_pos: | |
| # Step 1: exp. reduce_cmd is ALWAYS explicit (see the vendor note: an idle cmd | |
| # with reduce_op/reduce_res would write a stale register). | |
| if exp_tile_idx == first_emit: | |
| exp_reduce_cmd = nisa.reduce_cmd.reset_reduce | |
| else: | |
| exp_reduce_cmd = nisa.reduce_cmd.reduce | |
| if exp_tile_idx == last_emit: | |
| exp_reduce_res = bufs.exp_partial_sum[grp_i][ | |
| :num_p, | |
| large_tile_idx * atp.num_exp_insts_per_large_tile + exp_tile_idx, | |
| ] | |
| else: | |
| exp_reduce_res = None | |
| nisa.activation( | |
| bufs.exp_sb[grp_i][large_tile_idx][ | |
| :num_p, nl.ds(exp_tile_idx * atp.exp_inst_elems, num_f) | |
| ], | |
| op=nl.exp, | |
| data=bufs.mm1_masked[grp_i][large_tile_idx][ | |
| :num_p, nl.ds(exp_tile_idx * atp.exp_inst_elems, num_f) | |
| ], | |
| reduce_op=nl.add, | |
| reduce_cmd=exp_reduce_cmd, | |
| reduce_res=exp_reduce_res, | |
| bias=bufs.mm1_running_max[:num_p, grp_i], | |
| ) | |
| # Step 2: P^T dma_transpose into exp_tp_sb ([128, 4, 128]). | |
| num_f_outer = num_f // atp.sb_p | |
| num_f_inner = num_f % atp.sb_p | |
| if num_f_outer >= 1: | |
| nisa.dma_transpose( | |
| dst=bufs.exp_tp_sb[grp_i][large_tile_idx][exp_tile_idx].ap( | |
| [ | |
| [atp.mm2_grp_sz, atp.sb_p], | |
| [1, 1], | |
| [atp.sb_p, num_f_outer], | |
| [1, num_p], | |
| ] | |
| ), | |
| src=bufs.exp_sb[grp_i][large_tile_idx].ap( | |
| [ | |
| [_LARGE_TILE_SZ, num_p], | |
| [1, 1], | |
| [atp.sb_p, num_f_outer], | |
| [1, atp.sb_p], | |
| ], | |
| offset=exp_tile_idx * atp.mm2_grp_sz, | |
| ), | |
| ) | |
| if num_f_inner > 0: | |
| nisa.dma_transpose( | |
| dst=bufs.exp_tp_sb[grp_i][large_tile_idx][exp_tile_idx].ap( | |
| [ | |
| [atp.mm2_grp_sz, num_f_inner], | |
| [1, 1], | |
| [atp.sb_p, 1], | |
| [1, num_p], | |
| ], | |
| offset=num_f_outer * atp.sb_p, | |
| ), | |
| src=bufs.exp_sb[grp_i][large_tile_idx].ap( | |
| [ | |
| [_LARGE_TILE_SZ, num_p], | |
| [1, 1], | |
| [atp.sb_p, 1], | |
| [1, num_f_inner], | |
| ], | |
| offset=exp_tile_idx * atp.mm2_grp_sz + num_f_outer * atp.sb_p, | |
| ), | |
| ) | |
| # sink block DEAD (sink=None). | |
| def _pv_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, bufs: AttnInternalBuffers): | |
| """MM2 (P@V) for a Q group. LIVE: is_causal=False -> always compute.""" | |
| # is_causal=False -> has_any_compute_pred is True. | |
| # v1.2: the mm2_sb zero-fill is skipped when there is a single large tile (N <= 2048, the only | |
| # configuration this fork serves): its first write is the PSUM->SBUF tensor_copy over exactly the | |
| # [:, :num_f] region that write-back reads, so the zeros were never observed. | |
| if atp.num_large_tiles_per_section > 1: | |
| nisa.memset(bufs.mm2_sb[grp_i][...], value=0.0) | |
| for large_tile_idx in range(atp.num_large_tiles_per_section): | |
| _pv_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs) | |
| def _fused_qkmax_and_pv_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, | |
| bufs: AttnInternalBuffers, batch_id: int = 0): | |
| """Pipelined MM2 for grp_i while doing QK+max for grp_i+2. LIVE: is_causal=False -> both | |
| predicates True. | |
| h10 EDIT (site :3294): the ``mm1_partial_max[qkmax_grp]`` init memset is pinned to GpSimd | |
| (same rationale as :2876; 2,688/core, the larger of the two).""" | |
| qkmax_grp = grp_i + 2 | |
| # is_causal=False -> both has_any_compute predicates are True. | |
| nisa.memset(bufs.mm1_partial_max[qkmax_grp][...], value=_FLOAT32_MIN, engine=engine.gpsimd) | |
| for large_tile_idx in range(atp.num_large_tiles_per_section): | |
| _pv_large_tile_impl(grp_i, large_tile_idx, ac, atp, sp, bufs) | |
| _qk_and_max_large_tile_impl(qkmax_grp, large_tile_idx, ac, atp, sp, bufs, batch_id) | |
| def _write_back_impl(grp_i, ac: AttnConfig, atp: AttnTileParams, sp: SectionParams, | |
| bufs: AttnInternalBuffers, o, batch_id): | |
| """Finalise output. LIVE: is_causal=False (always compute), num_sections==1, | |
| skip_output_normalization=False -> single-section reciprocal + scale_reciprocal_write_back. | |
| The flash multi-section accumulation and the skip-normalization branches are DEAD.""" | |
| # is_causal=False -> has_any_compute_pred is True. | |
| q_seqlen_offset = grp_i * atp.sb_p | |
| # num_sections==1, not skip_output_normalization. | |
| if atp.num_large_tiles_per_section == 1: | |
| # v1.2: with one large tile the chained exp-sum leaves the WHOLE row sum in one column | |
| # (the last emitted exp tile); every other column is the memset 0.0. Read that column | |
| # directly instead of reducing the zero-padded row (x + 0.0 is exact, so bit-identical). | |
| # last emitted exp tile = ceil(seqlen_k / exp_inst_elems) - 1 (clamped to the large tile) | |
| _last_col = min(div_ceil(atp.seqlen_k_active_updated, atp.exp_inst_elems), | |
| _LARGE_TILE_SZ // atp.exp_inst_elems) - 1 | |
| nisa.reciprocal(bufs.exp_sum_reciprocal[:, grp_i], bufs.exp_partial_sum[grp_i][:, _last_col:_last_col + 1]) | |
| else: | |
| nisa.tensor_reduce(bufs.exp_section_sum[grp_i][...], nl.add, bufs.exp_partial_sum[grp_i], axis=1) | |
| nisa.reciprocal(bufs.exp_sum_reciprocal[:, grp_i], bufs.exp_section_sum[grp_i]) | |
| num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ) | |
| num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ) | |
| # Single section: scale by reciprocal and write current output. | |
| _scale_reciprocal_write_back_impl(bufs.mm2_sb[grp_i], grp_i, ac, atp, bufs, o, batch_id, num_p, num_f) | |
| def _scale_reciprocal_write_back_impl(src_buf, grp_i, ac: AttnConfig, atp: AttnTileParams, | |
| bufs: AttnInternalBuffers, o, batch_id, num_p, num_f): | |
| """Reciprocal-normalise and write back (final section). LIVE: tp_out=True. | |
| h8 EDIT (site 1): the tp_out PSUM->SBUF evict ``tensor_copy`` is pinned to Scalar -- the | |
| un-pinned copy defaults to Vector (K2's binding engine); GpSimd is illegal (PSUM source), so | |
| Scalar is the only shedding target and is the exercised majority path. The tp_out=False | |
| ``tensor_scalar(..., engine=engine.vector)`` branch is DEAD (tp_out always True) and removed.""" | |
| # tp_out=True. | |
| nisa.nc_transpose( | |
| bufs.tp_exp_sum_reciprocal_psum[grp_i].ap(pattern=[[atp.sb_p, ac.d], [1, atp.sb_p]], offset=0), | |
| bufs.exp_sum_reciprocal.ap(pattern=[[atp.num_grps, atp.sb_p], [0, ac.d]], offset=grp_i), | |
| ) | |
| if _WB_FUSED: | |
| # v1.5: one Vector multiply reads the transposed reciprocal straight from PSUM and writes bf16 into the | |
| # caller's O slice -- replaces the Scalar PSUM evict, the fp32 multiply into mm2_final and the GpSimd | |
| # fp32->bf16 cast (same single rounding to bf16 as before: fp32 product, rounded once on write). | |
| nisa.tensor_tensor( | |
| o[grp_i][: ac.d, :num_f], | |
| src_buf[: ac.d, :num_f], | |
| bufs.tp_exp_sum_reciprocal_psum[grp_i][: ac.d, :num_f], | |
| nl.multiply, | |
| ) | |
| return | |
| # ---- h8 SITE 1: PSUM->SBUF evict pinned to Scalar. ---- | |
| nisa.tensor_copy( | |
| bufs.tp_exp_sum_reciprocal_sb[grp_i][: ac.d, :num_f], | |
| bufs.tp_exp_sum_reciprocal_psum[grp_i][: ac.d, :num_f], | |
| engine=engine.scalar, | |
| ) | |
| nisa.tensor_tensor( | |
| bufs.mm2_final[grp_i][: ac.d, :num_f], | |
| src_buf[: ac.d, :num_f], | |
| bufs.tp_exp_sum_reciprocal_sb[grp_i][: ac.d, :num_f], | |
| nl.multiply, | |
| ) | |
| _write_back_o_impl(bufs.mm2_final[grp_i], grp_i, ac, atp, o, batch_id, num_p, num_f) | |
| def _write_back_o_impl(src_buf, grp_i, ac: AttnConfig, atp: AttnTileParams, o, batch_id, num_p, num_f): | |
| """Evict finished O. LIVE: ``o`` is a LIST of caller-owned SBUF slices, tp_out=True, so the | |
| output is copied into the caller's O tile (kept on-chip for the fused out-projection) rather | |
| than DMA'd to HBM. The HBM-sink branch is DEAD (o is always a list) and removed. | |
| h8 EDIT (site 3): the tp_out list-branch ``tensor_copy`` (SBUF->SBUF fp32->bf16 CAST) is | |
| pinned to GpSimd -- all three engines are legal and GpSimd is the idle engine, so it sheds | |
| off Vector.""" | |
| # o is a list (caller-owned SBUF slices), tp_out=True. | |
| # ---- h8 SITE 3: SBUF->SBUF CAST pinned to GpSimd. ---- | |
| nisa.tensor_copy(dst=o[grp_i][: ac.d, :num_f], src=src_buf[: ac.d, :num_f], engine=engine.gpsimd) | |
| def _qk_and_max_large_tile_impl(qkmax_grp, large_tile_idx, ac: AttnConfig, atp: AttnTileParams, | |
| sp: SectionParams, bufs: AttnInternalBuffers, batch_id: int = 0): | |
| """MM1 (QK^T) + masking for a Q group and a 2048 K tile. LIVE: is_causal=False, | |
| dynamic_sel_mask=False, is_prior_tile=False -> the FINAL ``else: # no masking`` | |
| ``tensor_scalar_reduce`` branch. The diagonal (affine_select) and range_select branches are | |
| DEAD and removed; SBUF-prefix-caching K-tile selection is DEAD (k_sb_prior is None).""" | |
| q_seqlen_offset = qkmax_grp * atp.sb_p | |
| num_k_tiles_in_large_tile = _LARGE_TILE_SZ // _K_TILE_SZ | |
| # v1.3: chain the row-max across the executed k-tiles of this large tile (reset on the first, | |
| # accumulate on the rest, write reduce_res only on the last) instead of one read-out per k-tile | |
| # followed by a tensor_reduce. Executed tiles are those with k_start < seqlen_k (non-causal: | |
| # every tile up to the active key length). max is exact under any grouping -> bit-identical. | |
| # single large tile (the only case chained): executed k-tiles are 0 .. ceil(seqlen_k/512)-1 | |
| _n_exec = min(div_ceil(atp.seqlen_k_active_updated, _K_TILE_SZ), num_k_tiles_in_large_tile, | |
| atp.num_k_tiles_per_section) | |
| _chain = (atp.num_large_tiles_per_section == 1 and _n_exec >= 2) | |
| _first_kt = 0 | |
| _last_kt = _n_exec - 1 | |
| for k_tile_idx in range(num_k_tiles_in_large_tile): | |
| mm1_psum_tile = bufs.mm1_psum[qkmax_grp][large_tile_idx][k_tile_idx] | |
| # dynamic_sel_mask=False, but the no-mask branch below reads neither mm1_copy_sb nor | |
| # mm1_affine_select_output, so their per-tile locals are not needed. | |
| mm1_masked_tile = bufs.mm1_masked[qkmax_grp][large_tile_idx] | |
| mm1_partial_max_tile = bufs.mm1_partial_max[qkmax_grp] | |
| k_tile_idx_in_section = large_tile_idx * num_k_tiles_in_large_tile + k_tile_idx | |
| _kv_sec_idx = sp.kv_section_idx if sp.kv_section_idx is not None else sp.section_idx | |
| k_tile_idx_global = atp.num_k_tiles_per_section * _kv_sec_idx + k_tile_idx_in_section | |
| is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc( | |
| ac.is_prefix_caching, | |
| False, | |
| True, | |
| atp.seqlen_k_active_updated, | |
| ac.seqlen_k_prior, | |
| k_tile_idx_global * _K_TILE_SZ, | |
| None, | |
| ) | |
| matmul_selection = True # is_causal=False | |
| if q_seqlen_offset >= ac.seqlen_q or k_start_pos >= seqlen_k: | |
| matmul_selection = False | |
| if matmul_selection and k_tile_idx_in_section < atp.num_k_tiles_per_section: | |
| num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ) | |
| num_q_free = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ) | |
| # Step 1: MM1 matmul. Non-APC path -> use k_tile_idx_in_section. | |
| k_tile_to_use = bufs.k_sb[k_tile_idx_in_section] | |
| nisa.nc_matmul( | |
| mm1_psum_tile[:num_q_free, :num_f], | |
| bufs.q_sb[qkmax_grp // atp.num_q_grps_per_load][ | |
| : ac.d, | |
| nl.ds((qkmax_grp % atp.num_q_grps_per_load) * _Q_GRP_SZ, num_q_free), | |
| ], | |
| k_tile_to_use[:, :num_f], | |
| ) | |
| # Step 2: no masking (is_causal=False, dynamic_sel_mask=False). | |
| num_p = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ) | |
| num_f = min(seqlen_k - k_start_pos, _K_TILE_SZ) | |
| if _chain: | |
| nisa.tensor_scalar_reduce( | |
| mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)], | |
| data=mm1_psum_tile[:num_p, :num_f], | |
| op0=nl.multiply, | |
| operand0=ac.scale, | |
| reduce_op=nl.maximum, | |
| reduce_cmd=(nisa.reduce_cmd.reset_reduce if k_tile_idx == _first_kt else nisa.reduce_cmd.reduce), | |
| reduce_res=(mm1_partial_max_tile[:num_p, k_tile_idx_in_section] if k_tile_idx == _last_kt else None), | |
| ) | |
| else: | |
| nisa.tensor_scalar_reduce( | |
| mm1_masked_tile[:num_p, nl.ds(k_tile_idx * _K_TILE_SZ, num_f)], | |
| data=mm1_psum_tile[:num_p, :num_f], | |
| op0=nl.multiply, | |
| operand0=ac.scale, | |
| reduce_op=nl.maximum, | |
| reduce_res=mm1_partial_max_tile[:num_p, k_tile_idx_in_section], | |
| ) | |
| def _pv_large_tile_impl(pv_grp, large_tile_idx, ac: AttnConfig, atp: AttnTileParams, | |
| sp: SectionParams, bufs: AttnInternalBuffers): | |
| """MM2 (P@V) for a Q group and 2048 V tile. LIVE: is_causal=False (all tiles selected), | |
| tp_out=True, single large tile so ``large_tile_idx==0`` -> the ``tensor_copy`` PSUM->SBUF | |
| reduction (never the accumulate ``tensor_tensor`` branch). SBUF-prefix V-tile selection is | |
| DEAD (v_sb_prior is None); the tp_out=False MM2 orientation is DEAD.""" | |
| q_seqlen_offset = pv_grp * atp.sb_p | |
| num_mm2_grps_in_large_tile = _LARGE_TILE_SZ // atp.mm2_grp_sz | |
| mm2_psum_set = False | |
| mm2_psum_tile = bufs.mm2_psum[pv_grp][large_tile_idx] | |
| # Step 1: matmul + accumulate in PSUM per group. | |
| for mm2_grp_i in range(num_mm2_grps_in_large_tile): | |
| num_mm2_per_grp = atp.mm2_grp_sz // _V_TILE_SZ | |
| num_mm2_per_large_tile = num_mm2_per_grp * num_mm2_grps_in_large_tile | |
| exp_tp_sb_tile = bufs.exp_tp_sb[pv_grp][large_tile_idx][mm2_grp_i] | |
| is_prior_tile, seqlen_k, k_start_pos_512_tile, _ = _get_kv_tile_apc( | |
| ac.is_prefix_caching, | |
| False, | |
| True, | |
| atp.seqlen_k_active_updated, | |
| ac.seqlen_k_prior, | |
| sp.section_offset + large_tile_idx * _LARGE_TILE_SZ + mm2_grp_i * atp.mm2_grp_sz, | |
| None, | |
| ) | |
| for mm2_i in range(num_mm2_per_grp): | |
| v_tile_idx = large_tile_idx * num_mm2_per_large_tile + mm2_grp_i * num_mm2_per_grp + mm2_i | |
| k_start_pos = k_start_pos_512_tile + mm2_i * _V_TILE_SZ | |
| num_p = min(seqlen_k - k_start_pos, _V_TILE_SZ) | |
| num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ) | |
| mm2_sel_mask = True # is_causal=False | |
| if mm2_sel_mask and v_tile_idx < atp.num_v_tiles_per_section and num_p > 0 and num_f > 0: | |
| mm2_psum_set = True | |
| # Non-APC path -> use v_tile_idx. | |
| v_tile_to_use = bufs.v_sb[v_tile_idx] | |
| # tp_out=True orientation. | |
| nisa.nc_matmul( | |
| mm2_psum_tile[: ac.d, :num_f], | |
| v_tile_to_use[:num_p, : ac.d], | |
| exp_tp_sb_tile[:num_p, nl.ds(mm2_i * _V_TILE_SZ, num_f)], | |
| ) | |
| # Step 2: reduce MM2 groups into the large tile. is_prior_tile=False, is_causal=False -> | |
| # loop_reduce_sel True; single large tile -> large_tile_idx==0 -> tensor_copy (never the | |
| # accumulate tensor_tensor branch). | |
| is_prior_tile, seqlen_k, k_start_pos, _ = _get_kv_tile_apc( | |
| ac.is_prefix_caching, | |
| False, | |
| True, | |
| atp.seqlen_k_active_updated, | |
| ac.seqlen_k_prior, | |
| sp.section_offset + large_tile_idx * _LARGE_TILE_SZ, | |
| None, | |
| ) | |
| k_seqlen_mask = k_start_pos < seqlen_k | |
| loop_reduce_sel = True # is_causal=False | |
| if loop_reduce_sel and k_seqlen_mask and mm2_psum_set: | |
| # tp_out=True. | |
| num_f = min(ac.seqlen_q - q_seqlen_offset, _Q_GRP_SZ) | |
| if large_tile_idx == 0: | |
| nisa.tensor_copy(bufs.mm2_sb[pv_grp][:, :num_f], mm2_psum_tile[:, :num_f]) | |
| else: | |
| nisa.tensor_tensor( | |
| bufs.mm2_sb[pv_grp][:, :num_f], | |
| bufs.mm2_sb[pv_grp][:, :num_f], | |
| mm2_psum_tile[:, :num_f], | |
| nl.add, | |
| ) | |