Merged
fix(sampling): add ops #20
fix(sampling): add ops #20
Merged
wangliweiAdd on Sep 28
Total: 89 files changed+22401-0
@@ -47,8 +47,84 @@ __all__ = [
47 'swiglu_forward',47 'swiglu_forward',
48 'swiglu_forward_and_per_token_cast',48 'swiglu_forward_and_per_token_cast',
49 'unpack_from_e2m1fn_x2',49 'unpack_from_e2m1fn_x2',
50+ 'apply_token_bitmask_inplace',
51+ 'attn_rel_h_rel_w',
52+ 'batch_find_value_by_index',
53+ 'bmp_to_patches',
54+ 'compress_and_update_state_decode',
55+ 'compress_and_update_state_prefill',
56+ 'compute_confidence_error',
57+ 'compute_seqlens_k_zos',
58+ 'compute_seqlens_q_by_confidence_rates',
59+ 'confidence_head_fused',
60+ 'convert_kv_index',
61+ 'dflash_input_store',
62+ 'find_value_by_index',
63+ 'fused_sample',
64+ 'gather',
65+ 'gather_engram_tokens_zos',
66+ 'gather_input_ids_zos',
67+ 'get_accept_result',
68+ 'get_dflash_mainx_slot_mapping',
69+ 'get_fused_dflash_get_input',
70+ 'get_kv_ca_index_decode',
71+ 'get_kv_ca_index_prefill',
72+ 'get_kv_swa_index_decode',
73+ 'get_load_kv_index_kernel',
74+ 'get_logits_cur_by_step',
75+ 'get_prompt_logprobs_tensor',
76+ 'get_sum_smaller_probs',
77+ 'kv_index_cat',
78+ 'layer_norm_fn',
79+ 'load_index_kv_cache',
80+ 'load_kv_cache',
81+ 'load_kv_cache_llama',
82+ 'load_state_to_cache',
83+ 'log_softmax',
84+ 'norm_fn',
85+ 'prepare_for_mirror_cp_tilelang',
86+ 'prepare_image_attn_pos_id',
87+ 'prepare_llama_decode_metadata',
88+ 'prepare_v33_decode_metadata',
89+ 'random_sampling',
90+ 'replace_input_embeds',
91+ 'rotary_embedding',
92+ 'sampled_tokens_update',
93+ 'scatter_verified',
94+ 'select_state_slice',
95+ 'shuffle_image_tokens',
96+ 'softmax',
97+ 'softmax_and_logsoftmax',
98+ 'store_index_kv_cache',
99+ 'store_kv_cache',
100+ 'store_kv_cache_llama',
101+ 'unbind',
102+ 'update_verify_states',
50]103]
51 104 
105+from .attention import (
106+ convert_kv_index,
107+ get_dflash_mainx_slot_mapping,
108+ get_fused_dflash_get_input,
109+ get_kv_ca_index_decode,
110+ get_kv_ca_index_prefill,
111+ get_kv_swa_index_decode,
112+ get_load_kv_index_kernel,
113+ kv_index_cat,
114+ load_index_kv_cache,
115+ load_kv_cache,
116+ load_kv_cache_llama,
117+ load_state_to_cache,
118+ prepare_for_mirror_cp_tilelang,
119+ prepare_llama_decode_metadata,
120+ prepare_v33_decode_metadata,
121+ rotary_embedding,
122+ select_state_slice,
123+ store_index_kv_cache,
124+ store_kv_cache,
125+ store_kv_cache_llama,
126+)
127+from .compression import compress_and_update_state_decode, compress_and_update_state_prefill
52from .engram import (128from .engram import (
53 engram_fused_weight,129 engram_fused_weight,
54 engram_gate_bwd,130 engram_gate_bwd,
@@ -82,6 +158,14 @@ from .mhc import (
82 mhc_sinkhorn_forward,158 mhc_sinkhorn_forward,
83)159)
84from .moe import moe_topk_gate, normalize_weight, topk_gate160from .moe import moe_topk_gate, normalize_weight, topk_gate
161+from .multimodal import (
162+ attn_rel_h_rel_w,
163+ bmp_to_patches,
164+ prepare_image_attn_pos_id,
165+ replace_input_embeds,
166+ shuffle_image_tokens,
167+)
168+from .normalization import layer_norm_fn, norm_fn
85from .quant import (169from .quant import (
86 cast_back,170 cast_back,
87 per_block_cast,171 per_block_cast,
@@ -99,3 +183,25 @@ from .quant import (
99 swiglu_forward_and_per_token_cast,183 swiglu_forward_and_per_token_cast,
100 unpack_from_e2m1fn_x2,184 unpack_from_e2m1fn_x2,
101)185)
186+from .sampling import (
187+ apply_token_bitmask_inplace,
188+ batch_find_value_by_index,
189+ compute_confidence_error,
190+ compute_seqlens_q_by_confidence_rates,
191+ confidence_head_fused,
192+ dflash_input_store,
193+ find_value_by_index,
194+ fused_sample,
195+ get_accept_result,
196+ get_logits_cur_by_step,
197+ get_prompt_logprobs_tensor,
198+ get_sum_smaller_probs,
199+ log_softmax,
200+ random_sampling,
201+ sampled_tokens_update,
202+ scatter_verified,
203+ softmax,
204+ softmax_and_logsoftmax,
205+ update_verify_states,
206+)
207+from .tensor import compute_seqlens_k_zos, gather, gather_engram_tokens_zos, gather_input_ids_zos, unbind
@@ -0,0 +1,48 @@
1+"""Attention and KV-cache operators for Ascend."""
2+ 
3+__all__ = [
4+ 'convert_kv_index',
5+ 'get_dflash_mainx_slot_mapping',
6+ 'get_fused_dflash_get_input',
7+ 'get_kv_ca_index_decode',
8+ 'get_kv_ca_index_prefill',
9+ 'get_kv_swa_index_decode',
10+ 'get_load_kv_index_kernel',
11+ 'kv_index_cat',
12+ 'load_index_kv_cache',
13+ 'load_kv_cache',
14+ 'load_kv_cache_llama',
15+ 'load_state_to_cache',
16+ 'prepare_for_mirror_cp_tilelang',
17+ 'prepare_llama_decode_metadata',
18+ 'prepare_v33_decode_metadata',
19+ 'rotary_embedding',
20+ 'select_state_slice',
21+ 'store_index_kv_cache',
22+ 'store_kv_cache',
23+ 'store_kv_cache_llama',
24+]
25+ 
26+from .cache import (
27+ load_index_kv_cache,
28+ load_kv_cache,
29+ load_kv_cache_llama,
30+ store_index_kv_cache,
31+ store_kv_cache,
32+ store_kv_cache_llama,
33+)
34+from .flash_mla_utils import convert_kv_index
35+from .get_kv_index import (
36+ get_dflash_mainx_slot_mapping,
37+ get_fused_dflash_get_input,
38+ get_kv_ca_index_decode,
39+ get_kv_ca_index_prefill,
40+ get_kv_swa_index_decode,
41+ get_load_kv_index_kernel,
42+)
43+from .kv_index_cat import kv_index_cat
44+from .prepare_cp_mirror import prepare_for_mirror_cp_tilelang
45+from .prepare_llama_decode_metadata import prepare_llama_decode_metadata
46+from .prepare_v33_decode_metadata import prepare_v33_decode_metadata
47+from .rope import rotary_embedding
48+from .state_cache import load_state_to_cache, select_state_slice
@@ -0,0 +1,31 @@
1+"""Small Ascend SIMD helpers shared by migrated cache kernels."""
2+ 
3+from typing import Any
4+ 
5+import tilelang
6+from tilelang.ascend import language as T
7+from tilelang.backend.target import determine_target
8+ 
9+ASCEND_VSTSX2_BF16_PRELUDE = r"""
10+__simd_callee__ inline void tl_vstsx2_bf16(
11+ __ubuf__ bfloat16_t *dst, vector_bf16 even, vector_bf16 odd,
12+ vector_bool mask) {
13+ ::vsts(even, odd, dst, 0, INTLV_B16, mask);
14+}
15+"""
16+ 
17+ 
18+def is_pto_target() -> bool:
19+ target = determine_target(tilelang.env.get_default_target(), return_object=True)
20+ keys = target.keys
21+ if isinstance(keys, str):
22+ keys = (keys,)
23+ return any(str(key) == 'pto' for key in keys)
24+ 
25+ 
26+def vstsx2_bf16(dst: Any, even: Any, odd: Any, mask: Any) -> None:
27+ dst_ptr = T.access_ptr(dst, 'w', 256)
28+ if is_pto_target():
29+ T.evaluate(T.call_extern('void', 'pto.vstsx2', even, odd, dst_ptr, T.int32(0), 'INTLV_B16', mask))
30+ else:
31+ T.evaluate(T.call_extern('void', 'tl_vstsx2_bf16', dst_ptr, even, odd, mask))
@@ -0,0 +1,401 @@
1+import torch
2+ 
3+from cann_ops_tilelang.attention.cache_kernel import (
4+ _load_index_kv_cache_fp4_tl_ascend,
5+ _load_index_kv_cache_fp8_per32_tl_ascend,
6+ _load_index_kv_cache_tl_ascend,
7+ _load_kv_cache_llama_tl_ascend,
8+ _load_kv_cache_per32_tl_ascend,
9+ _load_kv_cache_tl_ascend,
10+ _store_index_kv_cache_fp4_tl_ascend,
11+ _store_index_kv_cache_fp8_per32_tl_ascend,
12+ _store_index_kv_cache_tl_ascend,
13+ _store_kv_cache_llama_tl_ascend,
14+ _store_kv_cache_per32_tl_ascend,
15+ _store_kv_cache_tl_ascend,
16+)
17+from cann_ops_tilelang.config import require_ascend
18+from cann_ops_tilelang.quant import per_token_cast
19+ 
20+ 
21+def _validate_npu_tensor(tensor: torch.Tensor, name: str) -> None:
22+ if tensor.device.type != 'npu':
23+ raise ValueError(f'{name} must be on an NPU, got {tensor.device}.')
24+ 
25+ 
26+def _validate_slot_mapping(slot_mapping: torch.Tensor, expected_tokens: int | None = None) -> None:
27+ _validate_npu_tensor(slot_mapping, 'slot_mapping')
28+ if slot_mapping.ndim != 1 or slot_mapping.dtype != torch.int32:
29+ raise ValueError('slot_mapping must be a 1D torch.int32 tensor.')
30+ if expected_tokens is not None and slot_mapping.numel() != expected_tokens:
31+ raise ValueError(f'slot_mapping must contain {expected_tokens} entries, got {slot_mapping.numel()}.')
32+ 
33+ 
34+def _validate_cache(cache: torch.Tensor, slot_mapping: torch.Tensor, *, dtype: torch.dtype) -> None:
35+ _validate_npu_tensor(cache, 'kv_cache')
36+ if cache.device != slot_mapping.device:
37+ raise ValueError('kv_cache and slot_mapping must be on the same device.')
38+ if cache.ndim != 4 or cache.dtype != dtype:
39+ raise ValueError(f'kv_cache must be a 4D {dtype} tensor.')
40+ 
41+ 
42+def store_kv_cache(
43+ kv: torch.Tensor,
44+ kv_cache: torch.Tensor | None,
45+ slot_mapping: torch.Tensor,
46+ use_fp8_rope: bool = False,
47+ num_per_channels: int = 64,
48+) -> None:
49+ """Store KV to cache with fp8 quantization.
50+ 
51+ Supported configurations:
52+ - (use_fp8_rope=False, num_per_channels=64): nope(448) per-64 fp8 + rope(64) bf16.
53+ bytes_per_token = 448 + 64*2 + 8 = 584.
54+ - (use_fp8_rope=True, num_per_channels=32): full 512 per-32 fp8.
55+ bytes_per_token = 512 + 16 = 528.
56+ 
57+ Args:
58+ kv: [n, 1, 512], bf16
59+ kv_cache: [num_blocks, block_size, 1, bytes_per_token], uint8
60+ slot_mapping: [n], int32, -1 means padding
61+ use_fp8_rope: if True, rope is also stored as fp8 (per32 mode)
62+ num_per_channels: quantization group size (64 or 32)
63+ """
64+ require_ascend()
65+ _validate_npu_tensor(kv, 'kv')
66+ _validate_slot_mapping(slot_mapping, kv.shape[0])
67+ if kv.ndim != 3 or kv.shape[1:] != (1, 512) or kv.dtype != torch.bfloat16:
68+ raise ValueError('kv must have shape [num_tokens, 1, 512] and dtype torch.bfloat16.')
69+ if kv_cache is None:
70+ return
71+ _validate_cache(kv_cache, slot_mapping, dtype=torch.uint8)
72+ if kv.device != kv_cache.device:
73+ raise ValueError('kv and kv_cache must be on the same device.')
74+ if not use_fp8_rope and num_per_channels == 64:
75+ num_blocks, block_size, constant_1, bytes_per_token = kv_cache.shape
76+ assert constant_1 == 1
77+ assert bytes_per_token == 448 + 64 * 2 + 8
78+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
79+ kv_cache_value = kv_cache[:, : block_size * (448 + 64 * 2)].view(num_blocks, block_size, 448 + 64 * 2)
80+ kv_cache_sf = kv_cache[:, block_size * (448 + 64 * 2) :]
81+ kv_cache_fp8_raw = kv_cache_value[:, :, :448]
82+ kv_cache_fp8 = kv_cache_fp8_raw.view(torch.float8_e4m3fn)
83+ kv_cache_bf16 = kv_cache_value[:, :, 448:]
84+ kv_cache_bf16 = kv_cache_bf16.view(*kv_cache_bf16.shape[:-1], -1).view(torch.bfloat16)
85+ kv_cache_sf = kv_cache_sf.view(num_blocks, block_size, 8)
86+ assert kv_cache_fp8.stride(0) == kv_cache_bf16.stride(0) * 2
87+ assert kv_cache_fp8.stride(0) == kv_cache_sf.stride(0)
88+ num_stages = 2
89+ n_tokens = kv.shape[0]
90+ if n_tokens % 64 == 0:
91+ store_block_m = 64
92+ elif n_tokens % 32 == 0:
93+ store_block_m = 32
94+ else:
95+ store_block_m = 16
96+ full_tiles = n_tokens % store_block_m == 0
97+ store_kernel = _store_kv_cache_tl_ascend(
98+ block_size,
99+ num_stages=num_stages,
100+ block_m=store_block_m,
101+ cache_block_stride=kv_cache.stride(0),
102+ static_n=n_tokens if n_tokens == 65536 else 0,
103+ static_num_blocks=num_blocks if block_size in (64, 129) else 0,
104+ full_tiles=full_tiles,
105+ )
106+ store_kernel(kv[:, 0, :], kv_cache, slot_mapping)
107+ elif use_fp8_rope and num_per_channels == 32:
108+ num_blocks, block_size, constant_1, bytes_per_token = kv_cache.shape
109+ assert constant_1 == 1
110+ assert bytes_per_token == 512 + 16
111+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
112+ kv_cache_fp8 = kv_cache[:, : block_size * 512].view(num_blocks, block_size, 512).view(torch.float8_e4m3fn)
113+ kv_cache_sf = kv_cache[:, block_size * 512 :].view(num_blocks, block_size, 16)
114+ assert kv_cache_fp8.stride(0) == kv_cache_sf.stride(0)
115+ store_block_m = 64
116+ store_kernel = _store_kv_cache_per32_tl_ascend(
117+ block_size, full_tiles=kv.shape[0] % store_block_m == 0, block_m=store_block_m
118+ )
119+ store_kernel(kv[:, 0, :], kv_cache_fp8, kv_cache_sf, slot_mapping)
120+ else:
121+ raise ValueError(
122+ f'Unsupported combination: use_fp8_rope={use_fp8_rope}, '
123+ f'num_per_channels={num_per_channels}. Supported: (False, 64) and (True, 32).'
124+ )
125+ 
126+ 
127+def load_kv_cache(
128+ kv_cache: torch.Tensor | None,
129+ slot_mapping: torch.Tensor,
130+ out: torch.Tensor | None = None,
131+ use_fp8_rope: bool = False,
132+ num_per_channels: int = 64,
133+) -> torch.Tensor:
134+ """Load KV from cache with fp8 dequantization.
135+ 
136+ Supported configurations:
137+ - (use_fp8_rope=False, num_per_channels=64): nope(448) per-64 fp8 + rope(64) bf16.
138+ - (use_fp8_rope=True, num_per_channels=32): full 512 per-32 fp8.
139+ 
140+ Args:
141+ kv_cache: [num_blocks, block_size, 1, bytes_per_token], uint8
142+ slot_mapping: [n], int32, -1 means padding
143+ out: optional output tensor [n, 1, 512], bf16
144+ use_fp8_rope: if True, rope is also stored as fp8 (per32 mode)
145+ num_per_channels: quantization group size (64 or 32)
146+ 
147+ Returns:
148+ out: [n, 1, 512], bf16
149+ """
150+ require_ascend()
151+ _validate_slot_mapping(slot_mapping)
152+ if out is not None:
153+ _validate_npu_tensor(out, 'out')
154+ if out.device != slot_mapping.device or out.dtype != torch.bfloat16:
155+ raise ValueError('out must be a BF16 tensor on the slot_mapping device.')
156+ if kv_cache is None:
157+ o = torch.randn(slot_mapping.shape[0], 1, 512, dtype=torch.bfloat16, device=slot_mapping.device)
158+ if out is None:
159+ out = o
160+ else:
161+ out.copy_(o)
162+ return out
163+ _validate_cache(kv_cache, slot_mapping, dtype=torch.uint8)
164+ n = slot_mapping.shape[0]
165+ if not use_fp8_rope and num_per_channels == 64:
166+ num_blocks, block_size, constant_1, bytes_per_token = kv_cache.shape
167+ assert constant_1 == 1
168+ assert bytes_per_token == 448 + 64 * 2 + 8
169+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
170+ kv_cache_value = kv_cache[:, : block_size * (448 + 64 * 2)].view(num_blocks, block_size, 448 + 64 * 2)
171+ kv_cache_sf = kv_cache[:, block_size * (448 + 64 * 2) :]
172+ kv_cache_fp8 = kv_cache_value[:, :, :448].view(torch.float8_e4m3fn)
173+ kv_cache_bf16 = kv_cache_value[:, :, 448:]
174+ kv_cache_bf16 = kv_cache_bf16.view(*kv_cache_bf16.shape[:-1], -1).view(torch.bfloat16)
175+ kv_cache_sf = kv_cache_sf.view(num_blocks, block_size, 8)
176+ assert kv_cache_fp8.stride(0) == kv_cache_bf16.stride(0) * 2
177+ assert kv_cache_fp8.stride(0) == kv_cache_sf.stride(0)
178+ if out is None:
179+ out = torch.empty((n, 1, 512), dtype=torch.bfloat16, device=slot_mapping.device)
180+ else:
181+ assert out.shape == (n, 1, 512)
182+ if n % 64 == 0:
183+ load_kernel = _load_kv_cache_tl_ascend(block_size, num_stages=2, full_tiles=True, block_m=64)
184+ else:
185+ load_kernel = _load_kv_cache_tl_ascend(block_size, num_stages=4, full_tiles=False, block_m=32)
186+ load_kernel(out[:, 0, :], kv_cache_value, kv_cache_sf, slot_mapping)
187+ elif use_fp8_rope and num_per_channels == 32:
188+ num_blocks, block_size, constant_1, bytes_per_token = kv_cache.shape
189+ assert constant_1 == 1
190+ assert bytes_per_token == 512 + 16
191+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
192+ kv_cache_fp8 = kv_cache[:, : block_size * 512].view(num_blocks, block_size, 512).view(torch.float8_e4m3fn)
193+ kv_cache_sf = kv_cache[:, block_size * 512 :].view(num_blocks, block_size, 16)
194+ cache_block_stride = kv_cache_fp8.stride(0)
195+ assert cache_block_stride == kv_cache_sf.stride(0)
196+ assert cache_block_stride >= block_size * (512 + 16)
197+ assert cache_block_stride % 32 == 0
198+ assert kv_cache_fp8.stride(1) == 512
199+ assert kv_cache_sf.stride(1) == 16
200+ if out is None:
201+ out = torch.empty((n, 1, 512), dtype=torch.bfloat16, device=slot_mapping.device)
202+ else:
203+ assert out.shape == (n, 1, 512)
204+ load_block_m = 64 if n % 64 == 0 else 32
205+ load_kernel = _load_kv_cache_per32_tl_ascend(
206+ block_size,
207+ full_tiles=n % load_block_m == 0,
208+ block_m=load_block_m,
209+ cache_block_stride=cache_block_stride,
210+ prefetch_slot_waves=n == 65536 and load_block_m == 64,
211+ )
212+ load_kernel(out[:, 0, :], kv_cache_fp8, kv_cache_sf, slot_mapping)
213+ else:
214+ raise ValueError(
215+ f'Unsupported combination: use_fp8_rope={use_fp8_rope}, '
216+ f'num_per_channels={num_per_channels}. Supported: (False, 64) and (True, 32).'
217+ )
218+ return out
219+ 
220+ 
221+def store_index_kv_cache(
222+ kv: torch.Tensor,
223+ kv_cache: torch.Tensor | None,
224+ slot_mapping: torch.Tensor,
225+ is_fp4: bool = False,
226+ is_mxfp8: bool = False,
227+) -> None:
228+ """
229+ Args:
230+ kv: [n, 1, dim], bf16
231+ kv_cache: [num_blocks, block_size, 1, bytes_per_token], uint8
232+ slot_mapping: [n], int32, -1 means padding
233+ """
234+ require_ascend()
235+ _validate_npu_tensor(kv, 'kv')
236+ _validate_slot_mapping(slot_mapping, kv.shape[0])
237+ if kv.ndim != 3 or kv.shape[1:] != (1, 128) or kv.dtype != torch.bfloat16:
238+ raise ValueError('kv must have shape [num_tokens, 1, 128] and dtype torch.bfloat16.')
239+ if kv_cache is None:
240+ return
241+ _validate_cache(kv_cache, slot_mapping, dtype=torch.uint8)
242+ if kv.device != kv_cache.device:
243+ raise ValueError('kv and kv_cache must be on the same device.')
244+ num_blocks, block_size, constant_1, bytes_per_token = kv_cache.shape
245+ assert constant_1 == 1
246+ if is_fp4:
247+ assert bytes_per_token == 64 + 4
248+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
249+ kv_cache_fp4 = kv_cache[:, : block_size * 64].view(num_blocks, block_size, 64).view(torch.int8)
250+ kv_cache_sf = kv_cache[:, block_size * 64 :].view(num_blocks, block_size, 4).view(torch.uint8)
251+ assert kv_cache_fp4.stride(0) == kv_cache_sf.stride(0)
252+ store_kernel = _store_index_kv_cache_fp4_tl_ascend(
253+ block_size, kv_cache_fp4.stride(1) * 2, kv_cache_sf.stride(1)
254+ )
255+ store_kernel(kv[:, 0, :], kv_cache_fp4, kv_cache_sf, slot_mapping)
256+ elif is_mxfp8:
257+ assert bytes_per_token == 128 + 4
258+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
259+ kv_cache_fp8 = kv_cache[:, : block_size * 128].view(num_blocks, block_size, 128).view(torch.float8_e4m3fn)
260+ kv_cache_sf = kv_cache[:, block_size * 128 :].view(num_blocks, block_size, 4).view(torch.uint8)
261+ assert kv_cache_fp8.stride(0) == kv_cache_sf.stride(0)
262+ store_kernel = _store_index_kv_cache_fp8_per32_tl_ascend(
263+ block_size, kv_cache_fp8.stride(1), kv_cache_sf.stride(1)
264+ )
265+ store_kernel(kv[:, 0, :], kv_cache_fp8, kv_cache_sf, slot_mapping)
266+ else:
267+ assert bytes_per_token == 128 + 4
268+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
269+ kv_cache_fp8 = kv_cache[:, : block_size * 128].view(num_blocks, block_size, 128).view(torch.float8_e4m3fn)
270+ kv_cache_sf = kv_cache[:, block_size * 128 :].view(num_blocks, block_size, 4)
271+ kv_cache_sf = kv_cache_sf.view(*kv_cache_sf.shape[:-1], -1).view(torch.float)
272+ assert kv_cache_fp8.stride(0) == kv_cache_sf.stride(0) * 4
273+ store_kernel = _store_index_kv_cache_tl_ascend(block_size, kv_cache_fp8.stride(1), kv_cache_sf.stride(1))
274+ store_kernel(kv[:, 0, :], kv_cache_fp8, kv_cache_sf, slot_mapping)
275+ 
276+ 
277+def load_index_kv_cache(
278+ kv_cache: torch.Tensor | None, slot_mapping: torch.Tensor, is_fp4: bool = False, is_mxfp8: bool = False
279+) -> tuple[torch.Tensor, torch.Tensor]:
280+ """
281+ Args:
282+ kv_cache: [num_blocks, block_size, 1, bytes_per_token], uint8
283+ slot_mapping: [n], int32, -1 means padding
284+ is_fp4: bool, whether to return fp4 or fp8
285+ 
286+ Returns:
287+ if is_fp4:
288+ kv_fp4: [n, dim // 2], int8
289+ kv_sf: [n], int32
290+ if is_mxfp8 (fp8 per-32 ue8m0):
291+ kv_fp8: [n, dim], float8_e4m3
292+ kv_sf: [n], int32 (packed 4 ue8m0)
293+ else (fp8 per-token float32):
294+ kv_fp8: [n, dim], float8_e4m3
295+ kv_sf: [n], float32
296+ """
297+ require_ascend()
298+ _validate_slot_mapping(slot_mapping)
299+ if kv_cache is None:
300+ if is_fp4:
301+ k_bf16 = torch.randn(slot_mapping.shape[0], 128, dtype=torch.bfloat16, device=slot_mapping.device)
302+ k_fp4, k_sf = per_token_cast(
303+ k_bf16, 'e2m1', 32, round_sf=True, use_tma_aligned_col_major_sf=True, use_packed_ue8m0=True
304+ )
305+ return (k_fp4, k_sf.squeeze(-1))
306+ elif is_mxfp8:
307+ k_bf16 = torch.randn(slot_mapping.shape[0], 128, dtype=torch.bfloat16, device=slot_mapping.device)
308+ k_fp8, k_sf = per_token_cast(
309+ k_bf16, 'e4m3', 32, round_sf=True, use_tma_aligned_col_major_sf=False, use_packed_ue8m0=True
310+ )
311+ return (k_fp8, k_sf.squeeze(-1))
312+ else:
313+ k_bf16 = torch.randn(slot_mapping.shape[0], 128, dtype=torch.bfloat16, device=slot_mapping.device)
314+ k_fp8, k_sf = per_token_cast(k_bf16, 'e4m3', 128, round_sf=True)
315+ return (k_fp8, k_sf.squeeze(-1))
316+ _validate_cache(kv_cache, slot_mapping, dtype=torch.uint8)
317+ num_blocks, block_size, constant_1, bytes_per_token = kv_cache.shape
318+ n = slot_mapping.shape[0]
319+ assert constant_1 == 1
320+ if is_fp4:
321+ assert bytes_per_token == 64 + 4
322+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
323+ kv_cache_fp4 = kv_cache[:, : block_size * 64].view(num_blocks, block_size, 64).view(torch.int8)
324+ kv_cache_sf = kv_cache[:, block_size * 64 :].view(num_blocks, block_size, 4)
325+ kv_cache_sf = kv_cache_sf.view(*kv_cache_sf.shape[:-1], -1).view(torch.int32)
326+ assert kv_cache_fp4.stride(0) == kv_cache_sf.stride(0) * 4
327+ kv_fp4 = torch.empty((n, 64), dtype=torch.int8, device=slot_mapping.device)
328+ kv_sf = torch.empty((n,), dtype=torch.int32, device=slot_mapping.device)
329+ load_kernel = _load_index_kv_cache_fp4_tl_ascend(block_size, kv_cache_fp4.stride(1), kv_cache_sf.stride(1))
330+ load_kernel(kv_fp4.view(torch.uint8), kv_sf, kv_cache_fp4.view(torch.uint8), kv_cache_sf, slot_mapping)
331+ return (kv_fp4, kv_sf)
332+ elif is_mxfp8:
333+ assert bytes_per_token == 128 + 4
334+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
335+ kv_cache_fp8 = kv_cache[:, : block_size * 128].view(num_blocks, block_size, 128).view(torch.float8_e4m3fn)
336+ kv_cache_sf = kv_cache[:, block_size * 128 :].view(num_blocks, block_size, 4)
337+ kv_cache_sf = kv_cache_sf.view(*kv_cache_sf.shape[:-1], -1).view(torch.int32)
338+ assert kv_cache_fp8.stride(0) == kv_cache_sf.stride(0) * 4
339+ kv_fp8 = torch.empty((n, 128), dtype=torch.float8_e4m3fn, device=slot_mapping.device)
340+ kv_scale = torch.empty((n,), dtype=torch.int32, device=slot_mapping.device)
341+ load_kernel = _load_index_kv_cache_fp8_per32_tl_ascend(
342+ block_size, kv_cache_fp8.stride(1), kv_cache_sf.stride(1)
343+ )
344+ load_kernel(kv_fp8.view(torch.uint8), kv_scale, kv_cache_fp8.view(torch.uint8), kv_cache_sf, slot_mapping)
345+ return (kv_fp8, kv_scale)
346+ else:
347+ assert bytes_per_token == 128 + 4
348+ kv_cache = kv_cache.view(num_blocks, block_size * bytes_per_token)
349+ kv_cache_fp8 = kv_cache[:, : block_size * 128].view(num_blocks, block_size, 128).view(torch.float8_e4m3fn)
350+ kv_cache_sf = kv_cache[:, block_size * 128 :].view(num_blocks, block_size, 4)
351+ kv_cache_sf = kv_cache_sf.view(*kv_cache_sf.shape[:-1], -1).view(torch.float)
352+ assert kv_cache_fp8.stride(0) == kv_cache_sf.stride(0) * 4
353+ kv_fp8 = torch.empty((n, 128), dtype=torch.float8_e4m3fn, device=slot_mapping.device)
354+ kv_sf = torch.empty((n,), dtype=torch.float, device=slot_mapping.device)
355+ load_kernel = _load_index_kv_cache_tl_ascend(block_size, kv_cache_fp8.stride(1), kv_cache_sf.stride(1))
356+ load_kernel(
357+ kv_fp8.view(torch.uint8),
358+ kv_sf.view(torch.int32),
359+ kv_cache_fp8.view(torch.uint8),
360+ kv_cache_sf.view(torch.int32),
361+ slot_mapping,
362+ )
363+ return (kv_fp8, kv_sf)
364+ 
365+ 
366+def store_kv_cache_llama(store_kv: torch.Tensor, kv_cache: torch.Tensor | None, slot_mapping: torch.Tensor) -> None:
367+ require_ascend()
368+ _validate_npu_tensor(store_kv, 'store_kv')
369+ _validate_slot_mapping(slot_mapping, store_kv.shape[0])
370+ if store_kv.ndim != 3 or store_kv.shape[2] != 128 or store_kv.dtype != torch.bfloat16:
371+ raise ValueError('store_kv must have shape [num_tokens, num_heads, 128] and dtype torch.bfloat16.')
372+ if kv_cache is None:
373+ return
374+ _validate_cache(kv_cache, slot_mapping, dtype=torch.bfloat16)
375+ if store_kv.device != kv_cache.device:
376+ raise ValueError('store_kv and kv_cache must be on the same device.')
377+ _, block_size, n_head_kv, head_size = kv_cache.shape
378+ assert head_size == 128
379+ store_kv = store_kv.view(-1, n_head_kv * head_size)
380+ kv_cache = kv_cache.view(-1, block_size, n_head_kv * head_size)
381+ store_kernel = _store_kv_cache_llama_tl_ascend(block_size, n_head_kv * head_size)
382+ store_kernel(store_kv, kv_cache, slot_mapping)
383+ 
384+ 
385+def load_kv_cache_llama(kv_cache: torch.Tensor | None, n_head_kv: int, slot_mapping: torch.Tensor) -> torch.Tensor:
386+ require_ascend()
387+ _validate_slot_mapping(slot_mapping)
388+ if n_head_kv <= 0:
389+ raise ValueError('n_head_kv must be positive.')
390+ if kv_cache is None:
391+ return torch.randn((slot_mapping.shape[0], n_head_kv, 128), dtype=torch.bfloat16, device=slot_mapping.device)
392+ _validate_cache(kv_cache, slot_mapping, dtype=torch.bfloat16)
393+ _, block_size, num_head_kv, head_size = kv_cache.shape
394+ assert head_size == 128
395+ assert num_head_kv == n_head_kv
396+ n = slot_mapping.shape[0]
397+ kv_cache = kv_cache.view(-1, block_size, n_head_kv * head_size)
398+ kv = torch.empty((n, n_head_kv * head_size), dtype=torch.bfloat16, device=slot_mapping.device)
399+ load_kernel = _load_kv_cache_llama_tl_ascend(block_size, n_head_kv * head_size)
400+ load_kernel(kv, kv_cache, slot_mapping)
401+ return kv.view(n, n_head_kv, head_size)
@@ -0,0 +1,37 @@
1+import torch
2+ 
3+from cann_ops_tilelang.attention.flash_mla_utils_kernel import _convert_kv_index_tl_ascend
4+from cann_ops_tilelang.config import require_ascend
5+ 
6+ 
7+def convert_kv_index(
8+ topk_idxs: torch.Tensor, block_size: int, context_lens: torch.Tensor, block_tables: torch.Tensor
9+) -> None:
10+ """
11+ Args:
12+ topk_idxs: [bsz, seqlen_q, topk_tokens]
13+ context_lens: [bsz, seqlen_q]
14+ block_tables: [bsz, max_num_blocks]
15+ """
16+ require_ascend()
17+ if any(tensor.device.type != 'npu' for tensor in (topk_idxs, context_lens, block_tables)):
18+ raise ValueError('topk_idxs, context_lens, and block_tables must be NPU tensors.')
19+ if not (topk_idxs.device == context_lens.device == block_tables.device):
20+ raise ValueError('topk_idxs, context_lens, and block_tables must be on the same device.')
21+ if any(tensor.dtype != torch.int32 for tensor in (topk_idxs, context_lens, block_tables)):
22+ raise TypeError('topk_idxs, context_lens, and block_tables must have dtype torch.int32.')
23+ if block_size <= 0:
24+ raise ValueError('block_size must be positive.')
25+ assert topk_idxs.is_contiguous()
26+ assert context_lens.is_contiguous()
27+ assert block_tables.is_contiguous()
28+ assert topk_idxs.ndim == 3
29+ assert context_lens.ndim == 2
30+ assert block_tables.ndim == 2
31+ b, s, k = topk_idxs.shape
32+ _b, n = block_tables.shape
33+ assert b == _b
34+ if context_lens.shape != (b, s):
35+ raise ValueError(f'context_lens must have shape {(b, s)}, got {tuple(context_lens.shape)}.')
36+ kernel = _convert_kv_index_tl_ascend(s, k, n, block_size)
37+ kernel(topk_idxs, context_lens, block_tables)
@@ -0,0 +1,262 @@
1+# Ruff's simplification rules do not model TileLang's compile-time branches.
2+# ruff: noqa: SIM108
3+ 
4+import tilelang
5+from tilelang.ascend import language as T
6+ 
7+from cann_ops_tilelang.config import get_num_vector_cores
8+ 
9+_UB_BUDGET_BYTES = 200 * 1024
10+ 
11+ 
12+@tilelang.jit(
13+ out_idx=None,
14+ target='ascend',
15+ pass_configs={
16+ tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True,
17+ tilelang.PassConfigKey.TL_DISABLE_DATA_RACE_CHECK: True,
18+ },
19+)
20+def _convert_kv_index_tl_ascend(
21+ s: int,
22+ k: int,
23+ n: int,
24+ block_size: int,
25+) -> None:
26+ """Single kernel with compile-time specialization: layouts where k is a
27+ multiple of 64 and n a multiple of 8 use BR-task batches with flat
28+ contiguous DMAs (the batch stride B == k, so DMA and compute offsets
29+ match); anything else uses BR=1 per-task 2D copies (unaligned-safe).
30+ Both share the same SIMD vgather2 core; blk_ub is padded to >=VL
31+ elements so vgather2's 64-lane span stays inside one version slot."""
32+ b = T.dynamic('b')
33+ num_cores = get_num_vector_cores()
34+ vector_length = 64
35+ index_tile_size = (k + vector_length - 1) // vector_length * vector_length
36+ table_tile_size = max(n, vector_length)
37+ batched = k % vector_length == 0 and n % 8 == 0
38+ chunked = False
39+ rows_per_batch = 5
40+ while (
41+ rows_per_batch > 1
42+ and (rows_per_batch * index_tile_size + rows_per_batch * table_tile_size) * 4 * 2 > _UB_BUDGET_BYTES
43+ ):
44+ rows_per_batch -= 1
45+ if batched and (rows_per_batch * index_tile_size + rows_per_batch * table_tile_size) * 4 * 2 > _UB_BUDGET_BYTES:
46+ batched = False
47+ if batched:
48+ num_stages = 2
49+ else:
50+ rows_per_batch = 1
51+ one_stage_bytes = (index_tile_size + table_tile_size) * 4
52+ if one_stage_bytes > _UB_BUDGET_BYTES:
53+ chunked = True
54+ num_stages = 1
55+ else:
56+ num_stages = min(4, _UB_BUDGET_BYTES // one_stage_bytes)
57+ is_pow2 = (block_size & (block_size - 1)) == 0
58+ block_bits = block_size.bit_length() - 1 if is_pow2 else 0
59+ 
60+ if chunked:
61+ index_tile = 4096
62+ block_table_tile = 32768
63+ num_index_blocks = (k + index_tile - 1) // index_tile
64+ 
65+ @T.prim_func
66+ def _convert_kv_index_chunked(
67+ topk_idxs: T.Tensor([b, s, k], T.int32), # type: ignore
68+ context_lens: T.Tensor([b, s], T.int32), # type: ignore
69+ block_tables: T.Tensor([b, n], T.int32), # type: ignore
70+ ):
71+ with T.Kernel(num_cores) as pid:
72+ idx_ub = T.alloc_shared((index_tile,), 'int32')
73+ out_ub = T.alloc_shared((index_tile,), 'int32')
74+ blk_ub = T.alloc_shared((block_table_tile,), 'int32')
75+ 
76+ for task in T.Persistent([b * s * num_index_blocks], num_cores, pid, group_size=1, num_stages=1):
77+ row_task = task // num_index_blocks
78+ index_block = task % num_index_blocks
79+ bid = row_task // s
80+ sid = row_task % s
81+ cl = context_lens[bid, sid]
82+ index_begin = index_block * index_tile
83+ index_count = T.min(index_tile, k - index_begin)
84+ T.copy(
85+ topk_idxs[bid, sid, index_begin : index_begin + index_count],
86+ idx_ub[:index_count],
87+ )
88+ 
89+ with T.SimdVF():
90+ mask = T.simd.pset(32)
91+ neg1 = T.simd.vdup(-1, 'int32', mask)
92+ for vector in T.serial(T.ceildiv(index_count, vector_length)):
93+ T.simd.vsts(out_ub[vector * vector_length], neg1, mask)
94+ 
95+ for table_block in T.serial(T.ceildiv(n, block_table_tile)):
96+ table_begin = table_block * block_table_tile
97+ table_count = T.min(block_table_tile, n - table_begin)
98+ T.copy(
99+ block_tables[bid, table_begin : table_begin + table_count],
100+ blk_ub[:table_count],
101+ )
102+ 
103+ with T.SimdVF():
104+ mask = T.simd.pset(32)
105+ zero = T.simd.vdup(0, 'int32', mask)
106+ table_begin_vec = T.simd.vdup(table_begin, 'int32', mask)
107+ table_end_vec = T.simd.vdup(table_begin + table_count, 'int32', mask)
108+ if is_pow2:
109+ bs_mask = T.simd.vdup(block_size - 1, 'int32', mask)
110+ else:
111+ bs_vec = T.simd.vdup(block_size, 'int32', mask)
112+ 
113+ for vector in T.serial(T.ceildiv(index_count, vector_length)):
114+ global_index = T.simd.vci(index_begin + vector * vector_length, 'int32')
115+ x_vec = T.simd.vld(idx_ub[vector * vector_length])
116+ if is_pow2:
117+ blk_idx = T.simd.vshrs(x_vec, block_bits, mask)
118+ rem = T.simd.vand(x_vec, bs_mask, mask)
119+ else:
120+ blk_idx = T.simd.vdiv(x_vec, bs_vec, mask)
121+ rem = T.simd.vsub(x_vec, T.simd.vmul(blk_idx, bs_vec, mask), mask)
122+ 
123+ valid_index = T.simd.vcmps(global_index, k, mask, 'lt')
124+ valid_context = T.simd.vcmps(global_index, cl, mask, 'lt')
125+ in_table_lower = T.simd.vcmp(blk_idx, table_begin_vec, mask, 'ge')
126+ in_table_upper = T.simd.vcmp(blk_idx, table_end_vec, mask, 'lt')
127+ valid = T.simd.pand(valid_index, valid_context, mask)
128+ valid = T.simd.pand(valid, in_table_lower, mask)
129+ valid = T.simd.pand(valid, in_table_upper, mask)
130+ 
131+ local_blk_idx = T.simd.vsub(blk_idx, table_begin_vec, mask)
132+ local_blk_idx = T.simd.vsel(local_blk_idx, zero, valid)
133+ blk_val = T.simd.vgather2(
134+ blk_ub[0],
135+ T.reinterpret('uint32x64', local_blk_idx),
136+ valid,
137+ )
138+ if is_pow2:
139+ converted = T.simd.vadd(T.simd.vshls(blk_val, block_bits, mask), rem, mask)
140+ else:
141+ converted = T.simd.vadd(T.simd.vmul(blk_val, bs_vec, mask), rem, mask)
142+ previous = T.simd.vld(out_ub[vector * vector_length])
143+ T.simd.vsts(
144+ out_ub[vector * vector_length],
145+ T.simd.vsel(converted, previous, valid),
146+ mask,
147+ )
148+ 
149+ T.copy(
150+ out_ub[:index_count],
151+ topk_idxs[bid, sid, index_begin : index_begin + index_count],
152+ )
153+ 
154+ return _convert_kv_index_chunked
155+ 
156+ @T.prim_func
157+ def _convert_kv_index(
158+ topk_idxs: T.Tensor([b, s, k], T.int32), # type: ignore
159+ context_lens: T.Tensor([b, s], T.int32), # type: ignore
160+ block_tables: T.Tensor([b, n], T.int32), # type: ignore
161+ ):
162+ topk_flat = T.reshape(topk_idxs, (b * s * k,))
163+ cl_flat = T.reshape(context_lens, (b * s,))
164+ bt_flat = T.reshape(block_tables, (b * n,))
165+ ntask = b * s
166+ with T.Kernel(num_cores) as pid:
167+ idx_ub = T.alloc_shared((rows_per_batch * index_tile_size,), 'int32')
168+ blk_ub = T.alloc_shared((rows_per_batch * table_tile_size,), 'int32')
169+ T.annotate_buffer_versions({idx_ub: num_stages, blk_ub: num_stages})
170+ 
171+ t0 = (pid * ntask) // num_cores
172+ t1 = ((pid + 1) * ntask) // num_cores
173+ my = t1 - t0
174+ 
175+ for j in T.Pipelined(T.ceildiv(my, rows_per_batch), num_stages=num_stages):
176+ lo = t0 + j * rows_per_batch
177+ hi = T.min(lo + rows_per_batch, t1)
178+ ln = hi - lo
179+ if ln > 0:
180+ bid_lo = lo // s
181+ 
182+ if batched:
183+ bid_hi = (hi - 1) // s
184+ nrows = bid_hi - bid_lo + 1
185+ 
186+ T.copy(topk_flat[lo * k : hi * k], idx_ub[0 : ln * k])
187+ T.copy(bt_flat[bid_lo * n : bid_lo * n + nrows * n], blk_ub[0 : nrows * n])
188+ else:
189+ for i in T.serial(rows_per_batch):
190+ if i < ln:
191+ task = lo + i
192+ bid = task // s
193+ sid = task % s
194+ T.copy(topk_idxs[bid, sid, 0:k], idx_ub[i * index_tile_size : i * index_tile_size + k])
195+ T.copy(block_tables[bid, 0:n], blk_ub[i * table_tile_size : i * table_tile_size + n])
196+ 
197+ for i in T.serial(rows_per_batch):
198+ if i < ln:
199+ task = lo + i
200+ bid = task // s
201+ cl = cl_flat[task]
202+ if batched:
203+ row = (bid - bid_lo) * n
204+ else:
205+ row = i * table_tile_size
206+ 
207+ with T.SimdVF():
208+ mask = T.simd.pset(32)
209+ neg1 = T.simd.vdup(-1, 'int32', mask)
210+ 
211+ if is_pow2:
212+ bs_mask = T.simd.vdup(block_size - 1, 'int32', mask)
213+ for v in T.serial(index_tile_size // vector_length):
214+ gidx = T.simd.vci(v * vector_length, 'int32')
215+ x_vec = T.simd.vld(idx_ub[i * index_tile_size + v * vector_length])
216+ blk_idx = T.simd.vshrs(x_vec, block_bits, mask)
217+ rem = T.simd.vand(x_vec, bs_mask, mask)
218+ 
219+ mask_k = T.simd.vcmps(gidx, k, mask, 'lt')
220+ mask_cl = T.simd.vcmps(gidx, cl, mask, 'lt')
221+ mask_gather = T.simd.pand(mask_k, mask_cl, mask)
222+ 
223+ blk_val = T.simd.vgather2(
224+ blk_ub[row],
225+ T.reinterpret('uint32x64', blk_idx),
226+ mask_gather,
227+ )
228+ val = T.simd.vadd(T.simd.vshls(blk_val, block_bits, mask), rem, mask)
229+ res = T.simd.vsel(val, neg1, mask_gather)
230+ T.simd.vsts(idx_ub[i * index_tile_size + v * vector_length], res, mask)
231+ else:
232+ bs_vec = T.simd.vdup(block_size, 'int32', mask)
233+ for v in T.serial(index_tile_size // vector_length):
234+ gidx = T.simd.vci(v * vector_length, 'int32')
235+ x_vec = T.simd.vld(idx_ub[i * index_tile_size + v * vector_length])
236+ blk_idx = T.simd.vdiv(x_vec, bs_vec, mask)
237+ rem = T.simd.vsub(x_vec, T.simd.vmul(blk_idx, bs_vec, mask), mask)
238+ 
239+ mask_k = T.simd.vcmps(gidx, k, mask, 'lt')
240+ mask_cl = T.simd.vcmps(gidx, cl, mask, 'lt')
241+ mask_gather = T.simd.pand(mask_k, mask_cl, mask)
242+ 
243+ blk_val = T.simd.vgather2(
244+ blk_ub[row],
245+ T.reinterpret('uint32x64', blk_idx),
246+ mask_gather,
247+ )
248+ val = T.simd.vadd(T.simd.vmul(blk_val, bs_vec, mask), rem, mask)
249+ res = T.simd.vsel(val, neg1, mask_gather)
250+ T.simd.vsts(idx_ub[i * index_tile_size + v * vector_length], res, mask)
251+ 
252+ if batched:
253+ T.copy(idx_ub[0 : ln * k], topk_flat[lo * k : hi * k])
254+ else:
255+ for i in T.serial(rows_per_batch):
256+ if i < ln:
257+ task = lo + i
258+ bid = task // s
259+ sid = task % s
260+ T.copy(idx_ub[i * index_tile_size : i * index_tile_size + k], topk_idxs[bid, sid, 0:k])
261+ 
262+ return _convert_kv_index
@@ -0,0 +1,183 @@
1+import torch
2+ 
3+from cann_ops_tilelang.attention.get_kv_index_kernel import (
4+ _get_dflash_mainx_slot_mapping_tl_ascend,
5+ _get_fused_dflash_get_input_tl_ascend,
6+ _load_kv_index_tl_ascend,
7+)
8+from cann_ops_tilelang.config import require_ascend
9+ 
10+ 
11+def _validate_int32_npu_tensor(tensor: torch.Tensor, name: str, ndim: int | None = None) -> None:
12+ if tensor.device.type != 'npu':
13+ raise ValueError(f'{name} must be on an NPU, got {tensor.device}.')
14+ if tensor.dtype != torch.int32:
15+ raise TypeError(f'{name} must have dtype torch.int32, got {tensor.dtype}.')
16+ if ndim is not None and tensor.ndim != ndim:
17+ raise ValueError(f'{name} must be {ndim}D, got shape {tuple(tensor.shape)}.')
18+ 
19+ 
20+def _validate_offset_len(tensor: torch.Tensor, name: str) -> None:
21+ _validate_int32_npu_tensor(tensor, name, ndim=2)
22+ if tensor.shape[1] != 2:
23+ raise ValueError(f'{name} must have shape [num_sequences, 2], got {tuple(tensor.shape)}.')
24+ 
25+ 
26+def get_load_kv_index_kernel(index_length: int, block_size: int, has_block_tables: bool) -> object:
27+ require_ascend()
28+ if index_length <= 0:
29+ raise ValueError('index_length must be positive.')
30+ if block_size < 0 or (has_block_tables and block_size == 0):
31+ raise ValueError('block_size must be positive when block tables are used, otherwise non-negative.')
32+ return _load_kv_index_tl_ascend(index_length, block_size, has_block_tables)
33+ 
34+ 
35+def get_kv_ca_index_prefill(kv_ca_index_offset_len: torch.Tensor, kv_ca_index_length: int) -> torch.Tensor:
36+ require_ascend()
37+ _validate_offset_len(kv_ca_index_offset_len, 'kv_ca_index_offset_len')
38+ if kv_ca_index_length <= 0:
39+ raise ValueError('kv_ca_index_length must be positive.')
40+ kv_ca_index = torch.empty(
41+ (kv_ca_index_offset_len.shape[0], kv_ca_index_length),
42+ dtype=torch.int32,
43+ device=kv_ca_index_offset_len.device,
44+ )
45+ kernel = get_load_kv_index_kernel(kv_ca_index_length, 0, has_block_tables=False)
46+ kernel(kv_ca_index_offset_len, kv_ca_index, None)
47+ return kv_ca_index
48+ 
49+ 
50+def get_kv_ca_index_decode(
51+ kv_ca_index_offset_len: torch.Tensor, kv_ca_index_length: int, token_per_block: int, block_tables: torch.Tensor
52+) -> torch.Tensor:
53+ require_ascend()
54+ _validate_offset_len(kv_ca_index_offset_len, 'kv_ca_index_offset_len')
55+ _validate_int32_npu_tensor(block_tables, 'block_tables')
56+ if block_tables.device != kv_ca_index_offset_len.device:
57+ raise ValueError('block_tables and kv_ca_index_offset_len must be on the same device.')
58+ if kv_ca_index_length <= 0 or token_per_block <= 0:
59+ raise ValueError('kv_ca_index_length and token_per_block must be positive.')
60+ kv_ca_index = torch.empty(
61+ (kv_ca_index_offset_len.shape[0], kv_ca_index_length),
62+ dtype=torch.int32,
63+ device=kv_ca_index_offset_len.device,
64+ )
65+ kernel = get_load_kv_index_kernel(kv_ca_index_length, token_per_block, has_block_tables=True)
66+ kernel(kv_ca_index_offset_len, kv_ca_index, block_tables.flatten())
67+ return kv_ca_index
68+ 
69+ 
70+def get_kv_swa_index_decode(
71+ kv_swa_index_offset_len: torch.Tensor, window_size: int, swa_block_size: int
72+) -> torch.Tensor:
73+ require_ascend()
74+ _validate_offset_len(kv_swa_index_offset_len, 'kv_swa_index_offset_len')
75+ if window_size <= 0 or swa_block_size <= 0:
76+ raise ValueError('window_size and swa_block_size must be positive.')
77+ kv_swa_index = torch.empty(
78+ (kv_swa_index_offset_len.shape[0], window_size),
79+ dtype=torch.int32,
80+ device=kv_swa_index_offset_len.device,
81+ )
82+ kernel = get_load_kv_index_kernel(window_size, swa_block_size, has_block_tables=False)
83+ kernel(kv_swa_index_offset_len, kv_swa_index, None)
84+ return kv_swa_index
85+ 
86+ 
87+def get_fused_dflash_get_input(
88+ slot_mapping_swa: torch.Tensor,
89+ positions: torch.Tensor,
90+ ftp: torch.Tensor,
91+ accepted_pos_real: torch.Tensor,
92+ swa_block_size: int,
93+ dflash_block_size: int,
94+ window_size: int,
95+ has_valid_token_mask: bool = False,
96+ has_gate_force_random: bool = False,
97+ input_ids_dtype: torch.dtype = torch.int32,
98+) -> tuple[
99+ torch.Tensor,
100+ torch.Tensor,
101+ torch.Tensor,
102+ torch.Tensor | None,
103+ torch.Tensor | None,
104+ torch.Tensor,
105+]:
106+ """Return slot mapping, positions, IDs, optional masks, and KV indices."""
107+ require_ascend()
108+ for name, tensor in (
109+ ('slot_mapping_swa', slot_mapping_swa),
110+ ('positions', positions),
111+ ('ftp', ftp),
112+ ('accepted_pos_real', accepted_pos_real),
113+ ):
114+ _validate_int32_npu_tensor(tensor, name, ndim=1)
115+ devices = {tensor.device for tensor in (slot_mapping_swa, positions, ftp, accepted_pos_real)}
116+ if len(devices) != 1:
117+ raise ValueError('all inputs must be on the same NPU device.')
118+ if not 0 < dflash_block_size <= 32:
119+ raise ValueError(f'dflash_block_size must be in [1, 32], got {dflash_block_size}.')
120+ if swa_block_size <= 0 or window_size <= 0:
121+ raise ValueError('swa_block_size and window_size must be positive.')
122+ real_cnt = accepted_pos_real.shape[0]
123+ if real_cnt > ftp.numel():
124+ raise ValueError('accepted_pos_real cannot contain more entries than ftp.')
125+ kv_len = window_size + dflash_block_size
126+ padded_kv_len = (kv_len + 63) // 64 * 64
127+ device = accepted_pos_real.device
128+ newx_slot_mapping_out = torch.empty((real_cnt, dflash_block_size), dtype=torch.int32, device=device)
129+ positions_out = torch.empty((real_cnt, dflash_block_size), dtype=torch.int32, device=device)
130+ input_ids_out = torch.empty((real_cnt, dflash_block_size), dtype=torch.int32, device=device)
131+ valid_token_mask_out = (
132+ torch.empty((real_cnt, dflash_block_size), dtype=torch.bool, device=device) if has_valid_token_mask else None
133+ )
134+ gate_force_random_out = (
135+ torch.empty((real_cnt, dflash_block_size), dtype=torch.bool, device=device) if has_gate_force_random else None
136+ )
137+ kv_index_out = torch.empty((real_cnt * dflash_block_size, padded_kv_len), dtype=torch.int32, device=device)
138+ kernel = _get_fused_dflash_get_input_tl_ascend(
139+ swa_block_size, padded_kv_len, dflash_block_size, has_valid_token_mask, has_gate_force_random
140+ )
141+ kernel(
142+ slot_mapping_swa,
143+ positions,
144+ ftp[:real_cnt],
145+ accepted_pos_real,
146+ newx_slot_mapping_out,
147+ positions_out,
148+ input_ids_out,
149+ valid_token_mask_out,
150+ gate_force_random_out,
151+ kv_index_out,
152+ kv_len,
153+ window_size,
154+ )
155+ valid_token_mask = valid_token_mask_out.view(-1) if has_valid_token_mask else None
156+ gate_force_random = gate_force_random_out.view(-1) if has_gate_force_random else None
157+ return (
158+ newx_slot_mapping_out.view(-1),
159+ positions_out.view(-1),
160+ input_ids_out.to(input_ids_dtype).view(-1),
161+ valid_token_mask,
162+ gate_force_random,
163+ kv_index_out,
164+ )
165+ 
166+ 
167+def get_dflash_mainx_slot_mapping(
168+ ftp: torch.Tensor, valid_offsets: torch.Tensor, slot_mapping_swa: torch.Tensor, real_cnt: int
169+) -> torch.Tensor:
170+ """Fused tilelang kernel for computing dflash_mainx_slot_mapping_store."""
171+ require_ascend()
172+ for name, tensor in (('ftp', ftp), ('valid_offsets', valid_offsets), ('slot_mapping_swa', slot_mapping_swa)):
173+ _validate_int32_npu_tensor(tensor, name, ndim=1)
174+ if not (ftp.device == valid_offsets.device == slot_mapping_swa.device):
175+ raise ValueError('ftp, valid_offsets, and slot_mapping_swa must be on the same device.')
176+ if not 0 <= real_cnt <= min(ftp.numel(), valid_offsets.numel()):
177+ raise ValueError('real_cnt must fit within ftp and valid_offsets.')
178+ total_q = slot_mapping_swa.shape[0]
179+ out = torch.empty(total_q, dtype=torch.int32, device=slot_mapping_swa.device)
180+ get_kernel = _get_dflash_mainx_slot_mapping_tl_ascend
181+ kernel = get_kernel()
182+ kernel(ftp[:real_cnt], valid_offsets[:real_cnt], slot_mapping_swa, out)
183+ return out
@@ -0,0 +1,405 @@
1+# Ruff's simplification rules do not model TileLang's compile-time branches.
2+# ruff: noqa: SIM102
3+ 
4+import tilelang
5+from tilelang.ascend import language as T
6+ 
7+from cann_ops_tilelang.config import get_num_vector_cores
8+ 
9+ 
10+@tilelang.jit(pass_configs={tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True})
11+def _load_kv_index_tl_ascend(
12+ index_length: int,
13+ block_size: int,
14+ has_block_tables: bool,
15+) -> None:
16+ n = T.dynamic('n')
17+ num_blocks = T.dynamic('num_blocks') if has_block_tables else 1
18+ num_cores = get_num_vector_cores()
19+ vector_length = 64
20+ index_tile_size = T.ceildiv(index_length, vector_length) * vector_length if index_length <= 8192 else 8192 * 2
21+ block_table_tile_size = T.ceildiv((index_tile_size // block_size + 1) if has_block_tables else 0, 8) * 8
22+ num_stages = 2
23+ is_pow2 = (block_size & (block_size - 1)) == 0
24+ assert not has_block_tables or is_pow2, 'swa or 2^k block size'
25+ if block_size > 0 and is_pow2:
26+ block_bits = block_size.bit_length() - 1
27+ inc_r = vector_length % block_size if block_size > 0 else 0
28+ 
29+ blocks_per_token = T.ceildiv(index_length, index_tile_size)
30+ 
31+ @T.prim_func
32+ def _load_kv_index(
33+ kv_index_offset_len: T.Tensor((n, 2), 'int32'), # type: ignore
34+ kv_index: T.Tensor((n, index_length), 'int32'), # type: ignore
35+ flatten_block_tables: T.Tensor((num_blocks), 'int32'), # type: ignore
36+ ):
37+ with T.Kernel(num_cores) as pid:
38+ index = T.alloc_shared((index_tile_size,), 'int32') # type: ignore
39+ T.annotate_buffer_versions({index: num_stages})
40+ if has_block_tables:
41+ blk_ub = T.alloc_shared((block_table_tile_size,), 'int32') # type: ignore
42+ T.annotate_buffer_versions({blk_ub: num_stages})
43+ for bid in T.Persistent([n * blocks_per_token], num_cores, pid, group_size=1, num_stages=num_stages):
44+ bx = bid // blocks_per_token
45+ i = bid % blocks_per_token
46+ copy_length = T.min(index_tile_size, index_length - i * index_tile_size)
47+ offset = kv_index_offset_len[bx, 0]
48+ valid_length = kv_index_offset_len[bx, 1]
49+ 
50+ if has_block_tables:
51+ valid_begin = i * index_tile_size
52+ valid_count = T.max(0, T.min(copy_length, valid_length - valid_begin))
53+ base_blk = T.alloc_var(T.int32, 0)
54+ slice_len = T.alloc_var(T.int32, 0)
55+ if valid_count > 0:
56+ base_blk = (offset + valid_begin) // block_size
57+ last_x = valid_begin + valid_count - 1
58+ slice_len = (offset + last_x) // block_size - base_blk + 1
59+ T.copy(flatten_block_tables[base_blk : base_blk + slice_len], blk_ub[:slice_len])
60+ 
61+ with T.SimdVF():
62+ mask = T.simd.pset(32)
63+ neg1 = T.simd.vdup(-1, 'int32', mask)
64+ 
65+ if has_block_tables:
66+ mask_const = T.simd.vdup(block_size - 1, 'int32', mask)
67+ inc_q = vector_length // block_size
68+ q_var = T.simd.alloc_var('int32')
69+ rem_var = T.simd.alloc_var('int32')
70+ pos0 = T.simd.vadds(T.simd.vci(i * index_tile_size, 'int32'), offset, mask)
71+ q_var = T.simd.vshrs(pos0, block_bits, mask)
72+ rem_var = T.simd.vand(pos0, mask_const, mask)
73+ q_var = T.simd.vadds(q_var, -base_blk, mask)
74+ for v in T.serial(index_tile_size // vector_length):
75+ gidx = T.simd.vci(i * index_tile_size + v * vector_length, 'int32')
76+ mask_valid = T.simd.vcmps(q_var, slice_len, mask, 'lt')
77+ blk = T.simd.vgather2(blk_ub[0], T.reinterpret('uint32x64', q_var), mask_valid)
78+ val = T.simd.vadd(T.simd.vshls(blk, block_bits, mask), rem_var, mask)
79+ res = T.simd.vsel(val, neg1, T.simd.vcmps(gidx, valid_length, mask, 'lt'))
80+ T.simd.vsts(index[v * vector_length], res, mask)
81+ 
82+ q_var = T.simd.vadds(q_var, inc_q, mask)
83+ if inc_r > 0:
84+ rem_var = T.simd.vadds(rem_var, inc_r, mask)
85+ ge = T.simd.vcmps(rem_var, block_size, mask, 'ge')
86+ rem_var = T.simd.vand(rem_var, mask_const, mask)
87+ q_var = T.simd.vsel(T.simd.vadds(q_var, 1, mask), q_var, ge)
88+ elif block_size == 0:
89+ for v in T.serial(index_tile_size // vector_length):
90+ gidx = T.simd.vci(offset + i * index_tile_size + v * vector_length, 'int32')
91+ res = T.simd.vsel(gidx, neg1, T.simd.vcmps(gidx, offset + valid_length, mask, 'lt'))
92+ T.simd.vsts(index[v * vector_length], res, mask)
93+ else:
94+ off_base = offset // block_size * block_size
95+ rem_var = T.simd.alloc_var('int32')
96+ pos0 = T.simd.vadds(T.simd.vci(i * index_tile_size, 'int32'), offset, mask)
97+ if is_pow2:
98+ mask_const = T.simd.vdup(block_size - 1, 'int32', mask)
99+ rem_var = T.simd.vand(pos0, mask_const, mask)
100+ else:
101+ bs_vec = T.simd.vdup(block_size, 'int32', mask)
102+ pos0_q = T.simd.vdiv(pos0, bs_vec, mask)
103+ rem_var = T.simd.vsub(pos0, T.simd.vmul(pos0_q, bs_vec, mask), mask)
104+ rem_var = T.simd.vadds(rem_var, off_base, mask)
105+ for v in T.serial(index_tile_size // vector_length):
106+ gidx = T.simd.vci(i * index_tile_size + v * vector_length, 'int32')
107+ res = T.simd.vsel(rem_var, neg1, T.simd.vcmps(gidx, valid_length, mask, 'lt'))
108+ T.simd.vsts(index[v * vector_length], res, mask)
109+ if inc_r > 0:
110+ rem_var = T.simd.vadds(rem_var, inc_r, mask)
111+ ge = T.simd.vcmps(rem_var, block_size + off_base, mask, 'ge')
112+ rem_var = T.simd.vsel(T.simd.vadds(rem_var, -block_size, mask), rem_var, ge)
113+ T.copy(index[:copy_length], kv_index[bx, i * index_tile_size : i * index_tile_size + copy_length])
114+ 
115+ return _load_kv_index
116+ 
117+ 
118+def get_ascend_disable_smem_reuse_config() -> dict:
119+ pass_configs = {tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True}
120+ key = getattr(tilelang.PassConfigKey, 'TL_DISABLE_SHARED_MEMORY_REUSE', None)
121+ if key is not None:
122+ pass_configs[key] = True
123+ return pass_configs
124+ 
125+ 
126+@tilelang.jit(
127+ out_idx=None,
128+ pass_configs=get_ascend_disable_smem_reuse_config(),
129+)
130+def _get_fused_dflash_get_input_tl_ascend(
131+ swa_block_size: int,
132+ padded_kv_len: int,
133+ dflash_block_size: int,
134+ has_valid_token_mask: bool,
135+ has_gate_force_random: bool,
136+) -> None:
137+ real_cnt = T.dynamic('real_cnt')
138+ total_q = T.dynamic('total_q')
139+ kv_index_out_shape0 = T.dynamic('kv_index_out_shape0')
140+ num_cores = get_num_vector_cores()
141+ vector_length = 64
142+ bs = dflash_block_size
143+ compact_kv = bs * padded_kv_len * 4 > 64 * 1024
144+ num_stages = 1 if compact_kv and padded_kv_len * 4 > 96 * 1024 else 2
145+ # requests per persistent iteration: keep the kv chunk UB under 64KB (x2 for double buffering)
146+ kv_elems_per_request = padded_kv_len if compact_kv else bs * padded_kv_len
147+ requests_per_group = max(1, min(8, (64 * 1024) // (kv_elems_per_request * 4)))
148+ rows = requests_per_group * bs
149+ token_span = 1024 # token-span staging window (elements) for the per-request scalar gather
150+ is_pow2 = (swa_block_size & (swa_block_size - 1)) == 0
151+ 
152+ @T.prim_func
153+ def _get_fused_dflash_get_input(
154+ slot_mapping_swa: T.Tensor((total_q,), 'int32'), # type: ignore
155+ positions: T.Tensor((total_q,), 'int32'), # type: ignore
156+ ftp: T.Tensor((real_cnt,), 'int32'), # type: ignore
157+ accepted_pos_real: T.Tensor((real_cnt,), 'int32'), # type: ignore
158+ newx_slot_mapping_out: T.Tensor((real_cnt, bs), 'int32'), # type: ignore
159+ positions_out: T.Tensor((real_cnt, bs), 'int32'), # type: ignore
160+ input_ids_out: T.Tensor((real_cnt, bs), 'int32'), # type: ignore
161+ valid_token_mask_out: T.Tensor((real_cnt, bs), 'bool'), # type: ignore
162+ gate_force_random_out: T.Tensor((real_cnt, bs), 'bool'), # type: ignore
163+ kv_index_out: T.Tensor((kv_index_out_shape0, padded_kv_len), 'int32'), # type: ignore
164+ kv_len: T.int32,
165+ window_size: T.int32,
166+ ):
167+ total_rows = real_cnt * bs
168+ ids_1d = T.reshape(input_ids_out, (total_rows,))
169+ vtm_1d = T.reshape(valid_token_mask_out, (total_rows,))
170+ gfr_1d = T.reshape(gate_force_random_out, (total_rows,))
171+ newx_1d = T.reshape(newx_slot_mapping_out, (total_rows,))
172+ pos_1d = T.reshape(positions_out, (total_rows,))
173+ kvout_1d = T.reshape(kv_index_out, (kv_index_out_shape0 * padded_kv_len,))
174+ 
175+ with T.Kernel(num_cores) as pid:
176+ # constant-fill sources: vector-filled int32 patterns (z: 0, t: 0x01010101)
177+ z_ub = T.alloc_shared((256,), 'int32') # type: ignore
178+ t32_ub = T.alloc_shared((256,), 'int32')
179+ z8 = T.view(z_ub, (1024,), T.bool) # type: ignore # bool view for gfr fill
180+ t_ub = T.view(t32_ub, (1024,), T.bool) # type: ignore # bool view for vtm fill
181+ with T.SimdVF():
182+ mask = T.simd.pset(32)
183+ z_vec = T.simd.vdup(0, 'int32', mask)
184+ t_vec = T.simd.vdup(0x01010101, 'int32', mask)
185+ for c in T.serial(4):
186+ T.simd.vsts(z_ub[c * 64], z_vec, mask)
187+ T.simd.vsts(t32_ub[c * 64], t_vec, mask)
188+ 
189+ ftp_ub = T.alloc_shared((requests_per_group + 1,), 'int32') # type: ignore
190+ ap_ub = T.alloc_shared((64,), 'int32') # type: ignore
191+ so_ub = T.alloc_shared((64,), 'int32') # type: ignore
192+ start_ub = T.alloc_shared((64,), 'int32') # type: ignore
193+ pos_span = T.alloc_shared((token_span,), 'int32') # type: ignore
194+ slot_span = T.alloc_shared((token_span,), 'int32') # type: ignore
195+ kv2 = T.alloc_shared((requests_per_group * kv_elems_per_request,), 'int32') # type: ignore
196+ nx_ub = T.alloc_shared(((rows + 63) // 64 * 64,), 'int32') # type: ignore
197+ pos_out_ub = T.alloc_shared(((rows + 63) // 64 * 64,), 'int32') # type: ignore
198+ T.annotate_buffer_versions(
199+ {
200+ ftp_ub: num_stages,
201+ ap_ub: num_stages,
202+ so_ub: num_stages,
203+ start_ub: num_stages,
204+ pos_span: num_stages,
205+ slot_span: num_stages,
206+ kv2: num_stages,
207+ nx_ub: num_stages,
208+ pos_out_ub: num_stages,
209+ }
210+ )
211+ 
212+ for blk in T.Persistent(
213+ [T.ceildiv(real_cnt, requests_per_group)], num_cores, pid, group_size=1, num_stages=num_stages
214+ ):
215+ g0 = blk * requests_per_group
216+ glen = T.min(requests_per_group, real_cnt - g0)
217+ rows_g = glen * bs
218+ 
219+ # 1) request metadata: ftp (+1 boundary), ap
220+ load_len = T.min(requests_per_group + 1, real_cnt - g0)
221+ T.copy(ftp[g0 : g0 + load_len], ftp_ub[:load_len])
222+ T.copy(accepted_pos_real[g0 : g0 + glen], ap_ub[:glen])
223+ s0 = ftp_ub[0]
224+ s1 = T.Select(g0 + glen < real_cnt, ftp_ub[glen], total_q)
225+ 
226+ # 2) per-request so/start via span windows (few large MTE2 instead of 2G tiny ones)
227+ nwin = T.ceildiv(s1 - s0, token_span)
228+ for w in T.serial(nwin):
229+ w0 = s0 + w * token_span
230+ wlen = T.min(token_span, s1 - w0)
231+ T.copy(positions[w0 : w0 + wlen], pos_span[:wlen])
232+ T.copy(slot_mapping_swa[w0 : w0 + wlen], slot_span[:wlen])
233+ for r in T.serial(requests_per_group):
234+ if r < glen:
235+ if ftp_ub[r] >= w0:
236+ if ftp_ub[r] < w0 + wlen:
237+ off = ftp_ub[r] - w0
238+ so_ub[r] = slot_span[off] - pos_span[off] % swa_block_size
239+ start_ub[r] = ap_ub[r] - (window_size - 1)
240+ 
241+ # 3) kv rows: compute one row per request, replicate to bs rows, one contiguous write
242+ with T.SimdVF():
243+ mask = T.simd.pset(32)
244+ neg1 = T.simd.vdup(-1, 'int32', mask)
245+ swa_vec = T.simd.vdup(swa_block_size, 'int32', mask)
246+ swa_mask_vec = T.simd.vdup(swa_block_size - 1, 'int32', mask)
247+ kv_len_vec = T.simd.vdup(kv_len, 'int32', mask)
248+ col = T.simd.alloc_var('int32')
249+ for r in T.serial(requests_per_group):
250+ if r < glen:
251+ so_vec = T.simd.vdup(so_ub[r], 'int32', mask)
252+ start_vec = T.simd.vdup(start_ub[r], 'int32', mask)
253+ col = T.simd.vci(0, 'int32')
254+ for v in T.serial(T.ceildiv(padded_kv_len, vector_length)):
255+ pos = T.simd.vadd(col, start_vec, mask)
256+ if is_pow2:
257+ rem = T.simd.vand(pos, swa_mask_vec, mask)
258+ else:
259+ pos_q = T.simd.vdiv(pos, swa_vec, mask)
260+ rem = T.simd.vsub(pos, T.simd.vmul(pos_q, swa_vec, mask), mask)
261+ val = T.simd.vadd(rem, so_vec, mask)
262+ # static-operand compares only (avoid scalar->vector syncs)
263+ mask_len = T.simd.vcmps(T.simd.vsub(col, kv_len_vec, mask), 0, mask, 'lt')
264+ mask_ge = T.simd.vcmps(pos, 0, mask, 'ge')
265+ res = T.simd.vsel(val, neg1, T.simd.pand(mask_len, mask_ge, mask))
266+ if compact_kv:
267+ T.simd.vsts(kv2[r * padded_kv_len + v * vector_length], res, mask)
268+ else:
269+ for bx in T.serial(bs):
270+ T.simd.vsts(kv2[(r * bs + bx) * padded_kv_len + v * vector_length], res, mask)
271+ col = T.simd.vadds(col, 64, mask)
272+ 
273+ # 4) newx / positions: gather pass over the group's rows, 64-lane chunks
274+ with T.SimdVF():
275+ mask = T.simd.pset(32)
276+ swa_vec = T.simd.vdup(swa_block_size, 'int32', mask)
277+ swa_mask_vec = T.simd.vdup(swa_block_size - 1, 'int32', mask)
278+ bs_vec = T.simd.vdup(bs, 'int32', mask)
279+ for ch in T.serial((rows + 63) // 64):
280+ lane = T.simd.vci(ch * 64, 'int32')
281+ r_lane = T.simd.vdiv(lane, bs_vec, mask)
282+ r_lane = T.simd.vmins(r_lane, requests_per_group - 1, mask)
283+ bx_lane = T.simd.vsub(lane, T.simd.vmul(r_lane, bs_vec, mask), mask)
284+ r_idx = T.reinterpret('uint32x64', r_lane)
285+ ap_g = T.simd.vgather2(ap_ub[0], r_idx, mask)
286+ so_g = T.simd.vgather2(so_ub[0], r_idx, mask)
287+ posv = T.simd.vadd(T.simd.vadds(bx_lane, 1, mask), ap_g, mask)
288+ if is_pow2:
289+ rem2 = T.simd.vand(posv, swa_mask_vec, mask)
290+ else:
291+ q2 = T.simd.vdiv(posv, swa_vec, mask)
292+ rem2 = T.simd.vsub(posv, T.simd.vmul(q2, swa_vec, mask), mask)
293+ newx = T.simd.vadd(rem2, so_g, mask)
294+ mact = T.simd.vcmps(lane, rows, mask, 'lt')
295+ T.simd.vsts(nx_ub[ch * 64], newx, mact)
296+ T.simd.vsts(pos_out_ub[ch * 64], posv, mact)
297+ T.copy(nx_ub[:rows_g], newx_1d[g0 * bs : g0 * bs + rows_g])
298+ T.copy(pos_out_ub[:rows_g], pos_1d[g0 * bs : g0 * bs + rows_g])
299+ 
300+ # 5) constant fills for the group's rows
301+ off1 = g0 * bs
302+ T.copy(z_ub[:rows_g], ids_1d[off1 : off1 + rows_g])
303+ if has_valid_token_mask:
304+ T.copy(t_ub[:rows_g], vtm_1d[off1 : off1 + rows_g])
305+ if has_gate_force_random:
306+ T.copy(z8[:rows_g], gfr_1d[off1 : off1 + rows_g])
307+ 
308+ # 6) kv_index: keep the one-copy path for normal shapes and
309+ # replicate from one staged row when the expanded group would
310+ # exceed UB capacity.
311+ if compact_kv:
312+ for r in T.serial(requests_per_group):
313+ if r < glen:
314+ for bx in T.serial(bs):
315+ out_row = (g0 + r) * bs + bx
316+ T.copy(
317+ kv2[r * padded_kv_len : (r + 1) * padded_kv_len],
318+ kvout_1d[out_row * padded_kv_len : (out_row + 1) * padded_kv_len],
319+ )
320+ else:
321+ T.copy(
322+ kv2[: rows_g * padded_kv_len],
323+ kvout_1d[g0 * bs * padded_kv_len : g0 * bs * padded_kv_len + rows_g * padded_kv_len],
324+ )
325+ 
326+ return _get_fused_dflash_get_input
327+ 
328+ 
329+@tilelang.jit(
330+ out_idx=None,
331+ pass_configs={
332+ tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True,
333+ },
334+)
335+def _get_dflash_mainx_slot_mapping_tl_ascend() -> None:
336+ real_cnt = T.dynamic('real_cnt')
337+ total_q = T.dynamic('total_q')
338+ num_cores = get_num_vector_cores()
339+ num_stages = 4
340+ threads = 32
341+ requests_per_block = 16
342+ slot_tile_size = requests_per_block * 64
343+ 
344+ @T.prim_func
345+ def _get_dflash_mainx_slot_mapping(
346+ ftp: T.Tensor((real_cnt,), 'int32'), # type: ignore
347+ valid_offsets: T.Tensor((real_cnt,), 'int32'), # type: ignore
348+ slot_mapping_swa: T.Tensor((total_q,), 'int32'), # type: ignore
349+ out: T.Tensor((total_q,), 'int32'), # type: ignore
350+ ):
351+ with T.Kernel(num_cores) as pid:
352+ valid_ub = T.alloc_shared((requests_per_block,), 'int32') # type: ignore
353+ ftp_ub = T.alloc_shared((requests_per_block,), 'int32') # type: ignore
354+ out_ub = T.alloc_shared((slot_tile_size,), 'int32') # type: ignore
355+ T.annotate_buffer_versions({ftp_ub: num_stages, valid_ub: num_stages, out_ub: num_stages})
356+ 
357+ for bid in T.Persistent([T.ceildiv(real_cnt, requests_per_block)], num_cores, pid):
358+ bx_start = bid * requests_per_block
359+ valid_cnt = T.min(requests_per_block, real_cnt - bx_start)
360+ total_start = ftp[bx_start]
361+ total_end = T.Select(bx_start + valid_cnt < real_cnt, ftp[bx_start + valid_cnt], total_q)
362+ range_len = total_end - total_start
363+ 
364+ T.copy(ftp[bx_start : bx_start + valid_cnt], ftp_ub[:valid_cnt])
365+ T.copy(valid_offsets[bx_start : bx_start + valid_cnt], valid_ub[:valid_cnt])
366+ if range_len <= slot_tile_size:
367+ T.copy(slot_mapping_swa[total_start:total_end], out_ub[:range_len])
368+ 
369+ with T.SimtVF(threads=requests_per_block * threads):
370+ for r, j in T.Parallel(requests_per_block, threads):
371+ if r < valid_cnt:
372+ local_start = ftp_ub[r]
373+ local_end = T.Select(r + 1 < valid_cnt, ftp_ub[r + 1], total_end)
374+ valid_end = local_start + valid_ub[r] + 1
375+ valid_cut = T.min(valid_end, local_end)
376+ for j0 in T.serial(valid_cut, local_end, threads):
377+ pos = j0 + j
378+ if pos < local_end:
379+ out_ub[pos - total_start] = -1
380+ 
381+ T.copy(out_ub[:range_len], out[total_start:total_end])
382+ else:
383+ for chunk_id in T.serial(T.ceildiv(range_len, slot_tile_size)):
384+ chunk_start = total_start + chunk_id * slot_tile_size
385+ chunk_len = T.min(slot_tile_size, total_end - chunk_start)
386+ chunk_end = chunk_start + chunk_len
387+ T.copy(slot_mapping_swa[chunk_start:chunk_end], out_ub[:chunk_len])
388+ 
389+ with T.SimtVF(threads=requests_per_block * threads):
390+ for r, j in T.Parallel(requests_per_block, threads):
391+ if r < valid_cnt:
392+ local_start = ftp_ub[r]
393+ local_end = T.Select(r + 1 < valid_cnt, ftp_ub[r + 1], total_end)
394+ valid_end = local_start + valid_ub[r] + 1
395+ valid_cut = T.min(valid_end, local_end)
396+ invalid_start = T.max(valid_cut, chunk_start)
397+ invalid_end = T.min(local_end, chunk_end)
398+ for j0 in T.serial(invalid_start, invalid_end, threads):
399+ pos = j0 + j
400+ if pos < invalid_end:
401+ out_ub[pos - chunk_start] = -1
402+ 
403+ T.copy(out_ub[:chunk_len], out[chunk_start:chunk_end])
404+ 
405+ return _get_dflash_mainx_slot_mapping
@@ -0,0 +1,47 @@
1+import torch
2+ 
3+from cann_ops_tilelang.attention.kv_index_cat_kernel import _kv_index_cat_tl_ascend
4+from cann_ops_tilelang.config import require_ascend
5+ 
6+ 
7+def kv_index_cat(kv_index: torch.Tensor, kv_ca_index: torch.Tensor, total_k_swa: int, total_k: int) -> torch.Tensor:
8+ """
9+ Args:
10+ kv_index: [total_q, window_size], int32
11+ kv_ca_index: [total_q, topk_tokens], int32
12+ total_k_swa: scalar int32
13+ total_k: scalar int32
14+ 
15+ Returns:
16+ out: [total_q, window_size + topk_tokens], int32
17+ """
18+ require_ascend()
19+ assert kv_index.ndim == 2, 'kv_index must be a 2D tensor'
20+ assert kv_ca_index.ndim == 2, 'kv_ca_index must be a 2D tensor'
21+ total_q, window_size = kv_index.shape
22+ _, topk_tokens = kv_ca_index.shape
23+ assert total_q > 0, 'total_q must be positive'
24+ assert window_size > 0, 'window_size must be positive'
25+ assert topk_tokens > 0, 'topk_tokens must be positive'
26+ assert kv_ca_index.shape[0] == total_q, 'both inputs must have the same total_q'
27+ assert kv_index.is_contiguous(), 'kv_index must be contiguous'
28+ assert kv_ca_index.is_contiguous(), 'kv_ca_index must be contiguous'
29+ assert kv_index.dtype == torch.int32, 'kv_index must use int32'
30+ assert kv_ca_index.dtype == torch.int32, 'kv_ca_index must use int32'
31+ assert kv_index.device == kv_ca_index.device, 'both inputs must be on the same device'
32+ if kv_index.device.type != 'npu':
33+ raise ValueError(f'input tensors must be on an NPU, got {kv_index.device}.')
34+ assert kv_index.stride(0) % 4 == 0, 'window_size must be aligned to 4 elements'
35+ out_shape = window_size + topk_tokens
36+ out_stride = (out_shape + 3) // 4 * 4
37+ out = torch.empty((total_q, out_stride), dtype=kv_index.dtype, device=kv_index.device)[:, :out_shape]
38+ kernel = _kv_index_cat_tl_ascend('int32', window_size, topk_tokens, out_stride)
39+ kernel(
40+ kv_index,
41+ kv_ca_index,
42+ out[:, :window_size],
43+ out[:, window_size : window_size + topk_tokens],
44+ total_k_swa,
45+ total_k,
46+ )
47+ return out
@@ -0,0 +1,128 @@
1+import tilelang
2+from tilelang.ascend import language as T
3+ 
4+from cann_ops_tilelang.config import get_num_vector_cores
5+ 
6+ 
7+@tilelang.jit(
8+ out_idx=None,
9+ pass_configs={
10+ tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True,
11+ tilelang.PassConfigKey.TL_DISABLE_DATA_RACE_CHECK: True,
12+ tilelang.PassConfigKey.TL_ENABLE_AUTO_SCHEDULE: False,
13+ },
14+)
15+def _kv_index_cat_tl_ascend(
16+ dtype: str,
17+ window_size: int,
18+ topk_tokens: int,
19+ out_stride: int,
20+) -> None:
21+ total_q = T.dynamic('total_q')
22+ 
23+ if max(window_size, topk_tokens) <= 128:
24+ block_size = 128
25+ rows_per_task = 64
26+ num_stages = 2
27+ else:
28+ block_size = 6336
29+ rows_per_task = 5
30+ num_stages = 2
31+ vec_len = 64
32+ blocks_l = (window_size + block_size - 1) // block_size
33+ blocks_r = (topk_tokens + block_size - 1) // block_size
34+ num_cores = get_num_vector_cores()
35+ 
36+ @T.prim_func
37+ def _kv_index_cat(
38+ kv_index: T.Tensor((total_q, window_size), dtype), # type: ignore
39+ kv_ca_index: T.Tensor((total_q, topk_tokens), dtype), # type: ignore
40+ kv_index_out: T.StridedTensor((total_q, window_size), (out_stride, 1), dtype), # type: ignore
41+ kv_ca_index_out: T.StridedTensor((total_q, topk_tokens), (out_stride, 1), dtype), # type: ignore
42+ total_k_swa: T.int32,
43+ total_k: T.int32,
44+ ) -> None:
45+ with T.Kernel(num_cores) as pid:
46+ data_ub = T.alloc_shared((num_stages, rows_per_task, block_size), dtype)
47+ 
48+ for stage in T.serial(num_stages):
49+ T.ascend_set_flag('MTE3_MTE2', stage)
50+ 
51+ for bid_l in T.Persistent(
52+ [T.ceildiv(total_q, rows_per_task) * blocks_l],
53+ num_cores,
54+ pid,
55+ group_size=1,
56+ num_stages=0,
57+ ):
58+ row_l = (bid_l // blocks_l) * rows_per_task
59+ block_l = bid_l % blocks_l
60+ offset_l = block_l * block_size
61+ copy_rows_l = T.min(rows_per_task, total_q - row_l)
62+ copy_length_l = T.min(block_size, window_size - offset_l)
63+ slot_l = (bid_l // num_cores) % num_stages
64+ 
65+ T.ascend_wait_flag('MTE3_MTE2', slot_l)
66+ T.copy(
67+ kv_index[row_l : row_l + copy_rows_l, offset_l : offset_l + copy_length_l],
68+ data_ub[slot_l, :copy_rows_l, :copy_length_l],
69+ l2_cache_ctrl='NOTALLOC_KEEP',
70+ )
71+ T.ascend_set_flag('MTE2_MTE3', slot_l)
72+ T.ascend_wait_flag('MTE2_MTE3', slot_l)
73+ T.copy(
74+ data_ub[slot_l, :copy_rows_l, :copy_length_l],
75+ kv_index_out[row_l : row_l + copy_rows_l, offset_l : offset_l + copy_length_l],
76+ l2_cache_ctrl='NOTALLOC_KEEP',
77+ )
78+ T.ascend_set_flag('MTE3_MTE2', slot_l)
79+ 
80+ for stage in T.serial(num_stages):
81+ T.ascend_wait_flag('MTE3_MTE2', stage)
82+ T.ascend_set_flag('MTE3_MTE2', stage)
83+ 
84+ for bid_r in T.Persistent(
85+ [T.ceildiv(total_q, rows_per_task) * blocks_r],
86+ num_cores,
87+ pid,
88+ group_size=1,
89+ num_stages=0,
90+ ):
91+ row_r = (bid_r // blocks_r) * rows_per_task
92+ block_r = bid_r % blocks_r
93+ offset_r = block_r * block_size
94+ copy_rows_r = T.min(rows_per_task, total_q - row_r)
95+ copy_length_r = T.min(block_size, topk_tokens - offset_r)
96+ slot_r = (bid_r // num_cores) % num_stages
97+ 
98+ T.ascend_wait_flag('MTE3_MTE2', slot_r)
99+ T.copy(
100+ kv_ca_index[row_r : row_r + copy_rows_r, offset_r : offset_r + copy_length_r],
101+ data_ub[slot_r, :copy_rows_r, :copy_length_r],
102+ l2_cache_ctrl='NOTALLOC_KEEP',
103+ )
104+ T.ascend_set_flag('MTE2_V', slot_r)
105+ T.ascend_wait_flag('MTE2_V', slot_r)
106+ 
107+ with T.SimdVF():
108+ mask = T.simd.pset(32)
109+ for tile_row in T.serial(copy_rows_r):
110+ for v in T.serial(T.ceildiv(copy_length_r, vec_len)):
111+ value = T.simd.vld(data_ub[slot_r, tile_row, v * vec_len])
112+ value_added = T.simd.vadds(value, total_k + total_k_swa, mask)
113+ value_out = T.simd.vsel(value, value_added, T.simd.vcmps(value, -1, mask, 'eq'))
114+ T.simd.vsts(data_ub[slot_r, tile_row, v * vec_len], value_out, mask)
115+ 
116+ T.ascend_set_flag('V_MTE3', slot_r)
117+ T.ascend_wait_flag('V_MTE3', slot_r)
118+ T.copy(
119+ data_ub[slot_r, :copy_rows_r, :copy_length_r],
120+ kv_ca_index_out[row_r : row_r + copy_rows_r, offset_r : offset_r + copy_length_r],
121+ l2_cache_ctrl='NOTALLOC_KEEP',
122+ )
123+ T.ascend_set_flag('MTE3_MTE2', slot_r)
124+ 
125+ for stage in T.serial(num_stages):
126+ T.ascend_wait_flag('MTE3_MTE2', stage)
127+ 
128+ return _kv_index_cat
@@ -0,0 +1,136 @@
1+"""Public Ascend interface for mirror context-parallel metadata."""
2+ 
3+import torch
4+ 
5+from cann_ops_tilelang.config import require_ascend
6+ 
7+from .prepare_cp_mirror_kernel import _precompute_tl_ascend, _scatter_tl_ascend
8+ 
9+ 
10+def prepare_for_mirror_cp_tilelang(
11+ cu_seqlens_q: torch.Tensor,
12+ cu_seqlens_k: torch.Tensor,
13+ cp_size: int,
14+ cp_rank: int,
15+ compress_ratio: int,
16+ total_q: int,
17+ output_len: int,
18+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
19+ """
20+ TileLang Ascend implementation of prepare_for_mirror_cp.
21+ This function is sync-free: it does not call .item() or perform a
22+ device-to-host synchronization. The caller must provide pre-computed sizes.
23+ 
24+ Args:
25+ cu_seqlens_q: [batch_size + 1], contiguous int32 device tensor
26+ cu_seqlens_k: [batch_size + 1], contiguous int32 device tensor
27+ cp_size: context parallelism size
28+ cp_rank: current rank
29+ compress_ratio: KV compression ratio
30+ total_q: total number of query tokens (= cu_seqlens_q[-1], pre-synced to CPU)
31+ output_len: total output length for cp_rank (= sum of 2*partial_seqlen, pre-synced to CPU)
32+ 
33+ Returns:
34+ mirror_cp_index_tensor,
35+ mirror_cp_reverse_index_tensor,
36+ ks_tensor,
37+ ke_tensor,
38+ cu_seqlens_q_cp_tensor,
39+ cu_seqlens_k_cp_tensor
40+ """
41+ assert cu_seqlens_q.ndim == 1, 'cu_seqlens_q must be a 1D tensor'
42+ assert cu_seqlens_k.ndim == 1, 'cu_seqlens_k must be a 1D tensor'
43+ assert cu_seqlens_q.dtype == torch.int32, 'cu_seqlens_q must use int32'
44+ assert cu_seqlens_k.dtype == torch.int32, 'cu_seqlens_k must use int32'
45+ assert cu_seqlens_q.is_contiguous(), 'cu_seqlens_q must be contiguous'
46+ assert cu_seqlens_k.is_contiguous(), 'cu_seqlens_k must be contiguous'
47+ assert cu_seqlens_q.device == cu_seqlens_k.device, 'both inputs must be on the same device'
48+ assert cp_size > 0, 'cp_size must be positive'
49+ assert 0 <= cp_rank < cp_size, 'cp_rank must be in [0, cp_size)'
50+ assert compress_ratio > 0, 'compress_ratio must be positive'
51+ assert total_q >= 0, 'total_q must be non-negative'
52+ assert output_len >= 0, 'output_len must be non-negative'
53+ 
54+ require_ascend()
55+ assert cu_seqlens_q.device.type == 'npu', 'input tensors must be on an NPU device'
56+ device = cu_seqlens_q.device
57+ batch_size = cu_seqlens_q.numel() - 1
58+ assert batch_size >= 0, 'cu_seqlens_q must contain at least one element'
59+ assert cu_seqlens_k.numel() == batch_size + 1
60+ 
61+ if batch_size == 0 or output_len == 0 or total_q == 0:
62+ empty = torch.empty(0, dtype=torch.int32, device=device)
63+ z1 = torch.zeros(1, dtype=torch.int32, device=device)
64+ return (
65+ empty,
66+ torch.zeros(total_q, dtype=torch.int32, device=device),
67+ empty,
68+ empty,
69+ z1,
70+ z1,
71+ )
72+ 
73+ # ---- Phase 1: precompute per-batch quantities (NPU kernel, no sync) ---- #
74+ k_c4_lens = torch.empty(batch_size, dtype=torch.int32, device=device)
75+ partial_seqlens = torch.empty(batch_size, dtype=torch.int32, device=device)
76+ elems_per_batch = torch.empty(batch_size, dtype=torch.int32, device=device)
77+ delta_sq = torch.empty(batch_size, 4, dtype=torch.int32, device=device)
78+ delta_sk = torch.empty(batch_size, 4, dtype=torch.int32, device=device)
79+ 
80+ precompute_kern = _precompute_tl_ascend(cp_size=cp_size, cp_rank=cp_rank, compress_ratio=compress_ratio)
81+ precompute_kern(
82+ cu_seqlens_q,
83+ cu_seqlens_k,
84+ k_c4_lens,
85+ partial_seqlens,
86+ elems_per_batch,
87+ delta_sq,
88+ delta_sk,
89+ )
90+ 
91+ # ---- Phase 2: cumulative sums (NPU ops, no sync) ---- #
92+ cu_seqlens_k_c4 = torch.zeros(batch_size + 1, dtype=torch.int32, device=device)
93+ cu_seqlens_k_c4[1:] = torch.cumsum(k_c4_lens, dim=0)
94+ 
95+ cu_elems = torch.zeros(batch_size + 1, dtype=torch.int32, device=device)
96+ cu_elems[1:] = torch.cumsum(elems_per_batch, dim=0)
97+ 
98+ total_per_rank = cu_elems[batch_size : batch_size + 1].contiguous() # [1] tensor on NPU
99+ 
100+ # ---- Phase 3: main scatter kernel (NPU kernel, no sync) ---- #
101+ mirror_cp_reverse_index = torch.zeros(total_q, dtype=torch.int32, device=device)
102+ mirror_cp_index = torch.empty(output_len, dtype=torch.int32, device=device)
103+ ks_out = torch.empty(output_len, dtype=torch.int32, device=device)
104+ ke_out = torch.empty(output_len, dtype=torch.int32, device=device)
105+ 
106+ scatter_kern = _scatter_tl_ascend(cp_size=cp_size, cp_rank=cp_rank, compress_ratio=compress_ratio)
107+ scatter_kern(
108+ cu_seqlens_q,
109+ cu_seqlens_k,
110+ cu_seqlens_k_c4,
111+ partial_seqlens,
112+ cu_elems,
113+ total_per_rank,
114+ mirror_cp_reverse_index,
115+ mirror_cp_index,
116+ ks_out,
117+ ke_out,
118+ )
119+ 
120+ # ---- Phase 4: cu_seqlens_q_cp / cu_seqlens_k_cp (NPU ops, no sync) ---- #
121+ delta_sq_flat = delta_sq.reshape(-1)
122+ delta_sk_flat = delta_sk.reshape(-1)
123+ 
124+ cu_seqlens_q_cp = torch.zeros(batch_size * 4 + 1, dtype=torch.int32, device=device)
125+ cu_seqlens_k_cp = torch.zeros(batch_size * 4 + 1, dtype=torch.int32, device=device)
126+ cu_seqlens_q_cp[1:] = torch.cumsum(delta_sq_flat, dim=0)
127+ cu_seqlens_k_cp[1:] = torch.cumsum(delta_sk_flat, dim=0)
128+ 
129+ return (
130+ mirror_cp_index,
131+ mirror_cp_reverse_index,
132+ ks_out,
133+ ke_out,
134+ cu_seqlens_q_cp,
135+ cu_seqlens_k_cp,
136+ )
@@ -0,0 +1,265 @@
1+"""TileLang Ascend kernels for mirror context-parallel metadata."""
2+ 
3+import tilelang
4+from tilelang.ascend import language as T
5+ 
6+from cann_ops_tilelang.config import get_num_vector_cores
7+ 
8+ 
9+@tilelang.jit(
10+ pass_configs={
11+ tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True,
12+ tilelang.PassConfigKey.TL_DISABLE_DATA_RACE_CHECK: True,
13+ },
14+)
15+def _precompute_tl_ascend(cp_size: int, cp_rank: int, compress_ratio: int) -> None:
16+ """Ascend AIV implementation of the per-batch precomputation."""
17+ batch_size = T.dynamic('batch_size')
18+ num_cores = get_num_vector_cores()
19+ 
20+ @T.prim_func
21+ def _precompute(
22+ cu_seqlens_q: T.Tensor([batch_size + 1], 'int32'), # type: ignore
23+ cu_seqlens_k: T.Tensor([batch_size + 1], 'int32'), # type: ignore
24+ k_c4_lens: T.Tensor([batch_size], 'int32'), # type: ignore
25+ partial_seqlens: T.Tensor([batch_size], 'int32'), # type: ignore
26+ elems_per_batch: T.Tensor([batch_size], 'int32'), # type: ignore
27+ delta_sq: T.Tensor([batch_size, 4], 'int32'), # type: ignore
28+ delta_sk: T.Tensor([batch_size, 4], 'int32'), # type: ignore
29+ ) -> None:
30+ with T.Kernel(num_cores) as pid:
31+ for i in T.Persistent([batch_size], num_cores, pid, group_size=1):
32+ sq = T.int32(cu_seqlens_q[i + 1]) - T.int32(cu_seqlens_q[i])
33+ sk = T.int32(cu_seqlens_k[i + 1]) - T.int32(cu_seqlens_k[i])
34+ 
35+ k_c4_val = sk // compress_ratio
36+ k_c4_lens[i] = T.max(1, k_c4_val)
37+ 
38+ ps = (sq + cp_size * 2 - 1) // (cp_size * 2)
39+ partial_seqlens[i] = ps
40+ elems_per_batch[i] = 2 * ps
41+ 
42+ nv_h0 = T.min(ps, T.max(0, sq - ps * cp_rank))
43+ mirror_rank = cp_size * 2 - 1 - cp_rank
44+ nv_h1 = T.min(ps, T.max(0, sq - ps * mirror_rank))
45+ kl_h0 = sk - sq + ps * cp_rank + nv_h0
46+ kl_h1 = sk - sq + ps * mirror_rank + nv_h1
47+ 
48+ delta_sk[i, 0] = kl_h0 // compress_ratio
49+ delta_sq[i, 0] = kl_h0 // compress_ratio - (kl_h0 - nv_h0) // compress_ratio
50+ 
51+ delta_sk[i, 1] = -(kl_h0 // compress_ratio)
52+ delta_sq[i, 1] = -(kl_h0 // compress_ratio) + (kl_h0 + ps - nv_h0) // compress_ratio
53+ 
54+ delta_sk[i, 2] = kl_h1 // compress_ratio
55+ delta_sq[i, 2] = kl_h1 // compress_ratio - (kl_h1 - nv_h1) // compress_ratio
56+ 
57+ sk_arg3 = sq - ps * mirror_rank - nv_h1
58+ diff3 = sk_arg3 - (ps - nv_h1)
59+ td_sk3 = T.if_then_else(sk_arg3 >= 0, sk_arg3 // compress_ratio, -((-sk_arg3) // compress_ratio))
60+ td_diff3 = T.if_then_else(diff3 >= 0, diff3 // compress_ratio, -((-diff3) // compress_ratio))
61+ delta_sk[i, 3] = td_sk3
62+ delta_sq[i, 3] = td_sk3 - td_diff3
63+ 
64+ return _precompute
65+ 
66+ 
67+@tilelang.jit(
68+ pass_configs={
69+ tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True,
70+ tilelang.PassConfigKey.TL_DISABLE_DATA_RACE_CHECK: True,
71+ },
72+)
73+def _scatter_tl_ascend(cp_size: int, cp_rank: int, compress_ratio: int) -> None:
74+ """Ascend AIV scatter with bounded UB tiles and independent work domains."""
75+ batch_size = T.dynamic('batch_size')
76+ total_q = T.dynamic('total_q')
77+ output_len = T.dynamic('output_len')
78+ 
79+ block_size = 8192
80+ vec_len = 64
81+ num_cores = get_num_vector_cores()
82+ ratio_is_pow2 = (compress_ratio & (compress_ratio - 1)) == 0
83+ ratio_shift = compress_ratio.bit_length() - 1
84+ 
85+ @T.prim_func
86+ def _scatter(
87+ cu_seqlens_q: T.Tensor([batch_size + 1], 'int32'), # type: ignore
88+ cu_seqlens_k: T.Tensor([batch_size + 1], 'int32'), # type: ignore
89+ cu_seqlens_k_c4: T.Tensor([batch_size + 1], 'int32'), # type: ignore
90+ partial_seqlens: T.Tensor([batch_size], 'int32'), # type: ignore
91+ cu_elems: T.Tensor([batch_size + 1], 'int32'), # type: ignore
92+ total_per_rank: T.Tensor([1], 'int32'), # type: ignore
93+ mirror_cp_reverse_index: T.Tensor([total_q], 'int32'), # type: ignore
94+ mirror_cp_index: T.Tensor([output_len], 'int32'), # type: ignore
95+ ks_out: T.Tensor([output_len], 'int32'), # type: ignore
96+ ke_out: T.Tensor([output_len], 'int32'), # type: ignore
97+ ) -> None:
98+ with T.Kernel(num_cores) as pid:
99+ total_per_rank_value = T.int32(total_per_rank[0])
100+ reverse_ub = T.alloc_shared((block_size,), 'int32')
101+ ks_ub = T.alloc_shared((block_size,), 'int32')
102+ ke_ub = T.alloc_shared((block_size,), 'int32')
103+ 
104+ for i_rev0, rank_rev0 in T.Persistent([batch_size, cp_size], num_cores, pid, group_size=1):
105+ sq_rev0 = T.int32(cu_seqlens_q[i_rev0 + 1]) - T.int32(cu_seqlens_q[i_rev0])
106+ cu_q_rev0 = T.int32(cu_seqlens_q[i_rev0])
107+ cu_elem_rev0 = T.int32(cu_elems[i_rev0])
108+ ps_rev0 = T.int32(partial_seqlens[i_rev0])
109+ local_start_rev0 = ps_rev0 * rank_rev0
110+ valid_count_rev0 = T.min(ps_rev0, T.max(0, sq_rev0 - local_start_rev0))
111+ 
112+ for block_rev0 in T.serial(T.ceildiv(valid_count_rev0, block_size)):
113+ offset_rev0 = block_rev0 * block_size
114+ copy_length_rev0 = T.min(block_size, valid_count_rev0 - offset_rev0)
115+ with T.SimdVF():
116+ mask_rev0 = T.simd.pset(32)
117+ for v_rev0 in T.serial(T.ceildiv(copy_length_rev0, vec_len)):
118+ value_rev0 = T.simd.vci(
119+ rank_rev0 * total_per_rank_value + cu_elem_rev0 + offset_rev0 + v_rev0 * vec_len,
120+ 'int32',
121+ )
122+ T.simd.vsts(reverse_ub[v_rev0 * vec_len], value_rev0, mask_rev0)
123+ reverse_start_rev0 = cu_q_rev0 + local_start_rev0 + offset_rev0
124+ T.copy(
125+ reverse_ub[:copy_length_rev0],
126+ mirror_cp_reverse_index[reverse_start_rev0 : reverse_start_rev0 + copy_length_rev0],
127+ )
128+ 
129+ for i_rev1, rank_rev1 in T.Persistent([batch_size, cp_size], num_cores, pid, group_size=1):
130+ sq_rev1 = T.int32(cu_seqlens_q[i_rev1 + 1]) - T.int32(cu_seqlens_q[i_rev1])
131+ cu_q_rev1 = T.int32(cu_seqlens_q[i_rev1])
132+ cu_elem_rev1 = T.int32(cu_elems[i_rev1])
133+ ps_rev1 = T.int32(partial_seqlens[i_rev1])
134+ local_start_rev1 = ps_rev1 * (cp_size * 2 - 1 - rank_rev1)
135+ valid_count_rev1 = T.min(ps_rev1, T.max(0, sq_rev1 - local_start_rev1))
136+ 
137+ for block_rev1 in T.serial(T.ceildiv(valid_count_rev1, block_size)):
138+ offset_rev1 = block_rev1 * block_size
139+ copy_length_rev1 = T.min(block_size, valid_count_rev1 - offset_rev1)
140+ with T.SimdVF():
141+ mask_rev1 = T.simd.pset(32)
142+ for v_rev1 in T.serial(T.ceildiv(copy_length_rev1, vec_len)):
143+ value_rev1 = T.simd.vci(
144+ rank_rev1 * total_per_rank_value
145+ + cu_elem_rev1
146+ + ps_rev1
147+ + offset_rev1
148+ + v_rev1 * vec_len,
149+ 'int32',
150+ )
151+ T.simd.vsts(reverse_ub[v_rev1 * vec_len], value_rev1, mask_rev1)
152+ reverse_start_rev1 = cu_q_rev1 + local_start_rev1 + offset_rev1
153+ T.copy(
154+ reverse_ub[:copy_length_rev1],
155+ mirror_cp_reverse_index[reverse_start_rev1 : reverse_start_rev1 + copy_length_rev1],
156+ )
157+ 
158+ # Forward outputs include valid and padded positions for cp_rank.
159+ for i_fwd0 in T.Persistent([batch_size], num_cores, pid, group_size=1):
160+ sq_fwd0 = T.int32(cu_seqlens_q[i_fwd0 + 1]) - T.int32(cu_seqlens_q[i_fwd0])
161+ sk_fwd0 = T.int32(cu_seqlens_k[i_fwd0 + 1]) - T.int32(cu_seqlens_k[i_fwd0])
162+ cu_q_fwd0 = T.int32(cu_seqlens_q[i_fwd0])
163+ cu_k_c4_fwd0 = T.int32(cu_seqlens_k_c4[i_fwd0])
164+ ps_fwd0 = T.int32(partial_seqlens[i_fwd0])
165+ local_start_fwd0 = ps_fwd0 * cp_rank
166+ out_start_fwd0 = T.int32(cu_elems[i_fwd0])
167+ 
168+ for block_fwd0 in T.serial(T.ceildiv(ps_fwd0, block_size)):
169+ offset_fwd0 = block_fwd0 * block_size
170+ copy_length_fwd0 = T.min(block_size, ps_fwd0 - offset_fwd0)
171+ with T.SimdVF():
172+ mask_fwd0 = T.simd.pset(32)
173+ neg1_fwd0 = T.simd.vdup(-1, 'int32', mask_fwd0)
174+ ks_vec_fwd0 = T.simd.vdup(cu_k_c4_fwd0, 'int32', mask_fwd0)
175+ if compress_ratio > 1 and not ratio_is_pow2:
176+ ratio_vec_fwd0 = T.simd.vdup(compress_ratio, 'int32', mask_fwd0)
177+ for v_fwd0 in T.serial(T.ceildiv(copy_length_fwd0, vec_len)):
178+ local_idx_vec_fwd0 = T.simd.vci(local_start_fwd0 + offset_fwd0 + v_fwd0 * vec_len, 'int32')
179+ valid_fwd0 = T.simd.vcmps(local_idx_vec_fwd0, sq_fwd0, mask_fwd0, 'lt')
180+ reverse_vec_fwd0 = T.simd.vadds(local_idx_vec_fwd0, cu_q_fwd0, mask_fwd0)
181+ ke_numerator_fwd0 = T.simd.vadds(local_idx_vec_fwd0, sk_fwd0 - sq_fwd0 + 1, mask_fwd0)
182+ if compress_ratio == 1:
183+ ke_delta_fwd0 = ke_numerator_fwd0
184+ elif ratio_is_pow2:
185+ ke_delta_fwd0 = T.simd.vshrs(ke_numerator_fwd0, ratio_shift, mask_fwd0)
186+ else:
187+ ke_delta_fwd0 = T.simd.vdiv(ke_numerator_fwd0, ratio_vec_fwd0, mask_fwd0)
188+ ke_vec_fwd0 = T.simd.vadds(ke_delta_fwd0, cu_k_c4_fwd0, mask_fwd0)
189+ T.simd.vsts(
190+ reverse_ub[v_fwd0 * vec_len],
191+ T.simd.vsel(reverse_vec_fwd0, neg1_fwd0, valid_fwd0),
192+ mask_fwd0,
193+ )
194+ T.simd.vsts(ks_ub[v_fwd0 * vec_len], ks_vec_fwd0, mask_fwd0)
195+ T.simd.vsts(
196+ ke_ub[v_fwd0 * vec_len], T.simd.vsel(ke_vec_fwd0, ks_vec_fwd0, valid_fwd0), mask_fwd0
197+ )
198+ 
199+ T.copy(
200+ reverse_ub[:copy_length_fwd0],
201+ mirror_cp_index[out_start_fwd0 + offset_fwd0 : out_start_fwd0 + offset_fwd0 + copy_length_fwd0],
202+ )
203+ T.copy(
204+ ks_ub[:copy_length_fwd0],
205+ ks_out[out_start_fwd0 + offset_fwd0 : out_start_fwd0 + offset_fwd0 + copy_length_fwd0],
206+ )
207+ T.copy(
208+ ke_ub[:copy_length_fwd0],
209+ ke_out[out_start_fwd0 + offset_fwd0 : out_start_fwd0 + offset_fwd0 + copy_length_fwd0],
210+ )
211+ 
212+ for i_fwd1 in T.Persistent([batch_size], num_cores, pid, group_size=1):
213+ sq_fwd1 = T.int32(cu_seqlens_q[i_fwd1 + 1]) - T.int32(cu_seqlens_q[i_fwd1])
214+ sk_fwd1 = T.int32(cu_seqlens_k[i_fwd1 + 1]) - T.int32(cu_seqlens_k[i_fwd1])
215+ cu_q_fwd1 = T.int32(cu_seqlens_q[i_fwd1])
216+ cu_k_c4_fwd1 = T.int32(cu_seqlens_k_c4[i_fwd1])
217+ ps_fwd1 = T.int32(partial_seqlens[i_fwd1])
218+ local_start_fwd1 = ps_fwd1 * (cp_size * 2 - 1 - cp_rank)
219+ out_start_fwd1 = T.int32(cu_elems[i_fwd1]) + ps_fwd1
220+ 
221+ for block_fwd1 in T.serial(T.ceildiv(ps_fwd1, block_size)):
222+ offset_fwd1 = block_fwd1 * block_size
223+ copy_length_fwd1 = T.min(block_size, ps_fwd1 - offset_fwd1)
224+ with T.SimdVF():
225+ mask_fwd1 = T.simd.pset(32)
226+ neg1_fwd1 = T.simd.vdup(-1, 'int32', mask_fwd1)
227+ ks_vec_fwd1 = T.simd.vdup(cu_k_c4_fwd1, 'int32', mask_fwd1)
228+ if compress_ratio > 1 and not ratio_is_pow2:
229+ ratio_vec_fwd1 = T.simd.vdup(compress_ratio, 'int32', mask_fwd1)
230+ for v_fwd1 in T.serial(T.ceildiv(copy_length_fwd1, vec_len)):
231+ local_idx_vec_fwd1 = T.simd.vci(local_start_fwd1 + offset_fwd1 + v_fwd1 * vec_len, 'int32')
232+ valid_fwd1 = T.simd.vcmps(local_idx_vec_fwd1, sq_fwd1, mask_fwd1, 'lt')
233+ reverse_vec_fwd1 = T.simd.vadds(local_idx_vec_fwd1, cu_q_fwd1, mask_fwd1)
234+ ke_numerator_fwd1 = T.simd.vadds(local_idx_vec_fwd1, sk_fwd1 - sq_fwd1 + 1, mask_fwd1)
235+ if compress_ratio == 1:
236+ ke_delta_fwd1 = ke_numerator_fwd1
237+ elif ratio_is_pow2:
238+ ke_delta_fwd1 = T.simd.vshrs(ke_numerator_fwd1, ratio_shift, mask_fwd1)
239+ else:
240+ ke_delta_fwd1 = T.simd.vdiv(ke_numerator_fwd1, ratio_vec_fwd1, mask_fwd1)
241+ ke_vec_fwd1 = T.simd.vadds(ke_delta_fwd1, cu_k_c4_fwd1, mask_fwd1)
242+ T.simd.vsts(
243+ reverse_ub[v_fwd1 * vec_len],
244+ T.simd.vsel(reverse_vec_fwd1, neg1_fwd1, valid_fwd1),
245+ mask_fwd1,
246+ )
247+ T.simd.vsts(ks_ub[v_fwd1 * vec_len], ks_vec_fwd1, mask_fwd1)
248+ T.simd.vsts(
249+ ke_ub[v_fwd1 * vec_len], T.simd.vsel(ke_vec_fwd1, ks_vec_fwd1, valid_fwd1), mask_fwd1
250+ )
251+ 
252+ T.copy(
253+ reverse_ub[:copy_length_fwd1],
254+ mirror_cp_index[out_start_fwd1 + offset_fwd1 : out_start_fwd1 + offset_fwd1 + copy_length_fwd1],
255+ )
256+ T.copy(
257+ ks_ub[:copy_length_fwd1],
258+ ks_out[out_start_fwd1 + offset_fwd1 : out_start_fwd1 + offset_fwd1 + copy_length_fwd1],
259+ )
260+ T.copy(
261+ ke_ub[:copy_length_fwd1],
262+ ke_out[out_start_fwd1 + offset_fwd1 : out_start_fwd1 + offset_fwd1 + copy_length_fwd1],
263+ )
264+ 
265+ return _scatter
@@ -0,0 +1,63 @@
1+"""Public Ascend interface for LLaMA decode metadata."""
2+ 
3+import torch
4+ 
5+from cann_ops_tilelang.config import get_num_vector_cores, require_ascend
6+ 
7+from .prepare_llama_decode_metadata_kernel import _prepare_llama_decode_metadata_tl_ascend
8+ 
9+ 
10+def prepare_llama_decode_metadata(
11+ seqlens_q: torch.Tensor,
12+ seqlens_k: torch.Tensor,
13+ cu_seqlens_q: torch.Tensor,
14+ block_tables_in: torch.Tensor,
15+ block_tables_offsets: torch.Tensor,
16+ is_placeholder: torch.Tensor,
17+ positions: torch.Tensor,
18+ valid_token_mask: torch.Tensor,
19+ gate_force_random: torch.Tensor,
20+ fix_routing_mask: torch.Tensor,
21+ context_lens: torch.Tensor,
22+ slot_mapping_store: torch.Tensor,
23+ query_indices: torch.Tensor,
24+ block_tables_out: torch.Tensor,
25+ *,
26+ max_seqlen_q: int,
27+ max_num_blocks: int,
28+ block_size: int,
29+ force_load_balance: bool = False,
30+) -> None:
31+ """Compute LLaMA decode metadata on the active accelerator via TileLang.
32+ 
33+ Standard (non-compressed, non-SWA) attention: each query token maps to a slot in
34+ a paged KV cache addressed by its block table. Supports variable-length seqlen_q.
35+ block_tables_in is compact 1D with block_tables_offsets for variable-length per-seq
36+ block tables; block_tables_out is the per-token expanded [total_q, max_num_blocks] form.
37+ """
38+ require_ascend()
39+ n_seq = seqlens_q.shape[0]
40+ num_cores = get_num_vector_cores()
41+ long_block_table_threshold = 512
42+ balance_sequences = num_cores < n_seq <= 64 or block_tables_in.numel() >= n_seq * long_block_table_threshold
43+ average_num_blocks = block_tables_in.numel() // max(n_seq, 1)
44+ use_double_buffer = n_seq >= 4 * num_cores or (n_seq > 2 * num_cores and average_num_blocks >= 128)
45+ kernel = _prepare_llama_decode_metadata_tl_ascend(
46+ max_seqlen_q, max_num_blocks, block_size, force_load_balance, balance_sequences, 2 if use_double_buffer else 1
47+ )
48+ kernel(
49+ seqlens_q,
50+ seqlens_k,
51+ cu_seqlens_q,
52+ block_tables_in,
53+ block_tables_offsets,
54+ is_placeholder,
55+ positions,
56+ valid_token_mask,
57+ gate_force_random,
58+ fix_routing_mask,
59+ context_lens,
60+ slot_mapping_store,
61+ query_indices,
62+ block_tables_out,
63+ )
@@ -0,0 +1,108 @@
1+"""TileLang Ascend kernel for LLaMA decode metadata."""
2+ 
3+import tilelang
4+from tilelang.ascend import language as T
5+ 
6+from cann_ops_tilelang.config import get_num_vector_cores
7+ 
8+ 
9+@tilelang.jit(
10+ out_idx=None,
11+ target='ascend',
12+ pass_configs={
13+ tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True,
14+ tilelang.PassConfigKey.TL_DISABLE_DATA_RACE_CHECK: True,
15+ },
16+)
17+def _prepare_llama_decode_metadata_tl_ascend(
18+ max_seqlen_q: int,
19+ max_num_blocks: int,
20+ block_size: int,
21+ force_load_balance: bool,
22+ balance_sequences: bool = False,
23+ num_stages: int = 1,
24+) -> None:
25+ n_seq = T.dynamic('n_seq')
26+ total_q = T.dynamic('total_q')
27+ total_blocks = T.dynamic('total_blocks')
28+ num_cores = get_num_vector_cores()
29+ 
30+ @T.prim_func
31+ def _prepare_llama_decode_metadata(
32+ seqlens_q: T.Tensor((n_seq,), 'int32'), # type: ignore
33+ seqlens_k: T.Tensor((n_seq,), 'int32'), # type: ignore
34+ cu_seqlens_q: T.Tensor((n_seq,), 'int32'), # type: ignore
35+ block_tables_in: T.Tensor((total_blocks,), 'int32'), # type: ignore
36+ block_tables_offsets: T.Tensor((n_seq + 1,), 'int32'), # type: ignore
37+ is_placeholder: T.Tensor((n_seq,), T.bool), # type: ignore
38+ positions: T.Tensor((total_q,), 'int32'), # type: ignore
39+ valid_token_mask: T.Tensor((total_q,), T.bool), # type: ignore
40+ gate_force_random: T.Tensor((total_q,), T.bool), # type: ignore
41+ fix_routing_mask: T.Tensor((total_q,), T.bool), # type: ignore
42+ context_lens: T.Tensor((total_q,), 'int32'), # type: ignore
43+ slot_mapping_store: T.Tensor((total_q,), 'int32'), # type: ignore
44+ query_indices: T.Tensor((total_q,), 'int32'), # type: ignore
45+ block_tables_out: T.Tensor((total_q, max_num_blocks), 'int32'), # type: ignore
46+ ):
47+ with T.Kernel(num_cores) as pid:
48+ block_table_ub = T.alloc_shared((max_num_blocks,), 'int32')
49+ if balance_sequences:
50+ base_sequences = T.floordiv(n_seq, num_cores)
51+ remainder = T.floormod(n_seq, num_cores)
52+ seq_begin = pid * base_sequences + T.min(pid, remainder)
53+ seq_end = seq_begin + base_sequences + T.if_then_else(pid < remainder, 1, 0)
54+ else:
55+ sequences_per_core = T.ceildiv(n_seq, num_cores)
56+ seq_begin = pid * sequences_per_core
57+ seq_end = T.min(seq_begin + sequences_per_core, n_seq)
58+ for seq_idx in T.Pipelined(
59+ seq_begin,
60+ seq_end,
61+ num_stages=num_stages,
62+ annotations={'multi_buffer_eligible': [block_table_ub]},
63+ ):
64+ sq = seqlens_q[seq_idx]
65+ sk = seqlens_k[seq_idx]
66+ is_pad = is_placeholder[seq_idx]
67+ q_base = T.if_then_else(seq_idx == 0, 0, cu_seqlens_q[seq_idx - 1])
68+ bt_offset = block_tables_offsets[seq_idx]
69+ num_blocks = block_tables_offsets[seq_idx + 1] - bt_offset
70+ 
71+ T.assume(sq > 0)
72+ T.assume(sq <= max_seqlen_q)
73+ T.assume(q_base >= 0)
74+ T.assume(q_base + sq <= total_q)
75+ T.assume(num_blocks > 0)
76+ T.assume(num_blocks <= max_num_blocks)
77+ T.assume(bt_offset >= 0)
78+ T.assume(bt_offset + num_blocks <= total_blocks)
79+ 
80+ # Cache each sequence's compact block table once, then reuse it for
81+ # both slot lookup and all per-token expanded block-table rows.
82+ T.copy(
83+ block_tables_in[bt_offset : bt_offset + num_blocks],
84+ block_table_ub[:num_blocks],
85+ )
86+ 
87+ with T.SimtVF(threads=max_seqlen_q):
88+ j = T.get_thread_binding()
89+ if j < sq:
90+ pos = sk - sq + j
91+ q_off = q_base + j
92+ block_number = block_table_ub[pos // block_size]
93+ 
94+ positions[q_off] = pos
95+ valid_token_mask[q_off] = T.if_then_else(is_pad, False, True)
96+ gate_force_random[q_off] = force_load_balance
97+ fix_routing_mask[q_off] = False
98+ context_lens[q_off] = pos + 1
99+ query_indices[q_off] = T.if_then_else(is_pad, 998244353, seq_idx)
100+ slot_mapping_store[q_off] = block_number * block_size + pos % block_size
101+ 
102+ for j in T.serial(sq):
103+ T.copy(
104+ block_table_ub[:num_blocks],
105+ block_tables_out[q_base + j, :num_blocks],
106+ )
107+ 
108+ return _prepare_llama_decode_metadata
@@ -0,0 +1,93 @@
1+"""Public Ascend interface for V33 decode metadata."""
2+ 
3+import torch
4+ 
5+from cann_ops_tilelang.config import get_num_vector_cores, require_ascend
6+ 
7+from .prepare_v33_decode_metadata_kernel import _prepare_v33_decode_metadata_tl_ascend
8+ 
9+ 
10+def prepare_v33_decode_metadata(
11+ seqlens_q: torch.Tensor,
12+ seqlens_k: torch.Tensor,
13+ cu_seqlens_q: torch.Tensor,
14+ block_tables_in: torch.Tensor,
15+ block_tables_offsets: torch.Tensor,
16+ state_block_idx_in: torch.Tensor,
17+ is_placeholder: torch.Tensor,
18+ positions: torch.Tensor,
19+ valid_token_mask: torch.Tensor,
20+ gate_force_random: torch.Tensor,
21+ fix_routing_mask: torch.Tensor,
22+ context_lens: torch.Tensor,
23+ klen: torch.Tensor,
24+ kv_ca_index_offset_len_4: torch.Tensor,
25+ kv_ca_index_offset_len_128: torch.Tensor,
26+ kv_swa_index_offset_len: torch.Tensor,
27+ slot_mapping_store_swa: torch.Tensor,
28+ query_indices: torch.Tensor,
29+ state_block_idx_out: torch.Tensor,
30+ block_tables_out: torch.Tensor,
31+ positions_c4: torch.Tensor,
32+ slot_mapping_store_ca4: torch.Tensor,
33+ positions_c128: torch.Tensor,
34+ slot_mapping_store_ca128: torch.Tensor,
35+ *,
36+ max_seqlen_q: int,
37+ max_num_blocks: int,
38+ window_size: int,
39+ swa_block_size: int,
40+ block_size: int,
41+ force_load_balance: bool = False,
42+) -> None:
43+ """Compute V33 decode metadata on the active accelerator via TileLang.
44+ 
45+ Supports positive variable-length seqlen_q. q_offsets is computed internally from seqlens_q.
46+ block_tables_in is compact 1D with block_tables_offsets for variable-length per-seq block tables.
47+ """
48+ require_ascend()
49+ if block_size < 128 or block_size % 128 != 0:
50+ raise ValueError(f'block_size must be a positive multiple of 128, got {block_size}')
51+ num_sequences = seqlens_q.shape[0]
52+ if num_sequences == 0:
53+ return
54+ num_vec_cores = get_num_vector_cores()
55+ average_num_blocks = block_tables_in.numel() // max(num_sequences, 1)
56+ use_double_buffer = num_sequences >= 4 * num_vec_cores or (
57+ num_sequences > 2 * num_vec_cores and average_num_blocks >= 128
58+ )
59+ kernel = _prepare_v33_decode_metadata_tl_ascend(
60+ max_seqlen_q,
61+ max_num_blocks,
62+ window_size,
63+ swa_block_size,
64+ block_size,
65+ force_load_balance,
66+ 2 if use_double_buffer else 1,
67+ )
68+ kernel(
69+ seqlens_q,
70+ seqlens_k,
71+ cu_seqlens_q,
72+ block_tables_in,
73+ block_tables_offsets,
74+ state_block_idx_in,
75+ is_placeholder,
76+ positions,
77+ valid_token_mask,
78+ gate_force_random,
79+ fix_routing_mask,
80+ context_lens,
81+ klen,
82+ kv_ca_index_offset_len_4,
83+ kv_ca_index_offset_len_128,
84+ kv_swa_index_offset_len,
85+ slot_mapping_store_swa,
86+ query_indices,
87+ state_block_idx_out,
88+ block_tables_out,
89+ positions_c4,
90+ slot_mapping_store_ca4,
91+ positions_c128,
92+ slot_mapping_store_ca128,
93+ )
@@ -0,0 +1,153 @@
1+"""TileLang Ascend kernel for V33 decode metadata."""
2+ 
3+import tilelang
4+from tilelang.ascend import language as T
5+ 
6+from cann_ops_tilelang.config import get_num_vector_cores
7+ 
8+ 
9+@tilelang.jit(
10+ out_idx=None,
11+ target='ascend',
12+ pass_configs={
13+ tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True,
14+ tilelang.PassConfigKey.TL_DISABLE_DATA_RACE_CHECK: True,
15+ },
16+)
17+def _prepare_v33_decode_metadata_tl_ascend(
18+ max_seqlen_q: int,
19+ max_num_blocks: int,
20+ window_size: int,
21+ swa_block_size: int,
22+ block_size: int,
23+ force_load_balance: bool,
24+ num_stages: int = 1,
25+) -> None:
26+ n_seq = T.dynamic('n_seq')
27+ total_q = T.dynamic('total_q')
28+ total_blocks = T.dynamic('total_blocks')
29+ c4_cbs = block_size // 4
30+ c4_tpb = block_size // 4
31+ c128_cbs = block_size // 128
32+ c128_tpb = block_size // 128
33+ num_cores = get_num_vector_cores()
34+ 
35+ @T.prim_func
36+ def _prepare_v33_decode_metadata(
37+ seqlens_q: T.Tensor((n_seq,), 'int32'), # type: ignore
38+ seqlens_k: T.Tensor((n_seq,), 'int32'), # type: ignore
39+ cu_seqlens_q: T.Tensor((n_seq,), 'int32'), # type: ignore
40+ block_tables_in: T.Tensor((total_blocks,), 'int32'), # type: ignore
41+ block_tables_offsets: T.Tensor((n_seq + 1,), 'int32'), # type: ignore
42+ state_block_idx_in: T.Tensor((n_seq,), 'int32'), # type: ignore
43+ is_placeholder: T.Tensor((n_seq,), T.bool), # type: ignore
44+ positions: T.Tensor((total_q,), 'int32'), # type: ignore
45+ valid_token_mask: T.Tensor((total_q,), T.bool), # type: ignore
46+ gate_force_random: T.Tensor((total_q,), T.bool), # type: ignore
47+ fix_routing_mask: T.Tensor((total_q,), T.bool), # type: ignore
48+ context_lens: T.Tensor((total_q,), 'int32'), # type: ignore
49+ klen: T.Tensor((total_q,), 'int32'), # type: ignore
50+ kv_ca_index_offset_len_4: T.Tensor((total_q, 2), 'int32'), # type: ignore
51+ kv_ca_index_offset_len_128: T.Tensor((total_q, 2), 'int32'), # type: ignore
52+ kv_swa_index_offset_len: T.Tensor((total_q, 2), 'int32'), # type: ignore
53+ slot_mapping_store_swa: T.Tensor((total_q,), 'int32'), # type: ignore
54+ query_indices: T.Tensor((total_q,), 'int32'), # type: ignore
55+ state_block_idx_out: T.Tensor((total_q,), 'int32'), # type: ignore
56+ block_tables_out: T.Tensor((total_q, max_num_blocks), 'int32'), # type: ignore
57+ positions_c4: T.Tensor((total_q,), 'int32'), # type: ignore
58+ slot_mapping_store_ca4: T.Tensor((total_q,), 'int32'), # type: ignore
59+ positions_c128: T.Tensor((total_q,), 'int32'), # type: ignore
60+ slot_mapping_store_ca128: T.Tensor((total_q,), 'int32'), # type: ignore
61+ ):
62+ with T.Kernel(T.min(n_seq, num_cores)) as pid:
63+ block_table_ub = T.alloc_shared((max_num_blocks,), 'int32')
64+ sequences_per_core = T.ceildiv(n_seq, num_cores)
65+ seq_begin = pid * sequences_per_core
66+ seq_end = T.min(seq_begin + sequences_per_core, n_seq)
67+ for seq_idx in T.Pipelined(
68+ seq_begin,
69+ seq_end,
70+ num_stages=num_stages,
71+ annotations={'multi_buffer_eligible': [block_table_ub]},
72+ ):
73+ sq = seqlens_q[seq_idx]
74+ sk = seqlens_k[seq_idx]
75+ q_base = T.if_then_else(seq_idx == 0, 0, cu_seqlens_q[seq_idx - 1])
76+ state_id = state_block_idx_in[seq_idx]
77+ is_pad = is_placeholder[seq_idx]
78+ bt_offset = block_tables_offsets[seq_idx]
79+ num_blocks = block_tables_offsets[seq_idx + 1] - bt_offset
80+ 
81+ if num_stages > 1:
82+ T.assume(sq > 0)
83+ T.assume(sq <= max_seqlen_q)
84+ T.assume(q_base >= 0)
85+ T.assume(q_base + sq <= total_q)
86+ T.assume(num_blocks > 0)
87+ T.assume(num_blocks <= max_num_blocks)
88+ T.assume(bt_offset >= 0)
89+ T.assume(bt_offset + num_blocks <= total_blocks)
90+ 
91+ # Load the compact table once per sequence. CA lookups and all
92+ # expanded per-token rows reuse the same UB-resident data.
93+ T.copy(
94+ block_tables_in[bt_offset : bt_offset + num_blocks],
95+ block_table_ub[:num_blocks],
96+ )
97+ 
98+ with T.SimtVF(threads=max_seqlen_q):
99+ j = T.get_thread_binding()
100+ if j < sq:
101+ pos = sk - sq + j
102+ q_off = q_base + j
103+ 
104+ positions[q_off] = pos
105+ valid_token_mask[q_off] = T.if_then_else(is_pad, False, True)
106+ gate_force_random[q_off] = force_load_balance
107+ fix_routing_mask[q_off] = False
108+ context_lens[q_off] = pos + 1
109+ klen[q_off] = (pos + 1) // 4
110+ 
111+ kv_ca_index_offset_len_4[q_off, 0] = q_off * max_num_blocks * c4_tpb
112+ kv_ca_index_offset_len_4[q_off, 1] = (pos + 1) // 4
113+ kv_ca_index_offset_len_128[q_off, 0] = q_off * max_num_blocks * c128_tpb
114+ kv_ca_index_offset_len_128[q_off, 1] = (pos + 1) // 128
115+ 
116+ swa_len = T.min(pos + 1, window_size)
117+ window_start_pos = pos - swa_len + 1
118+ kv_swa_index_offset_len[q_off, 0] = (
119+ state_id * swa_block_size + window_start_pos % swa_block_size
120+ )
121+ kv_swa_index_offset_len[q_off, 1] = swa_len
122+ slot_mapping_store_swa[q_off] = state_id * swa_block_size + pos % swa_block_size
123+ 
124+ query_indices[q_off] = T.if_then_else(is_pad, 998244353, seq_idx)
125+ state_block_idx_out[q_off] = state_id
126+ 
127+ c4_idx = pos // 4
128+ if (pos + 1) % 4 == 0:
129+ positions_c4[q_off] = c4_idx * 4
130+ bt_idx = c4_idx // c4_cbs
131+ block_number = block_table_ub[bt_idx]
132+ slot_mapping_store_ca4[q_off] = block_number * c4_cbs + c4_idx % c4_cbs
133+ else:
134+ positions_c4[q_off] = 0
135+ slot_mapping_store_ca4[q_off] = -1
136+ 
137+ c128_idx = pos // 128
138+ if (pos + 1) % 128 == 0:
139+ positions_c128[q_off] = c128_idx * 128
140+ bt_idx = c128_idx // c128_cbs
141+ block_number = block_table_ub[bt_idx]
142+ slot_mapping_store_ca128[q_off] = block_number * c128_cbs + c128_idx % c128_cbs
143+ else:
144+ positions_c128[q_off] = 0
145+ slot_mapping_store_ca128[q_off] = -1
146+ 
147+ for j in T.serial(sq):
148+ T.copy(
149+ block_table_ub[:num_blocks],
150+ block_tables_out[q_base + j, :num_blocks],
151+ )
152+ 
153+ return _prepare_v33_decode_metadata
@@ -0,0 +1,147 @@
1+"""Public Ascend interface for rotary embeddings."""
2+ 
3+from __future__ import annotations
4+ 
5+import torch
6+ 
7+from cann_ops_tilelang.config import require_ascend
8+ 
9+from .rope_kernel import _rotary_embedding_tl_ascend
10+ 
11+ 
12+def rotary_embedding(
13+ positions: torch.Tensor, # [seqlen]
14+ query: torch.Tensor, # [seqlen, num_heads * head_size] or [seqlen, num_heads, head_size]
15+ key: torch.Tensor | None, # [seqlen, num_kv_heads * head_size] or [seqlen, num_kv_heads, head_size]
16+ query_out: torch.Tensor | None, # The same shape as `query`
17+ key_out: torch.Tensor | None, # The same shape as `key`
18+ head_size: int,
19+ cos_sin_cache: torch.Tensor, # [seqlen_ro, rot_dim]
20+ is_neox: bool,
21+ conjugate: bool = False,
22+) -> None:
23+ require_ascend()
24+ assert positions.ndim == 1
25+ assert query.ndim == 2 or query.ndim == 3
26+ if key is not None:
27+ assert key.ndim == 2 or key.ndim == 3
28+ if query_out is not None:
29+ assert query_out.shape == query.shape
30+ if key_out is not None:
31+ assert key is not None and key_out.shape == key.shape
32+ assert cos_sin_cache.ndim == 2
33+ 
34+ assert head_size > 0
35+ assert positions.is_contiguous(), 'positions must be contiguous'
36+ assert positions.dtype in (torch.int32, torch.int64), 'positions must use int32 or int64'
37+ assert query.dtype in (torch.bfloat16, torch.float32), 'query must use bfloat16 or float32'
38+ assert cos_sin_cache.dtype == torch.float32, 'cos_sin_cache must use float32'
39+ assert cos_sin_cache.stride(-1) == 1, 'cos_sin_cache must be contiguous in its last dimension'
40+ assert positions.device == query.device == cos_sin_cache.device, 'inputs must be on the same device'
41+ 
42+ seqlen = positions.shape[0]
43+ _, rot_dim = cos_sin_cache.shape
44+ assert rot_dim > 0 and rot_dim % 2 == 0, 'rotary_dim must be a positive even number'
45+ assert rot_dim <= head_size, 'rotary_dim must not exceed head_size'
46+ 
47+ assert query.shape[0] == seqlen
48+ if key is not None:
49+ assert key.shape[0] == seqlen
50+ 
51+ if query.ndim == 2:
52+ assert query.shape[1] % head_size == 0
53+ num_heads = query.shape[1] // head_size
54+ query = query.view(seqlen, num_heads, head_size)
55+ if query_out is not None:
56+ query_out = query_out.view(seqlen, num_heads, head_size)
57+ else:
58+ assert query.shape[2] == head_size
59+ num_heads = query.shape[1]
60+ if key is not None:
61+ if key.ndim == 2:
62+ assert key.shape[1] % head_size == 0
63+ num_kv_heads = key.shape[1] // head_size
64+ key = key.view(seqlen, num_kv_heads, head_size)
65+ if key_out is not None:
66+ key_out = key_out.view(seqlen, num_kv_heads, head_size)
67+ else:
68+ assert key.shape[2] == head_size
69+ num_kv_heads = key.shape[1]
70+ 
71+ positions_dtype = 'int32'
72+ if positions is not None:
73+ if positions.dtype == torch.int32:
74+ positions_dtype = 'int32'
75+ elif positions.dtype == torch.int64:
76+ positions_dtype = 'int64'
77+ else:
78+ raise AssertionError('Unsupported dtype')
79+ 
80+ dtype_str = ''
81+ if query.dtype == torch.bfloat16:
82+ dtype_str = 'bfloat16'
83+ elif query.dtype == torch.float:
84+ dtype_str = 'float32'
85+ else:
86+ raise AssertionError('Unsupported dtype')
87+ if key is not None:
88+ assert key.dtype == query.dtype and key.device == query.device
89+ if query_out is not None:
90+ assert query_out.dtype == query.dtype, 'query_out must have the same dtype as query'
91+ assert query_out.device == query.device
92+ if key_out is not None:
93+ assert key_out.dtype == key.dtype and key_out.device == key.device
94+ for tensor in (query, key, query_out, key_out):
95+ assert tensor is None or tensor.stride(-1) == 1
96+ 
97+ assert query.device.type == 'npu', 'inputs must be NPU tensors'
98+ 
99+ cos = cos_sin_cache[:, : rot_dim // 2]
100+ sin = cos_sin_cache[:, rot_dim // 2 :]
101+ assert cos.stride(0) == sin.stride(0)
102+ query_rope_kernel = _rotary_embedding_tl_ascend(
103+ dtype_str,
104+ positions_dtype,
105+ num_heads,
106+ head_size,
107+ rot_dim,
108+ True,
109+ not is_neox,
110+ conjugate,
111+ cos_sin_cache.stride(0),
112+ query.stride(0),
113+ query.stride(1),
114+ query.stride(0) if query_out is None else query_out.stride(0),
115+ query.stride(1) if query_out is None else query_out.stride(1),
116+ )
117+ query_rope_kernel(
118+ query,
119+ cos_sin_cache,
120+ positions,
121+ query if query_out is None else query_out,
122+ 0,
123+ )
124+ 
125+ if key is not None:
126+ key_rope_kernel = _rotary_embedding_tl_ascend(
127+ dtype_str,
128+ positions_dtype,
129+ num_kv_heads,
130+ head_size,
131+ rot_dim,
132+ True,
133+ not is_neox,
134+ conjugate,
135+ cos_sin_cache.stride(0),
136+ key.stride(0),
137+ key.stride(1),
138+ key.stride(0) if key_out is None else key_out.stride(0),
139+ key.stride(1) if key_out is None else key_out.stride(1),
140+ )
141+ key_rope_kernel(
142+ key,
143+ cos_sin_cache,
144+ positions,
145+ key if key_out is None else key_out,
146+ 0,
147+ )
@@ -0,0 +1,136 @@
1+"""TileLang Ascend kernel for rotary embeddings."""
2+ 
3+import tilelang
4+from tilelang.ascend import language as T
5+ 
6+from cann_ops_tilelang.config import get_num_vector_cores
7+ 
8+ 
9+@tilelang.jit(
10+ out_idx=None,
11+ target='ascend',
12+ pass_configs={
13+ tilelang.PassConfigKey.TIR_DISABLE_VECTORIZE: True,
14+ tilelang.PassConfigKey.TL_DISABLE_DATA_RACE_CHECK: True,
15+ },
16+)
17+def _rotary_embedding_tl_ascend(
18+ dtype: str,
19+ positions_dtype: str,
20+ nheads: int,
21+ head_size: int,
22+ rotary_dim: int,
23+ has_position_tensor: bool,
24+ interleaved: bool,
25+ conjugate: bool,
26+ cos_sin_stride: int,
27+ x_stride_0: int,
28+ x_stride_1: int,
29+ y_stride_0: int,
30+ y_stride_1: int,
31+) -> None:
32+ seqlen = T.dynamic('seqlen')
33+ seqlen_ro = T.dynamic('seqlen_ro')
34+ half_d = rotary_dim // 2
35+ dtype_bytes = 4 if dtype == 'float32' else 2
36+ supported_simd_dtype = dtype in ('bfloat16', 'float32')
37+ simd_half_aligned = half_d * dtype_bytes % 32 == 0
38+ use_simd_fast_path = supported_simd_dtype and half_d <= 64 and not interleaved and simd_half_aligned
39+ simd_mask_pattern = 'PAT_ALL' if half_d >= 64 else f'PAT_VL{half_d}'
40+ if use_simd_fast_path:
41+ min_padded_head_size = max(head_size, half_d + 64)
42+ ub_align_elements = 32 // dtype_bytes
43+ padded_head_size = (min_padded_head_size + ub_align_elements - 1) // ub_align_elements * ub_align_elements
44+ else:
45+ padded_head_size = head_size
46+ padded_half_d = (half_d + 63) // 64 * 64 if use_simd_fast_path else half_d
47+ tile_elements_per_head = padded_head_size
48+ max_heads_per_tile = 64 if use_simd_fast_path else 32
49+ heads_per_tile = max(1, min(nheads, max_heads_per_tile, 32 * 1024 // (tile_elements_per_head * dtype_bytes)))
50+ while nheads % heads_per_tile != 0:
51+ heads_per_tile -= 1
52+ head_tiles = nheads // heads_per_tile
53+ threads = 256 if heads_per_tile * half_d >= 4096 else 128
54+ num_cores = get_num_vector_cores()
55+ num_stages = 4 if use_simd_fast_path else 1
56+ 
57+ @T.macro
58+ def rotate_simd_block(x0_buffer, x1_buffer, cos_sin_buffer, x0_offset, x1_offset, cos_offset, mask):
59+ cos_value = T.simd.vld(cos_sin_buffer[cos_offset])
60+ sin_value = T.simd.vld(cos_sin_buffer[half_d + cos_offset])
61+ for head in T.unroll(heads_per_tile, explicit=True):
62+ if dtype == 'bfloat16':
63+ x0 = T.simd.vcvt(T.simd.vld(x0_buffer[head, x0_offset], dist='UNPK_B16'), T.float32, mask, part=0)
64+ x1 = T.simd.vcvt(T.simd.vld(x1_buffer[head, x1_offset], dist='UNPK_B16'), T.float32, mask, part=0)
65+ else:
66+ x0 = T.simd.vld(x0_buffer[head, x0_offset])
67+ x1 = T.simd.vld(x1_buffer[head, x1_offset])
68+ if conjugate:
69+ x0_result = T.simd.vadd(T.simd.vmul(x0, cos_value, mask), T.simd.vmul(x1, sin_value, mask), mask)
70+ x1_result = T.simd.vsub(T.simd.vmul(x1, cos_value, mask), T.simd.vmul(x0, sin_value, mask), mask)
71+ else:
72+ x0_result = T.simd.vsub(T.simd.vmul(x0, cos_value, mask), T.simd.vmul(x1, sin_value, mask), mask)
73+ x1_result = T.simd.vadd(T.simd.vmul(x0, sin_value, mask), T.simd.vmul(x1, cos_value, mask), mask)
74+ if dtype == 'bfloat16':
75+ T.simd.vsts(x0_buffer[head, x0_offset], T.simd.vcvt(x0_result, T.bfloat16, mask), mask, 'PK_B32')
76+ T.simd.vsts(x1_buffer[head, x1_offset], T.simd.vcvt(x1_result, T.bfloat16, mask), mask, 'PK_B32')
77+ else:
78+ T.simd.vsts(x0_buffer[head, x0_offset], x0_result, mask)
79+ T.simd.vsts(x1_buffer[head, x1_offset], x1_result, mask)
80+ 
81+ @T.prim_func
82+ def _rotary_embedding_ascend(
83+ x: T.StridedTensor((seqlen, nheads, head_size), (x_stride_0, x_stride_1, 1), dtype), # type: ignore
84+ cos_sin: T.StridedTensor((seqlen_ro, rotary_dim), (cos_sin_stride, 1), 'float32'), # type: ignore
85+ positions: T.Tensor((seqlen,), positions_dtype), # type: ignore
86+ y: T.StridedTensor((seqlen, nheads, head_size), (y_stride_0, y_stride_1, 1), dtype), # type: ignore
87+ seqlen_offsets: T.int32,
88+ ) -> None:
89+ with T.Kernel(num_cores) as core_id:
90+ x_ub = T.alloc_shared((heads_per_tile, padded_head_size), dtype)
91+ cos_sin_ub = T.alloc_shared((padded_half_d * 2,), 'float32')
92+ T.annotate_buffer_versions({x_ub: num_stages, cos_sin_ub: num_stages})
93+ 
94+ for task_id in T.Persistent(
95+ [seqlen * head_tiles],
96+ num_cores,
97+ core_id,
98+ group_size=1,
99+ num_stages=num_stages,
100+ annotations={'enable_offset': True},
101+ ):
102+ token_id = task_id // head_tiles
103+ head_begin = task_id % head_tiles * heads_per_tile
104+ position = T.alloc_var(positions_dtype, seqlen_offsets)
105+ if has_position_tensor:
106+ position += positions[token_id]
107+ T.copy(x[token_id, head_begin : head_begin + heads_per_tile, :head_size], x_ub[:, :head_size])
108+ T.copy(cos_sin[position, :rotary_dim], cos_sin_ub[:rotary_dim])
109+ 
110+ if use_simd_fast_path:
111+ with T.SimdVF():
112+ mask = T.simd.pset(32, simd_mask_pattern)
113+ rotate_simd_block(x_ub, x_ub, cos_sin_ub, 0, half_d, 0, mask)
114+ else:
115+ with T.SimtVF(threads=threads):
116+ for head, lane in T.Parallel(heads_per_tile, half_d):
117+ if interleaved:
118+ x0_col = lane * 2
119+ x1_col = lane * 2 + 1
120+ else:
121+ x0_col = lane
122+ x1_col = half_d + lane
123+ x0 = T.cast(x_ub[head, x0_col], 'float32')
124+ x1 = T.cast(x_ub[head, x1_col], 'float32')
125+ cos_value = cos_sin_ub[lane]
126+ sin_value = cos_sin_ub[half_d + lane]
127+ if conjugate:
128+ x_ub[head, x0_col] = T.cast(x0 * cos_value + x1 * sin_value, dtype)
129+ x_ub[head, x1_col] = T.cast(x1 * cos_value - x0 * sin_value, dtype)
130+ else:
131+ x_ub[head, x0_col] = T.cast(x0 * cos_value - x1 * sin_value, dtype)
132+ x_ub[head, x1_col] = T.cast(x0 * sin_value + x1 * cos_value, dtype)
133+ 
134+ T.copy(x_ub[:, :head_size], y[token_id, head_begin : head_begin + heads_per_tile, :head_size])
135+ 
136+ return _rotary_embedding_ascend
@@ -0,0 +1,69 @@
1+import torch
2+ 
3+from cann_ops_tilelang.attention.state_cache_kernel import _load_state_to_cache_tl_ascend, _select_state_slice_tl_ascend
4+from cann_ops_tilelang.config import require_ascend
5+ 
6+ 
7+def select_state_slice(state_tensor: torch.Tensor, state_cache: torch.Tensor, last_hit_position: int) -> None:
8+ require_ascend()
9+ _validate_state_tensors(state_tensor, state_cache, last_hit_position)
10+ num_layers, block_size_kvstore, feature_dim = state_cache.shape
11+ _, block_size_inference, _ = state_tensor.shape
12+ contiguous_tokens = (
13+ feature_dim > 1 and state_tensor.stride(1) == feature_dim and (state_cache.stride(1) == feature_dim)
14+ )
15+ kernel = _select_state_slice_tl_ascend(
16+ block_size_kvstore, block_size_inference, num_layers, feature_dim, contiguous_tokens
17+ )
18+ return kernel(state_tensor, state_cache, last_hit_position)
19+ 
20+ 
21+def load_state_to_cache(state_tensor: torch.Tensor, state_cache: torch.Tensor, last_hit_position: int) -> None:
22+ require_ascend()
23+ _validate_state_tensors(state_tensor, state_cache, last_hit_position)
24+ return _load_state_to_cache_impl(state_tensor, state_cache, last_hit_position)
25+ 
26+ 
27+def _validate_state_tensors(
28+ state_tensor: torch.Tensor,
29+ state_cache: torch.Tensor,
30+ last_hit_position: int,
31+) -> None:
32+ if state_tensor.ndim != 3 or state_cache.ndim != 3:
33+ raise ValueError('state_tensor and state_cache must both be 3D tensors')
34+ if state_tensor.dtype != torch.uint8 or state_cache.dtype != torch.uint8:
35+ raise TypeError('state_tensor and state_cache must both have dtype torch.uint8')
36+ if state_tensor.device != state_cache.device:
37+ raise ValueError('state_tensor and state_cache must be on the same device')
38+ if state_tensor.device.type != 'npu':
39+ raise ValueError(f'state tensors must be on an NPU, got {state_tensor.device}.')
40+ num_layers, block_size_kvstore, feature_dim = state_cache.shape
41+ tensor_num_layers, block_size_inference, tensor_feature_dim = state_tensor.shape
42+ if num_layers <= 0 or block_size_kvstore <= 0 or block_size_inference <= 0 or (feature_dim <= 0):
43+ raise ValueError('all state tensor dimensions must be positive')
44+ if tensor_num_layers != num_layers or tensor_feature_dim != feature_dim:
45+ raise ValueError('state_tensor and state_cache must have matching layer and feature dimensions')
46+ for name, tensor in (('state_tensor', state_tensor), ('state_cache', state_cache)):
47+ if tensor.stride(2) != 1:
48+ raise ValueError(f'{name} must be contiguous in the feature dimension')
49+ if tensor.stride(1) < feature_dim or tensor.stride(0) < tensor.shape[1] * tensor.stride(1):
50+ raise ValueError(f'{name} must have a non-overlapping row-major layout')
51+ if not isinstance(last_hit_position, int) or not 0 <= last_hit_position <= torch.iinfo(torch.int32).max:
52+ raise ValueError('last_hit_position must be a non-negative int32 value')
53+ return None
54+ 
55+ 
56+def _load_state_to_cache_impl(
57+ state_tensor: torch.Tensor,
58+ state_cache: torch.Tensor,
59+ last_hit_position: int,
60+) -> None:
61+ num_layers, block_size_kvstore, feature_dim = state_cache.shape
62+ _, block_size_inference, _ = state_tensor.shape
63+ contiguous_tokens = (
64+ feature_dim > 1 and state_tensor.stride(1) == feature_dim and state_cache.stride(1) == feature_dim
65+ )
66+ kernel = _load_state_to_cache_tl_ascend(
67+ block_size_kvstore, block_size_inference, num_layers, feature_dim, contiguous_tokens
68+ )
69+ return kernel(state_tensor, state_cache, last_hit_position)