Merged
feat(engram): migrate remaining operators #8
feat(engram): migrate remaining operators #8
Merged
wangliweiAdd on Sep 17
Total: 19 files changed+1384-341
@@ -12,7 +12,7 @@ src/cann_ops_tilelang/engram/engram_fused_weight*.py tests/engram/test_engram_fu
12src/cann_ops_tilelang/engram/engram_gate*.py tests/engram/test_engram_gate_fwd.py tests/engram/test_engram_gate_bwd.py12src/cann_ops_tilelang/engram/engram_gate*.py tests/engram/test_engram_gate_fwd.py tests/engram/test_engram_gate_bwd.py
13src/cann_ops_tilelang/engram/engram_grad_w_reduce*.py tests/engram/test_engram_grad_w_reduce.py13src/cann_ops_tilelang/engram/engram_grad_w_reduce*.py tests/engram/test_engram_grad_w_reduce.py
14src/cann_ops_tilelang/engram/engram_hash*.py tests/engram/test_engram_hash.py14src/cann_ops_tilelang/engram/engram_hash*.py tests/engram/test_engram_hash.py
15-src/cann_ops_tilelang/engram/engram_sinkhorn_step*.py tests/engram/test_engram_sinkhorn_step.py15+src/cann_ops_tilelang/engram/engram_sinkhorn*.py tests/engram/test_engram_sinkhorn.py
16 16 
17# MHC17# MHC
18src/cann_ops_tilelang/mhc/expand*.py tests/mhc/test_expand.py18src/cann_ops_tilelang/mhc/expand*.py tests/mhc/test_expand.py
@@ -5,11 +5,23 @@ __all__ = [
5 'engram_gate_bwd',5 'engram_gate_bwd',
6 'engram_gate_fwd',6 'engram_gate_fwd',
7 'engram_grad_w_reduce',7 'engram_grad_w_reduce',
8+ 'engram_hash',
9+ 'engram_sinkhorn_finalize',
10+ 'engram_sinkhorn_momentum_update',
8 'engram_sinkhorn_step',11 'engram_sinkhorn_step',
9 'moe_topk_gate',12 'moe_topk_gate',
10 'normalize_weight',13 'normalize_weight',
11 'topk_gate',14 'topk_gate',
12]15]
13 16 
14-from .engram import engram_fused_weight, engram_gate_bwd, engram_gate_fwd, engram_grad_w_reduce, engram_sinkhorn_step17+from .engram import (
18+ engram_fused_weight,
19+ engram_gate_bwd,
20+ engram_gate_fwd,
21+ engram_grad_w_reduce,
22+ engram_hash,
23+ engram_sinkhorn_finalize,
24+ engram_sinkhorn_momentum_update,
25+ engram_sinkhorn_step,
26+)
15from .moe import moe_topk_gate, normalize_weight, topk_gate27from .moe import moe_topk_gate, normalize_weight, topk_gate
@@ -5,10 +5,14 @@ __all__ = [
5 'engram_gate_bwd',5 'engram_gate_bwd',
6 'engram_gate_fwd',6 'engram_gate_fwd',
7 'engram_grad_w_reduce',7 'engram_grad_w_reduce',
8+ 'engram_hash',
9+ 'engram_sinkhorn_finalize',
10+ 'engram_sinkhorn_momentum_update',
8 'engram_sinkhorn_step',11 'engram_sinkhorn_step',
9]12]
10 13 
11from .engram_fused_weight import engram_fused_weight14from .engram_fused_weight import engram_fused_weight
12from .engram_gate import engram_gate_bwd, engram_gate_fwd15from .engram_gate import engram_gate_bwd, engram_gate_fwd
13from .engram_grad_w_reduce import engram_grad_w_reduce16from .engram_grad_w_reduce import engram_grad_w_reduce
14-from .engram_sinkhorn_step import engram_sinkhorn_step17+from .engram_hash import engram_hash
18+from .engram_sinkhorn import engram_sinkhorn_finalize, engram_sinkhorn_momentum_update, engram_sinkhorn_step
@@ -0,0 +1,110 @@
1+"""Public interface for the Engram n-gram hash operator."""
2+ 
3+from __future__ import annotations
4+ 
5+import math
6+ 
7+import torch
8+ 
9+from cann_ops_tilelang.config import get_num_vector_cores, require_ascend, should_print_kernel_source
10+ 
11+from .engram_hash_kernel import get_engram_hash_kernel
12+ 
13+ 
14+def _check_npu_tensor(
15+ name: str,
16+ tensor: torch.Tensor,
17+ *,
18+ dtype: torch.dtype,
19+ device: torch.device | None = None,
20+) -> None:
21+ if not isinstance(tensor, torch.Tensor):
22+ raise TypeError(f'{name} must be a torch.Tensor.')
23+ if tensor.device.type != 'npu':
24+ raise ValueError(f'{name} must be an NPU tensor, got {tensor.device}.')
25+ if device is not None and tensor.device != device:
26+ raise ValueError(f'{name} must be on {device}, got {tensor.device}.')
27+ if tensor.dtype != dtype:
28+ raise TypeError(f'{name} must use {dtype}, got {tensor.dtype}.')
29+ if not tensor.is_contiguous():
30+ raise ValueError(f'{name} must be contiguous.')
31+ 
32+ 
33+def engram_hash(
34+ ngram_token_ids: torch.Tensor,
35+ multipliers: torch.Tensor,
36+ vocab_sizes: torch.Tensor,
37+ offsets: torch.Tensor,
38+) -> torch.Tensor:
39+ """Compute Engram n-gram hash embedding indices on Ascend.
40+ 
41+ ``ngram_token_ids`` has shape ``(num_tokens, max_ngram_size)`` and uses
42+ INT32. ``multipliers`` uses INT64, while ``vocab_sizes`` and ``offsets``
43+ use INT32. The caller must provide strictly positive vocabulary sizes.
44+ """
45+ 
46+ if not all(isinstance(tensor, torch.Tensor) for tensor in (ngram_token_ids, multipliers, vocab_sizes, offsets)):
47+ raise TypeError('ngram_token_ids, multipliers, vocab_sizes and offsets must be torch.Tensor instances.')
48+ 
49+ require_ascend()
50+ _check_npu_tensor('ngram_token_ids', ngram_token_ids, dtype=torch.int32)
51+ _check_npu_tensor('multipliers', multipliers, dtype=torch.int64, device=ngram_token_ids.device)
52+ _check_npu_tensor('vocab_sizes', vocab_sizes, dtype=torch.int32, device=ngram_token_ids.device)
53+ _check_npu_tensor('offsets', offsets, dtype=torch.int32, device=ngram_token_ids.device)
54+ 
55+ if ngram_token_ids.ndim != 2:
56+ raise ValueError('ngram_token_ids must have shape (num_tokens, max_ngram_size).')
57+ num_tokens, max_ngram_size = ngram_token_ids.shape
58+ if max_ngram_size < 2:
59+ raise ValueError(f'max_ngram_size must be at least 2, got {max_ngram_size}.')
60+ if vocab_sizes.ndim != 3:
61+ raise ValueError(
62+ 'vocab_sizes must have shape (num_ngram_layers, max_ngram_size - 1, num_embed_table_per_ngram).'
63+ )
64+ num_ngram_layers, vocab_ngram_size, num_embed_table_per_ngram = vocab_sizes.shape
65+ if num_ngram_layers == 0:
66+ raise ValueError('num_ngram_layers must be positive.')
67+ if vocab_ngram_size != max_ngram_size - 1:
68+ raise ValueError(f'vocab_sizes dimension 1 must be {max_ngram_size - 1}, got {vocab_ngram_size}.')
69+ if num_embed_table_per_ngram == 0:
70+ raise ValueError('num_embed_table_per_ngram must be positive.')
71+ if multipliers.shape != (num_ngram_layers, max_ngram_size):
72+ raise ValueError(
73+ f'multipliers must have shape {(num_ngram_layers, max_ngram_size)}, got {tuple(multipliers.shape)}.'
74+ )
75+ 
76+ num_out_cols = (max_ngram_size - 1) * num_embed_table_per_ngram
77+ if offsets.shape != (num_ngram_layers, num_out_cols):
78+ raise ValueError(f'offsets must have shape {(num_ngram_layers, num_out_cols)}, got {tuple(offsets.shape)}.')
79+ token_alignment = math.lcm(
80+ 8 // math.gcd(max_ngram_size, 8),
81+ 8 // math.gcd(num_out_cols, 8),
82+ )
83+ if 1024 // num_out_cols < token_alignment:
84+ raise ValueError(
85+ 'the n-gram configuration exceeds the 1024-thread Ascend SimtVF limit: '
86+ f'max_ngram_size={max_ngram_size}, num_out_cols={num_out_cols}.'
87+ )
88+ 
89+ output = torch.empty(
90+ (num_ngram_layers, num_tokens, num_out_cols),
91+ dtype=torch.int32,
92+ device=ngram_token_ids.device,
93+ )
94+ if num_tokens == 0:
95+ return output
96+ 
97+ available_cores = get_num_vector_cores()
98+ if num_ngram_layers > available_cores:
99+ raise ValueError(
100+ f'num_ngram_layers must not exceed the Ascend vector core count {available_cores}, got {num_ngram_layers}.'
101+ )
102+ kernel = get_engram_hash_kernel(
103+ max_ngram_size,
104+ num_ngram_layers,
105+ num_embed_table_per_ngram,
106+ )
107+ if should_print_kernel_source():
108+ print(kernel.get_kernel_source())
109+ kernel(ngram_token_ids, multipliers, vocab_sizes, offsets, output)
110+ return output
@@ -0,0 +1,107 @@
1+"""TileLang Ascend kernel for Engram n-gram hashing."""
2+ 
3+import math
4+ 
5+import tilelang
6+from tilelang import language as T
7+ 
8+from cann_ops_tilelang.config import get_num_vector_cores
9+ 
10+ 
11+@tilelang.jit
12+def get_engram_hash_kernel(
13+ max_ngram_size: int = 3,
14+ num_ngram_layers: int = 2,
15+ num_embed_table_per_ngram: int = 8,
16+):
17+ """Build the Engram hash kernel for Ascend vector cores."""
18+ 
19+ num_tokens = T.dynamic('num_tokens')
20+ available_cores = get_num_vector_cores()
21+ cores_per_layer = available_cores // num_ngram_layers
22+ num_out_cols = (max_ngram_size - 1) * num_embed_table_per_ngram
23+ max_simt_threads = 1024
24+ input_token_alignment = 8 // math.gcd(max_ngram_size, 8)
25+ output_token_alignment = 8 // math.gcd(num_out_cols, 8)
26+ token_alignment = math.lcm(input_token_alignment, output_token_alignment)
27+ max_tile_tokens = max_simt_threads // num_out_cols
28+ assert max_tile_tokens >= token_alignment
29+ tile_tokens = max_tile_tokens // token_alignment * token_alignment
30+ simt_threads = tile_tokens * num_out_cols
31+ num_cores = cores_per_layer * num_ngram_layers
32+ 
33+ @T.prim_func
34+ def engram_hash_kernel(
35+ ngram_token_ids: T.Tensor[(num_tokens, max_ngram_size), T.int32],
36+ multipliers: T.Tensor[(num_ngram_layers, max_ngram_size), T.int64],
37+ vocab_sizes: T.Tensor[
38+ (num_ngram_layers, max_ngram_size - 1, num_embed_table_per_ngram),
39+ T.int32,
40+ ],
41+ offsets: T.Tensor[(num_ngram_layers, num_out_cols), T.int32],
42+ output: T.Tensor[(num_ngram_layers, num_tokens, num_out_cols), T.int32],
43+ ):
44+ with T.Kernel(num_cores) as core_id:
45+ layer_idx = core_id // cores_per_layer
46+ layer_core_id = core_id % cores_per_layer
47+ token_ids_ub = T.alloc_shared((tile_tokens, max_ngram_size), T.int32)
48+ multipliers_ub = T.alloc_shared((max_ngram_size,), T.int64)
49+ vocab_sizes_ub = T.alloc_shared(
50+ (max_ngram_size - 1, num_embed_table_per_ngram),
51+ T.int32,
52+ )
53+ offsets_ub = T.alloc_shared((num_out_cols,), T.int32)
54+ output_ub = T.alloc_shared((tile_tokens, num_out_cols), T.int32)
55+ 
56+ T.copy(multipliers[layer_idx, :], multipliers_ub)
57+ T.copy(vocab_sizes[layer_idx, :, :], vocab_sizes_ub)
58+ T.copy(offsets[layer_idx, :], offsets_ub)
59+ 
60+ for tile_idx in T.Persistent(
61+ [T.ceildiv(num_tokens, tile_tokens)],
62+ cores_per_layer,
63+ layer_core_id,
64+ group_size=1,
65+ num_stages=0,
66+ ):
67+ token_start = tile_idx * tile_tokens
68+ valid_tokens = T.min(tile_tokens, num_tokens - token_start)
69+ T.copy(
70+ ngram_token_ids[token_start : token_start + valid_tokens, :],
71+ token_ids_ub[:valid_tokens, :],
72+ )
73+ 
74+ with T.SimtVF(threads=simt_threads):
75+ thread_idx = T.get_thread_binding()
76+ token_in_tile = thread_idx // num_out_cols
77+ out_col = thread_idx % num_out_cols
78+ if token_in_tile < valid_tokens:
79+ last_ngram_idx = out_col // num_embed_table_per_ngram + 1
80+ out_table_idx = out_col % num_embed_table_per_ngram
81+ first_token_id = T.cast(token_ids_ub[token_in_tile, 0], T.int64)
82+ first_product = first_token_id * multipliers_ub[0]
83+ hash_value = T.alloc_var(T.int64, init=first_product)
84+ for hash_ngram_idx in T.serial(1, max_ngram_size):
85+ if hash_ngram_idx <= last_ngram_idx:
86+ token_id = T.cast(token_ids_ub[token_in_tile, hash_ngram_idx], T.int64)
87+ product = token_id * multipliers_ub[hash_ngram_idx]
88+ hash_value = T.bitwise_xor(hash_value, product)
89+ 
90+ vocab_size = T.cast(
91+ vocab_sizes_ub[last_ngram_idx - 1, out_table_idx],
92+ T.int64,
93+ )
94+ remainder = T.truncmod(hash_value, vocab_size)
95+ hash_index = T.Select(
96+ remainder < 0,
97+ remainder + vocab_size,
98+ remainder,
99+ )
100+ output_ub[token_in_tile, out_col] = T.cast(hash_index, T.int32) + offsets_ub[out_col]
101+ 
102+ T.copy(
103+ output_ub[:valid_tokens, :],
104+ output[layer_idx, token_start : token_start + valid_tokens, :],
105+ )
106+ 
107+ return engram_hash_kernel
Rsrc/cann_ops_tilelang/engram/engram_sinkhorn_step.py→src/cann_ops_tilelang/engram/engram_sinkhorn.py+179-49
@@ -1,4 +1,4 @@
1-"""Public interface for one Engram Sinkhorn normalization step."""1+"""Public interfaces for the Engram Sinkhorn operator family."""
2 2 
3from __future__ import annotations3from __future__ import annotations
4 4 
@@ -6,19 +6,27 @@ import math
6from numbers import Real6from numbers import Real
7 7 
8import torch8import torch
9+from tilelang import language as T
9 10 
10from cann_ops_tilelang.config import get_num_vector_cores, require_ascend, should_print_kernel_source11from cann_ops_tilelang.config import get_num_vector_cores, require_ascend, should_print_kernel_source
11 12 
12-from .engram_sinkhorn_step_kernel import get_engram_sinkhorn_step_kernel13+from .engram_sinkhorn_kernel import (
14+ get_engram_sinkhorn_finalize_kernel,
15+ get_engram_sinkhorn_momentum_update_kernel,
16+ get_engram_sinkhorn_step_kernel,
17+)
13 18 
14-# SimtVF reserves 32 KiB of the physical 248 KiB UB for thread context.19+_PHYSICAL_UB_CAPACITY_BYTES = 248 * 1024
15-_SHARED_UB_CAPACITY_BYTES = (248 - 32) * 102420+# SimtVF reserves 32 KiB of the physical UB for thread context.
21+_SIMT_SHARED_UB_CAPACITY_BYTES = (248 - 32) * 1024
22+_MOMENTUM_TILE_SIZE = 4096
16 23 
17 24 
18-def _check_npu_fp32_tensor(25+def _check_npu_tensor(
19 name: str,26 name: str,
20 tensor: torch.Tensor,27 tensor: torch.Tensor,
21 *,28 *,
29+ dtypes: tuple[torch.dtype, ...],
22 device: torch.device | None = None,30 device: torch.device | None = None,
23) -> None:31) -> None:
24 if not isinstance(tensor, torch.Tensor):32 if not isinstance(tensor, torch.Tensor):
@@ -27,12 +35,23 @@ def _check_npu_fp32_tensor(
27 raise ValueError(f'{name} must be an NPU tensor, got {tensor.device}.')35 raise ValueError(f'{name} must be an NPU tensor, got {tensor.device}.')
28 if device is not None and tensor.device != device:36 if device is not None and tensor.device != device:
29 raise ValueError(f'{name} must be on {device}, got {tensor.device}.')37 raise ValueError(f'{name} must be on {device}, got {tensor.device}.')
30- if tensor.dtype != torch.float32:38+ if tensor.dtype not in dtypes:
31- raise TypeError(f'{name} must use torch.float32, got {tensor.dtype}.')39+ expected = ' or '.join(str(dtype) for dtype in dtypes)
40+ raise TypeError(f'{name} must use {expected}, got {tensor.dtype}.')
32 if not tensor.is_contiguous():41 if not tensor.is_contiguous():
33 raise ValueError(f'{name} must be contiguous.')42 raise ValueError(f'{name} must be contiguous.')
34 43 
35 44 
45+def _check_real(name: str, value: float, *, non_negative: bool = False) -> float:
46+ if not isinstance(value, Real):
47+ raise TypeError(f'{name} must be a real number.')
48+ value = float(value)
49+ if not math.isfinite(value) or (non_negative and value < 0):
50+ requirement = 'finite and non-negative' if non_negative else 'finite'
51+ raise ValueError(f'{name} must be {requirement}.')
52+ return value
53+ 
54+ 
36def _shares_storage(left: torch.Tensor, right: torch.Tensor) -> bool:55def _shares_storage(left: torch.Tensor, right: torch.Tensor) -> bool:
37 return (56 return (
38 left.numel() > 057 left.numel() > 0
@@ -41,41 +60,98 @@ def _shares_storage(left: torch.Tensor, right: torch.Tensor) -> bool:
41 )60 )
42 61 
43 62 
44-def _validate_inputs(63+def _reject_aliases(inputs: dict[str, torch.Tensor]) -> None:
45- matrix: torch.Tensor,
46- row_scale: torch.Tensor,
47- col_scale: torch.Tensor,
48- eps: float,
49-) -> tuple[int, int]:
50- if not all(isinstance(tensor, torch.Tensor) for tensor in (matrix, row_scale, col_scale)):
51- raise TypeError('matrix, row_scale and col_scale must be torch.Tensor instances.')
52- if not isinstance(eps, Real):
53- raise TypeError('eps must be a real number.')
54- if not math.isfinite(float(eps)) or eps < 0:
55- raise ValueError('eps must be finite and non-negative.')
56- 
57- require_ascend()
58- _check_npu_fp32_tensor('matrix', matrix)
59- _check_npu_fp32_tensor('row_scale', row_scale, device=matrix.device)
60- _check_npu_fp32_tensor('col_scale', col_scale, device=matrix.device)
61- 
62- if matrix.ndim != 2:
63- raise ValueError('matrix must have shape (num_rows, head_dim).')
64- num_rows, head_dim = matrix.shape
65- if head_dim == 0 or head_dim % 256:
66- raise ValueError(f'head_dim must be a positive multiple of 256, got {head_dim}.')
67- if row_scale.ndim != 1 or row_scale.shape[0] != num_rows:
68- raise ValueError(f'row_scale must have shape ({num_rows},), got {tuple(row_scale.shape)}.')
69- if col_scale.ndim != 1 or col_scale.shape[0] != head_dim:
70- raise ValueError(f'col_scale must have shape ({head_dim},), got {tuple(col_scale.shape)}.')
71- 
72- inputs = {'matrix': matrix, 'row_scale': row_scale, 'col_scale': col_scale}
73 input_items = list(inputs.items())64 input_items = list(inputs.items())
74 for index, (left_name, left) in enumerate(input_items):65 for index, (left_name, left) in enumerate(input_items):
75 for right_name, right in input_items[index + 1 :]:66 for right_name, right in input_items[index + 1 :]:
76 if _shares_storage(left, right):67 if _shares_storage(left, right):
77 raise ValueError(f'{left_name} and {right_name} must not share storage.')68 raise ValueError(f'{left_name} and {right_name} must not share storage.')
78- return num_rows, head_dim69+ 
70+ 
71+def _validate_matrix_inputs(
72+ matrix: torch.Tensor,
73+ row_scale: torch.Tensor,
74+ col_scale: torch.Tensor,
75+ eps: float,
76+ *,
77+ head_dim_alignment: int,
78+) -> tuple[int, int, float]:
79+ if not all(isinstance(tensor, torch.Tensor) for tensor in (matrix, row_scale, col_scale)):
80+ raise TypeError('matrix, row_scale and col_scale must be torch.Tensor instances.')
81+ eps = _check_real('eps', eps, non_negative=True)
82+ 
83+ require_ascend()
84+ fp32 = (torch.float32,)
85+ _check_npu_tensor('matrix', matrix, dtypes=fp32)
86+ _check_npu_tensor('row_scale', row_scale, dtypes=fp32, device=matrix.device)
87+ _check_npu_tensor('col_scale', col_scale, dtypes=fp32, device=matrix.device)
88+ 
89+ if matrix.ndim != 2:
90+ raise ValueError('matrix must have shape (num_rows, head_dim).')
91+ num_rows, head_dim = matrix.shape
92+ if head_dim == 0 or head_dim % head_dim_alignment:
93+ raise ValueError(f'head_dim must be a positive multiple of {head_dim_alignment}, got {head_dim}.')
94+ if row_scale.ndim != 1 or row_scale.shape[0] != num_rows:
95+ raise ValueError(f'row_scale must have shape ({num_rows},), got {tuple(row_scale.shape)}.')
96+ if col_scale.ndim != 1 or col_scale.shape[0] != head_dim:
97+ raise ValueError(f'col_scale must have shape ({head_dim},), got {tuple(col_scale.shape)}.')
98+ 
99+ _reject_aliases({'matrix': matrix, 'row_scale': row_scale, 'col_scale': col_scale})
100+ return num_rows, head_dim, eps
101+ 
102+ 
103+def engram_sinkhorn_momentum_update(
104+ grad: torch.Tensor,
105+ momentum_buffer: torch.Tensor,
106+ beta1: float,
107+ nesterov: bool = True,
108+) -> torch.Tensor:
109+ """Update Engram momentum in place and return the FP32 Sinkhorn input.
110+ 
111+ ``grad`` and ``momentum_buffer`` must be contiguous one-dimensional NPU
112+ tensors with equal shape. Each tensor may use BF16 or FP32, independently,
113+ and its length must be a multiple of 256. Computation is performed in FP32;
114+ the updated momentum is converted back to ``momentum_buffer.dtype``.
115+ """
116+ 
117+ if not isinstance(grad, torch.Tensor) or not isinstance(momentum_buffer, torch.Tensor):
118+ raise TypeError('grad and momentum_buffer must be torch.Tensor instances.')
119+ beta1 = _check_real('beta1', beta1)
120+ if not isinstance(nesterov, bool):
121+ raise TypeError('nesterov must be a bool.')
122+ 
123+ require_ascend()
124+ supported_dtypes = (torch.bfloat16, torch.float32)
125+ _check_npu_tensor('grad', grad, dtypes=supported_dtypes)
126+ _check_npu_tensor('momentum_buffer', momentum_buffer, dtypes=supported_dtypes, device=grad.device)
127+ if grad.ndim != 1 or momentum_buffer.ndim != 1:
128+ raise ValueError('grad and momentum_buffer must be one-dimensional.')
129+ if grad.shape != momentum_buffer.shape:
130+ raise ValueError(
131+ f'grad and momentum_buffer must have the same shape, got {tuple(grad.shape)} and '
132+ f'{tuple(momentum_buffer.shape)}.'
133+ )
134+ if grad.numel() % 256:
135+ raise ValueError(f'grad length must be a multiple of 256, got {grad.numel()}.')
136+ _reject_aliases({'grad': grad, 'momentum_buffer': momentum_buffer})
137+ 
138+ sinkhorn_input = torch.empty_like(momentum_buffer, dtype=torch.float32)
139+ if grad.numel() == 0:
140+ return sinkhorn_input
141+ 
142+ num_tiles = (grad.numel() + _MOMENTUM_TILE_SIZE - 1) // _MOMENTUM_TILE_SIZE
143+ num_cores = min(get_num_vector_cores(), num_tiles)
144+ kernel = get_engram_sinkhorn_momentum_update_kernel(
145+ T.dtype(grad.dtype),
146+ T.dtype(momentum_buffer.dtype),
147+ nesterov,
148+ beta1,
149+ num_cores,
150+ )
151+ if should_print_kernel_source():
152+ print(kernel.get_kernel_source())
153+ kernel(grad, momentum_buffer, sinkhorn_input)
154+ return sinkhorn_input
79 155 
80 156 
81def engram_sinkhorn_step(157def engram_sinkhorn_step(
@@ -86,18 +162,20 @@ def engram_sinkhorn_step(
86) -> tuple[torch.Tensor, torch.Tensor]:162) -> tuple[torch.Tensor, torch.Tensor]:
87 """Normalize one Engram Sinkhorn iterate over rows.163 """Normalize one Engram Sinkhorn iterate over rows.
88 164 
89- ``matrix`` has shape ``(num_rows, head_dim)`` and both scale tensors are165+ All inputs must be contiguous FP32 NPU tensors and ``head_dim`` must be a
90- one-dimensional. All tensors must be contiguous FP32 NPU tensors;166+ positive multiple of 256. The function updates ``row_scale`` in place and
91- ``head_dim`` must be a positive multiple of 256 and ``row_scale`` must167+ returns the pre-normalization row norms and normalized column squared sums.
92- contain non-negative Sinkhorn scaling factors. The function updates168+ The caller must provide non-negative Sinkhorn scaling factors in
93- ``row_scale`` in place and returns the pre-normalization ``row_norm`` and169+ ``row_scale``.
94- normalized per-column squared sums ``col_sumsq``.
95- 
96- The three inputs must not share storage. ``col_sumsq`` is initialized to
97- zero before the cross-core atomic reduction.
98 """170 """
99 171 
100- num_rows, head_dim = _validate_inputs(matrix, row_scale, col_scale, eps)172+ num_rows, head_dim, eps = _validate_matrix_inputs(
173+ matrix,
174+ row_scale,
175+ col_scale,
176+ eps,
177+ head_dim_alignment=256,
178+ )
101 if num_rows == 0:179 if num_rows == 0:
102 return torch.empty_like(row_scale), torch.zeros_like(col_scale)180 return torch.empty_like(row_scale), torch.zeros_like(col_scale)
103 181 
@@ -115,7 +193,7 @@ def engram_sinkhorn_step(
115 + 3 * rows_per_core * row_scale.element_size()193 + 3 * rows_per_core * row_scale.element_size()
116 + (row_bytes if head_dim > 512 else 0)194 + (row_bytes if head_dim > 512 else 0)
117 )195 )
118- if shared_ub_bytes > _SHARED_UB_CAPACITY_BYTES:196+ if shared_ub_bytes > _SIMT_SHARED_UB_CAPACITY_BYTES:
119 raise ValueError(197 raise ValueError(
120 'the requested num_rows/head_dim combination exceeds the 216 KiB usable shared UB capacity: '198 'the requested num_rows/head_dim combination exceeds the 216 KiB usable shared UB capacity: '
121 f'num_rows={num_rows}, head_dim={head_dim}, required_bytes={shared_ub_bytes}.'199 f'num_rows={num_rows}, head_dim={head_dim}, required_bytes={shared_ub_bytes}.'
@@ -124,7 +202,7 @@ def engram_sinkhorn_step(
124 col_sumsq = torch.zeros_like(col_scale)202 col_sumsq = torch.zeros_like(col_scale)
125 kernel = get_engram_sinkhorn_step_kernel(203 kernel = get_engram_sinkhorn_step_kernel(
126 head_dim,204 head_dim,
127- float(eps),205+ eps,
128 num_cores,206 num_cores,
129 rows_per_core,207 rows_per_core,
130 chunk_rows,208 chunk_rows,
@@ -134,3 +212,55 @@ def engram_sinkhorn_step(
134 print(kernel.get_kernel_source())212 print(kernel.get_kernel_source())
135 kernel(matrix, row_scale, col_scale, row_norm, col_sumsq)213 kernel(matrix, row_scale, col_scale, row_norm, col_sumsq)
136 return row_norm, col_sumsq214 return row_norm, col_sumsq
215+ 
216+ 
217+def engram_sinkhorn_finalize(
218+ matrix: torch.Tensor,
219+ row_scale: torch.Tensor,
220+ col_scale: torch.Tensor,
221+ eps: float,
222+ alpha: float,
223+ out: torch.Tensor | None = None,
224+) -> torch.Tensor:
225+ """Materialize the final row-normalized Engram Sinkhorn matrix.
226+ 
227+ The three inputs must be contiguous FP32 NPU tensors and ``head_dim`` must
228+ be a positive multiple of 64. ``out`` may be supplied as a separate,
229+ contiguous FP32 output tensor with the same shape and device as ``matrix``.
230+ """
231+ 
232+ num_rows, head_dim, eps = _validate_matrix_inputs(
233+ matrix,
234+ row_scale,
235+ col_scale,
236+ eps,
237+ head_dim_alignment=64,
238+ )
239+ alpha = _check_real('alpha', alpha)
240+ if out is not None:
241+ if not isinstance(out, torch.Tensor):
242+ raise TypeError('out must be a torch.Tensor when provided.')
243+ _check_npu_tensor('out', out, dtypes=(torch.float32,), device=matrix.device)
244+ if out.shape != matrix.shape:
245+ raise ValueError(f'out must have shape {tuple(matrix.shape)}, got {tuple(out.shape)}.')
246+ _reject_aliases({'matrix': matrix, 'row_scale': row_scale, 'col_scale': col_scale, 'out': out})
247+ else:
248+ out = torch.empty_like(matrix)
249+ 
250+ if num_rows == 0:
251+ return out
252+ 
253+ num_stages = 6 if head_dim <= 512 else 3
254+ row_bytes = head_dim * matrix.element_size()
255+ shared_ub_bytes = (2 * num_stages + 1) * row_bytes + num_stages * row_scale.element_size()
256+ if shared_ub_bytes > _PHYSICAL_UB_CAPACITY_BYTES:
257+ raise ValueError(
258+ 'the requested head_dim exceeds the 248 KiB shared UB capacity: '
259+ f'head_dim={head_dim}, required_bytes={shared_ub_bytes}.'
260+ )
261+ num_cores = min(get_num_vector_cores(), num_rows)
262+ kernel = get_engram_sinkhorn_finalize_kernel(head_dim, eps, alpha, num_cores)
263+ if should_print_kernel_source():
264+ print(kernel.get_kernel_source())
265+ kernel(matrix, row_scale, col_scale, out)
266+ return out
Rsrc/cann_ops_tilelang/engram/engram_sinkhorn_step_kernel.py→src/cann_ops_tilelang/engram/engram_sinkhorn_kernel.py+191-1
@@ -1,10 +1,117 @@
1-"""TileLang Ascend implementation of one Engram Sinkhorn step."""1+"""TileLang Ascend kernels for the Engram Sinkhorn operator family."""
2 2 
3import tilelang3import tilelang
4from tilelang import language as T4from tilelang import language as T
5from tilelang.language import simd as S5from tilelang.language import simd as S
6 6 
7 7 
8+@tilelang.jit
9+def get_engram_sinkhorn_momentum_update_kernel(
10+ grad_dtype: T.dtype,
11+ momentum_dtype: T.dtype,
12+ nesterov: bool,
13+ beta1: float,
14+ num_cores: int,
15+):
16+ """Build the vectorized Ascend Sinkhorn momentum-update kernel."""
17+ 
18+ tile_size = 4096
19+ num_stages = 2
20+ vector_size = 64
21+ grad_is_bf16 = grad_dtype == T.bfloat16
22+ momentum_is_bf16 = momentum_dtype == T.bfloat16
23+ numel = T.dynamic('numel')
24+ 
25+ @T.prim_func
26+ def engram_sinkhorn_momentum_update_kernel(
27+ grad: T.Tensor[(numel,), grad_dtype],
28+ momentum_buffer: T.Tensor[(numel,), momentum_dtype],
29+ sinkhorn_input: T.Tensor[(numel,), T.float32],
30+ ):
31+ total_tiles = T.ceildiv(numel, tile_size)
32+ 
33+ with T.Kernel(num_cores) as core_id:
34+ grad_ub = T.alloc_shared((tile_size,), grad_dtype)
35+ momentum_in_ub = T.alloc_shared((tile_size,), momentum_dtype)
36+ momentum_out_ub = T.alloc_shared((tile_size,), momentum_dtype)
37+ sinkhorn_ub = T.alloc_shared((tile_size,), T.float32)
38+ T.annotate_buffer_versions(
39+ {
40+ grad_ub: num_stages,
41+ momentum_in_ub: num_stages,
42+ momentum_out_ub: num_stages,
43+ sinkhorn_ub: num_stages,
44+ }
45+ )
46+ 
47+ for tile_id in T.Persistent([total_tiles], num_cores, core_id, num_stages=num_stages):
48+ base = tile_id * tile_size
49+ valid_elements = T.min(tile_size, numel - base)
50+ T.copy(grad[base : base + valid_elements], grad_ub[:valid_elements])
51+ T.copy(
52+ momentum_buffer[base : base + valid_elements],
53+ momentum_in_ub[:valid_elements],
54+ )
55+ 
56+ with T.SimdVF():
57+ full = S.pset(32, 'PAT_ALL')
58+ for vector in range(tile_size // vector_size):
59+ offset = vector * vector_size
60+ if grad_is_bf16:
61+ grad_vec = S.vcvt(
62+ S.vld(grad_ub[offset], dist='UNPK_B16'),
63+ T.float32,
64+ part=0,
65+ )
66+ else:
67+ grad_vec = S.vld(grad_ub[offset])
68+ 
69+ if momentum_is_bf16:
70+ momentum_vec = S.vcvt(
71+ S.vld(momentum_in_ub[offset], dist='UNPK_B16'),
72+ T.float32,
73+ part=0,
74+ )
75+ else:
76+ momentum_vec = S.vld(momentum_in_ub[offset])
77+ 
78+ updated_momentum = S.vadd(
79+ S.vmuls(momentum_vec, beta1, full),
80+ S.vmuls(grad_vec, 1.0 - beta1, full),
81+ full,
82+ )
83+ if nesterov:
84+ sinkhorn_vec = S.vadd(
85+ S.vmuls(updated_momentum, beta1, full),
86+ S.vmuls(grad_vec, 1.0 - beta1, full),
87+ full,
88+ )
89+ else:
90+ sinkhorn_vec = updated_momentum
91+ 
92+ if momentum_is_bf16:
93+ S.vsts(
94+ momentum_out_ub[offset],
95+ S.vcvt(updated_momentum, T.bfloat16),
96+ full,
97+ dist='PK_B32',
98+ )
99+ else:
100+ S.vsts(momentum_out_ub[offset], updated_momentum, full)
101+ S.vsts(sinkhorn_ub[offset], sinkhorn_vec, full)
102+ 
103+ T.copy(
104+ momentum_out_ub[:valid_elements],
105+ momentum_buffer[base : base + valid_elements],
106+ )
107+ T.copy(
108+ sinkhorn_ub[:valid_elements],
109+ sinkhorn_input[base : base + valid_elements],
110+ )
111+ 
112+ return engram_sinkhorn_momentum_update_kernel
113+ 
114+ 
8@tilelang.jit(115@tilelang.jit(
9 pass_configs={116 pass_configs={
10 tilelang.PassConfigKey.TL_ENABLE_AUTO_SCHEDULE: False,117 tilelang.PassConfigKey.TL_ENABLE_AUTO_SCHEDULE: False,
@@ -156,3 +263,86 @@ def get_engram_sinkhorn_step_kernel(
156 T.atomic_add(col_sumsq[index], col_acc_ub[index])263 T.atomic_add(col_sumsq[index], col_acc_ub[index])
157 264 
158 return engram_sinkhorn_step_kernel265 return engram_sinkhorn_step_kernel
266+ 
267+ 
268+@tilelang.jit
269+def get_engram_sinkhorn_finalize_kernel(
270+ head_dim: int,
271+ eps: float,
272+ alpha: float,
273+ num_cores: int,
274+):
275+ """Build the vectorized Ascend Sinkhorn finalize kernel."""
276+ 
277+ vector_size = 64
278+ assert head_dim % vector_size == 0
279+ num_vectors = head_dim // vector_size
280+ num_stages = 6 if head_dim <= 512 else 3
281+ num_rows = T.dynamic('num_rows')
282+ 
283+ @T.prim_func
284+ def engram_sinkhorn_finalize_kernel(
285+ matrix: T.Tensor[(num_rows, head_dim), T.float32],
286+ row_scale: T.Tensor[(num_rows,), T.float32],
287+ col_scale: T.Tensor[(head_dim,), T.float32],
288+ out: T.Tensor[(num_rows, head_dim), T.float32],
289+ ):
290+ with T.Kernel(num_cores) as core_id:
291+ x_ub = T.alloc_shared((head_dim,), T.float32)
292+ out_ub = T.alloc_shared((head_dim,), T.float32)
293+ col_scale_ub = T.alloc_shared((head_dim,), T.float32)
294+ row_scale_ub = T.alloc_shared((1,), T.float32)
295+ T.annotate_buffer_versions(
296+ {
297+ x_ub: num_stages,
298+ out_ub: num_stages,
299+ row_scale_ub: num_stages,
300+ }
301+ )
302+ 
303+ T.copy(col_scale[:head_dim], col_scale_ub[:head_dim])
304+ rows_per_core = T.ceildiv(num_rows, num_cores)
305+ row_begin = core_id * rows_per_core
306+ row_end = T.min(row_begin + rows_per_core, num_rows)
307+ iterations = row_end - row_begin
308+ for row_iter in T.Pipelined(
309+ iterations,
310+ num_stages=num_stages,
311+ annotations={'multi_buffer_eligible': [x_ub, out_ub, row_scale_ub]},
312+ ):
313+ row_id = row_begin + row_iter
314+ T.copy(matrix[row_id, :head_dim], x_ub[:head_dim])
315+ row_scale_ub[0] = row_scale[row_id]
316+ 
317+ with T.SimdVF():
318+ full = S.pset(32, 'PAT_ALL')
319+ zero = S.vdup(0.0, T.float32, full)
320+ alpha_vec = S.vdup(alpha, T.float32, full)
321+ scale = S.vld(row_scale_ub[0], dist='BRC_B32')
322+ sum_x2 = S.alloc_var(T.float32)
323+ sum_x2 = zero
324+ 
325+ for vector in range(num_vectors):
326+ offset = vector * vector_size
327+ x_vec = S.vmul(
328+ S.vmul(S.vld(x_ub[offset]), scale, full),
329+ S.vld(col_scale_ub[offset]),
330+ full,
331+ )
332+ S.vmula(sum_x2, x_vec, x_vec, full)
333+ 
334+ norm = S.vsqrt(S.vdupv(S.vcadd(sum_x2, full), full))
335+ output_scale = S.vdiv(alpha_vec, S.vadds(norm, eps))
336+ final_scale = S.vmul(scale, output_scale, full)
337+ for vector in range(num_vectors):
338+ offset = vector * vector_size
339+ out_vec = S.vmul(
340+ S.vmul(S.vld(x_ub[offset]), final_scale, full),
341+ S.vld(col_scale_ub[offset]),
342+ full,
343+ )
344+ S.vsts(out_ub[offset], out_vec, full)
345+ 
346+ T.copy(out_ub[:head_dim], out[row_id, :head_dim])
347+ 
348+ return engram_sinkhorn_finalize_kernel
@@ -1,6 +0,0 @@
1-{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 2048, "num_partials": 30}, "param_id": "hidden=2048,num_partials=30,", "cluster": "a5", "time_us": 2.15, "bandwidth_gbs": 503.2415}
2-{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 2560, "num_partials": 30}, "param_id": "hidden=2560,num_partials=30,", "cluster": "a5", "time_us": 2.33, "bandwidth_gbs": 581.0937}
3-{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 3072, "num_partials": 30}, "param_id": "hidden=3072,num_partials=30,", "cluster": "a5", "time_us": 2.33, "bandwidth_gbs": 695.5042}
4-{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 4096, "num_partials": 30}, "param_id": "hidden=4096,num_partials=30,", "cluster": "a5", "time_us": 2.58, "bandwidth_gbs": 836.9596}
5-{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 6144, "num_partials": 30}, "param_id": "hidden=6144,num_partials=30,", "cluster": "a5", "time_us": 2.83, "bandwidth_gbs": 1148.1818}
6-{"kernel": "engram_grad_w_reduce", "operation": "fwd", "params": {"hidden": 7168, "num_partials": 30}, "param_id": "hidden=7168,num_partials=30,", "cluster": "a5", "time_us": 2.87, "bandwidth_gbs": 1319.1494}
@@ -1,7 +1,3 @@
1-"""Pure PyTorch reference for the Engram gate operator."""
2- 
3-from __future__ import annotations
4- 
5import torch1import torch
6 2 
7 3 
@@ -14,34 +10,47 @@ def engram_gate_ref(
14 eps: float,10 eps: float,
15 save_for_backward: bool = False,11 save_for_backward: bool = False,
16) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:12) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
17- """Compute the Engram gate with eager PyTorch operations.13+ """Pure PyTorch reference implementation of engram gate (vectorized, supports autograd).
18 14 
19- This implementation is intentionally device-agnostic and supports15+ Computes: output = x + sigmoid(signed_sqrt(dot(RMSNorm(x, wh), RMSNorm(k, we)) * scalar)) * v
20- autograd, making it the numerical oracle for both forward and backward16+ 
21- TileLang kernels.17+ Args:
18+ hidden_states: Input of shape (num_tokens, hc_mult, hidden_size), bfloat16.
19+ kv: Packed key/value embeddings of shape (num_tokens, (hc_mult + 1) * hidden_size), bfloat16.
20+ weight_hidden: RMSNorm weight for hidden states, shape (hc_mult, hidden_size), bfloat16.
21+ weight_embed: RMSNorm weight for key embeddings, shape (hc_mult, hidden_size), bfloat16.
22+ clamp_value: Clamp threshold for signed-sqrt gate activation.
23+ eps: Epsilon for RMSNorm numerical stability.
24+ save_for_backward: If True, also return (dot, gate_score, rstd_x, rstd_k).
25+ 
26+ Returns:
27+ If save_for_backward is False: output tensor of shape (num_tokens, hc_mult, hidden_size), bfloat16.
28+ If save_for_backward is True: tuple of (output, dot, gate_score, rstd_x, rstd_k).
22 """29 """
23- 
24 num_tokens, hc_mult, hidden_size = hidden_states.shape30 num_tokens, hc_mult, hidden_size = hidden_states.shape
25 scalar = hidden_size**-0.531 scalar = hidden_size**-0.5
32+ 
26 k, v = kv.split([hc_mult * hidden_size, hidden_size], dim=-1)33 k, v = kv.split([hc_mult * hidden_size, hidden_size], dim=-1)
27 k = k.view(num_tokens, hc_mult, hidden_size)34 k = k.view(num_tokens, hc_mult, hidden_size)
28 35 
29 x = hidden_states.float()36 x = hidden_states.float()
30- k_float = k.float()37+ k_f = k.float()
31- weight_hidden_float = weight_hidden.float().unsqueeze(0)38+ wh = weight_hidden.float().unsqueeze(0)
32- weight_embed_float = weight_embed.float().unsqueeze(0)39+ we = weight_embed.float().unsqueeze(0)
33 40 
34- rstd_x = torch.rsqrt(x.square().mean(-1) + eps)41+ # RMSNorm
35- rstd_k = torch.rsqrt(k_float.square().mean(-1) + eps)42+ rstd_x = torch.rsqrt(x.pow(2).mean(-1) + eps)
36- raw_dot = torch.einsum(43+ rstd_k = torch.rsqrt(k_f.pow(2).mean(-1) + eps)
37- '...d,...d->...',44+ 
38- x * weight_hidden_float,45+ # Dot -> sqrt-gate -> sigmoid
39- k_float * weight_embed_float,46+ # raw_dot is the unnormalized sum(x * wh * k * we), matching the kernel's dot_out
40- )47+ raw_dot = torch.einsum('...d,...d->...', x * wh, k_f * we)
41- normalized_dot = raw_dot * rstd_x * rstd_k * scalar48+ dot = raw_dot * rstd_x * rstd_k * scalar
42- signed_sqrt = normalized_dot.abs().clamp_min(clamp_value).sqrt() * normalized_dot.sign()49+ signed_sqrt = dot.abs().clamp_min(clamp_value).sqrt() * dot.sign()
43 gate_score = signed_sqrt.sigmoid()50 gate_score = signed_sqrt.sigmoid()
44- output = (x + gate_score.unsqueeze(-1) * v.unsqueeze(-2)).bfloat16()51+ 
52+ output = x + gate_score.unsqueeze(-1) * v.unsqueeze(-2)
53+ output = output.bfloat16()
45 54 
46 if save_for_backward:55 if save_for_backward:
47 return output, raw_dot, gate_score, rstd_x, rstd_k56 return output, raw_dot, gate_score, rstd_x, rstd_k
@@ -0,0 +1,69 @@
1+import torch
2+ 
3+ 
4+def make_offsets(vocab_sizes: torch.Tensor) -> torch.Tensor:
5+ """Compute exclusive prefix-sum offsets from vocab_sizes.
6+ 
7+ Args:
8+ vocab_sizes: Per-layer per-ngram embedding table sizes of shape
9+ (num_ngram_layers, max_ngram_size - 1, num_embed_table_per_ngram), int32.
10+ 
11+ Returns:
12+ Offsets of shape (num_ngram_layers, (max_ngram_size - 1) * num_embed_table_per_ngram), int32.
13+ """
14+ num_ngram_layers = vocab_sizes.shape[0]
15+ offsets_list = []
16+ for layer_idx in range(num_ngram_layers):
17+ flat = vocab_sizes[layer_idx].view(-1)
18+ prefix = torch.cat(
19+ [
20+ torch.zeros(1, dtype=torch.int32, device=flat.device),
21+ flat[:-1].cumsum(0, dtype=torch.int32),
22+ ]
23+ )
24+ offsets_list.append(prefix)
25+ return torch.stack(offsets_list, dim=0)
26+ 
27+ 
28+def engram_hash_ref(
29+ ngram_token_ids: torch.Tensor,
30+ multipliers: torch.Tensor,
31+ vocab_sizes: torch.Tensor,
32+ offsets: torch.Tensor,
33+) -> torch.Tensor:
34+ """Pure PyTorch reference implementation of engram hash.
35+ 
36+ Args:
37+ ngram_token_ids: N-gram token IDs of shape (num_tokens, max_ngram_size), int32.
38+ multipliers: Per-layer hash multipliers of shape (num_ngram_layers, max_ngram_size), int64.
39+ vocab_sizes: Per-layer per-ngram embedding table sizes of shape
40+ (num_ngram_layers, max_ngram_size - 1, num_embed_table_per_ngram), int32.
41+ offsets: Per-layer embedding table offsets of shape
42+ (num_ngram_layers, (max_ngram_size - 1) * num_embed_table_per_ngram), int32.
43+ 
44+ Returns:
45+ Embedding indices of shape
46+ (num_ngram_layers, num_tokens, (max_ngram_size - 1) * num_embed_table_per_ngram), int32.
47+ """
48+ 
49+ num_ngram_layers = multipliers.shape[0]
50+ max_ngram_size = multipliers.shape[1]
51+ 
52+ prod = ngram_token_ids.to(torch.int64).unsqueeze(0) * multipliers.unsqueeze(1)
53+ 
54+ ans = [[] for _ in range(num_ngram_layers)]
55+ hashes = prod[:, :, 0].clone()
56+ for i in range(1, max_ngram_size):
57+ hashes.bitwise_xor_(prod[:, :, i])
58+ for layer_idx in range(num_ngram_layers):
59+ ans[layer_idx].append(
60+ (hashes[layer_idx].unsqueeze(-1) % vocab_sizes[layer_idx, i - 1].to(torch.int64).unsqueeze(0)).to(
61+ torch.int32
62+ )
63+ )
64+ 
65+ for layer_idx in range(num_ngram_layers):
66+ ans[layer_idx] = torch.cat(ans[layer_idx], dim=-1)
67+ 
68+ output = torch.stack(ans, dim=0)
69+ return output + offsets.unsqueeze(1)
Rtests/engram/engram_sinkhorn_step_ref.py→tests/engram/engram_sinkhorn_ref.py+38-1
@@ -1,10 +1,27 @@
1-"""Pure PyTorch reference for one Engram Sinkhorn normalization step."""1+"""Pure PyTorch references for the Engram Sinkhorn operator family."""
2 2 
3from __future__ import annotations3from __future__ import annotations
4 4 
5import torch5import torch
6 6 
7 7 
8+def engram_sinkhorn_momentum_update_ref(
9+ grad: torch.Tensor,
10+ momentum_buffer: torch.Tensor,
11+ beta1: float,
12+ nesterov: bool = True,
13+) -> torch.Tensor:
14+ """Update momentum in place and return the FP32 Sinkhorn input."""
15+ 
16+ grad_fp32 = grad.float()
17+ momentum_fp32 = momentum_buffer.float()
18+ momentum_fp32.mul_(beta1).add_(grad_fp32, alpha=1 - beta1)
19+ momentum_buffer.copy_(momentum_fp32)
20+ if nesterov:
21+ return torch.lerp(momentum_fp32, grad_fp32, 1 - beta1)
22+ return momentum_fp32
23+ 
24+ 
8def engram_sinkhorn_step_ref(25def engram_sinkhorn_step_ref(
9 matrix: torch.Tensor,26 matrix: torch.Tensor,
10 row_scale: torch.Tensor,27 row_scale: torch.Tensor,
@@ -18,3 +35,23 @@ def engram_sinkhorn_step_ref(
18 row_scale.div_(row_norm + eps)35 row_scale.div_(row_norm + eps)
19 normalized = row_scale.unsqueeze(1) * matrix * col_scale36 normalized = row_scale.unsqueeze(1) * matrix * col_scale
20 return row_norm, normalized.square().sum(dim=0)37 return row_norm, normalized.square().sum(dim=0)
38+ 
39+ 
40+def engram_sinkhorn_finalize_ref(
41+ matrix: torch.Tensor,
42+ row_scale: torch.Tensor,
43+ col_scale: torch.Tensor,
44+ eps: float,
45+ alpha: float,
46+ out: torch.Tensor | None = None,
47+) -> torch.Tensor:
48+ """Materialize the final row-normalized Sinkhorn matrix."""
49+ 
50+ iterate = row_scale.unsqueeze(1) * matrix * col_scale
51+ row_norm = torch.linalg.vector_norm(iterate, dim=1)
52+ normalized_row_scale = row_scale / (row_norm + eps)
53+ result = alpha * normalized_row_scale.unsqueeze(1) * matrix * col_scale
54+ if out is None:
55+ return result
56+ out.copy_(result)
57+ return out
@@ -1,20 +0,0 @@
1-{
2- "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=2048,num_partials=30,]": {
3- "peak_npu_mb": 33.0
4- },
5- "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=2560,num_partials=30,]": {
6- "peak_npu_mb": 33.3
7- },
8- "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=3072,num_partials=30,]": {
9- "peak_npu_mb": 33.5
10- },
11- "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=4096,num_partials=30,]": {
12- "peak_npu_mb": 34.1
13- },
14- "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=6144,num_partials=30,]": {
15- "peak_npu_mb": 35.1
16- },
17- "engram/test_engram_grad_w_reduce.py::test_engram_grad_w_reduce[hidden=7168,num_partials=30,]": {
18- "peak_npu_mb": 35.6
19- }
20-}
@@ -22,7 +22,6 @@ def generate_test_data(params):
22 22 
23 23 
24def generate_test_params(level: int) -> list[dict]:24def generate_test_params(level: int) -> list[dict]:
25- del level
26 return [{'hc': hc, 'hidden': hidden_size} for hc in (4,) for hidden_size in generate_hidden_sizes(256)]25 return [{'hc': hc, 'hidden': hidden_size} for hc in (4,) for hidden_size in generate_hidden_sizes(256)]
27 26 
28 27 
@@ -33,7 +32,6 @@ def test_engram_fused_weight(params):
33 ref = wh_data.float() * we_data.float()32 ref = wh_data.float() * we_data.float()
34 out = cann_ops_tilelang.engram_fused_weight(wh_data, we_data)33 out = cann_ops_tilelang.engram_fused_weight(wh_data, we_data)
35 34 
36- assert out.dtype == torch.float32
37 assert_equal(out, ref)35 assert_equal(out, ref)
38 36 
39 37 
@@ -0,0 +1,150 @@
1+import os
2+ 
3+import pytest
4+import torch
5+ 
6+import cann_ops_tilelang
7+from cann_ops_tilelang.config import get_device
8+from tests.engram.engram_gate_ref import engram_gate_ref
9+from tests.testing.bench import make_param_id
10+from tests.testing.generator import generate_hidden_sizes, generate_num_tokens, get_test_level
11+from tests.testing.numeric import calc_diff, count_bytes
12+ 
13+os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0'
14+pytestmark = pytest.mark.npu
15+ 
16+ 
17+def generate_test_data(params):
18+ if get_device().type == 'npu':
19+ torch.manual_seed(0)
20+ num_tokens = params['num_tokens']
21+ hc_mult = params['hc']
22+ hidden_size = params['hidden']
23+ eps = 1e-20
24+ clamp_value = 1e-6
25+ x_data = torch.randn(num_tokens, hc_mult, hidden_size, dtype=torch.bfloat16, device=get_device())
26+ kv_data = torch.randn(num_tokens, (hc_mult + 1) * hidden_size, dtype=torch.bfloat16, device=get_device())
27+ wh_data = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=get_device())
28+ we_data = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=get_device())
29+ weight_fused = wh_data.float() * we_data.float()
30+ grad_out = torch.randn(num_tokens, hc_mult, hidden_size, dtype=torch.bfloat16, device=get_device())
31+ return x_data, kv_data, wh_data, we_data, weight_fused, grad_out, eps, clamp_value
32+ 
33+ 
34+def generate_test_params(level: int) -> list[dict]:
35+ return [
36+ {'num_tokens': t, 'hc': hc, 'hidden': hidden_size}
37+ for t in generate_num_tokens(level)
38+ for hc in (4,)
39+ for hidden_size in generate_hidden_sizes(256)
40+ ]
41+ 
42+ 
43+@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id)
44+def test_engram_gate_bwd(params):
45+ x_data, kv_data, wh_data, we_data, weight_fused, grad_out, eps, clamp_value = generate_test_data(params)
46+ 
47+ x_ref = x_data.clone().requires_grad_(True)
48+ kv_ref = kv_data.clone().requires_grad_(True)
49+ wh_ref = wh_data.float().requires_grad_(True)
50+ we_ref = we_data.float().requires_grad_(True)
51+ output_ref, dot_ref, gate_score_ref, rstd_x_ref, rstd_k_ref = engram_gate_ref(
52+ x_ref,
53+ kv_ref,
54+ wh_ref,
55+ we_ref,
56+ clamp_value,
57+ eps,
58+ save_for_backward=True,
59+ )
60+ output_ref.backward(grad_out)
61+ 
62+ grad_x, grad_kv, grad_w_partial = cann_ops_tilelang.engram_gate_bwd(
63+ grad_out,
64+ x_data,
65+ kv_data,
66+ weight_fused,
67+ dot_ref,
68+ gate_score_ref,
69+ rstd_x_ref,
70+ rstd_k_ref,
71+ clamp_value,
72+ )
73+ grad_w_fused = grad_w_partial.sum(0)
74+ grad_wh = grad_w_fused * we_data.float()
75+ grad_we = grad_w_fused * wh_data.float()
76+ 
77+ diff_x = calc_diff(grad_x, x_ref.grad)
78+ assert diff_x < 1e-8, f'grad_x mismatch: {diff_x:.6e}'
79+ diff_kv = calc_diff(grad_kv, kv_ref.grad)
80+ assert diff_kv < 1e-8, f'grad_kv mismatch: {diff_kv:.6e}'
81+ diff_wh = calc_diff(grad_wh, wh_ref.grad)
82+ assert diff_wh < 1e-8, f'grad_wh mismatch: {diff_wh:.6e}'
83+ diff_we = calc_diff(grad_we, we_ref.grad)
84+ assert diff_we < 1e-8, f'grad_we mismatch: {diff_we:.6e}'
85+ 
86+ 
87+@pytest.mark.benchmark
88+@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)
89+def test_engram_gate_bwd_benchmark(benchmark_timer, benchmark_record, params):
90+ x_data, kv_data, wh_data, we_data, weight_fused, grad_out, eps, clamp_value = generate_test_data(params)
91+ 
92+ _, dot_ref, gate_score_ref, rstd_x_ref, rstd_k_ref = engram_gate_ref(
93+ x_data,
94+ kv_data,
95+ wh_data,
96+ we_data,
97+ clamp_value,
98+ eps,
99+ save_for_backward=True,
100+ )
101+ grad_x, grad_kv, grad_w_partial = cann_ops_tilelang.engram_gate_bwd(
102+ grad_out,
103+ x_data,
104+ kv_data,
105+ weight_fused,
106+ dot_ref,
107+ gate_score_ref,
108+ rstd_x_ref,
109+ rstd_k_ref,
110+ clamp_value,
111+ )
112+ grad_w_fused = grad_w_partial.sum(0)
113+ grad_wh = grad_w_fused * we_data.float()
114+ grad_we = grad_w_fused * wh_data.float()
115+ 
116+ def run_backward():
117+ return cann_ops_tilelang.engram_gate_bwd(
118+ grad_out,
119+ x_data,
120+ kv_data,
121+ weight_fused,
122+ dot_ref,
123+ gate_score_ref,
124+ rstd_x_ref,
125+ rstd_k_ref,
126+ clamp_value,
127+ )
128+ 
129+ t_us = benchmark_timer(run_backward)
130+ num_bytes = count_bytes(
131+ grad_out,
132+ x_data,
133+ kv_data,
134+ weight_fused,
135+ dot_ref,
136+ gate_score_ref,
137+ rstd_x_ref,
138+ rstd_k_ref,
139+ grad_x,
140+ grad_kv,
141+ grad_wh,
142+ grad_we,
143+ )
144+ benchmark_record(
145+ kernel='engram_gate_bwd',
146+ operation='bwd',
147+ params=params,
148+ time_us=t_us,
149+ bandwidth_gbs=num_bytes / t_us / 1e3,
150+ )
@@ -0,0 +1,147 @@
1+import os
2+ 
3+import pytest
4+import torch
5+ 
6+import cann_ops_tilelang
7+from cann_ops_tilelang.config import get_device
8+from tests.engram.engram_gate_ref import engram_gate_ref
9+from tests.testing.bench import make_param_id
10+from tests.testing.generator import generate_hidden_sizes, generate_num_tokens, get_test_level
11+from tests.testing.numeric import assert_equal, calc_diff, count_bytes
12+ 
13+os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0'
14+pytestmark = pytest.mark.npu
15+ 
16+ 
17+def generate_test_data(params):
18+ if get_device().type == 'npu':
19+ torch.manual_seed(0)
20+ num_tokens = params['num_tokens']
21+ hc_mult = params['hc']
22+ hidden_size = params['hidden']
23+ eps = 1e-20
24+ clamp_value = 1e-6
25+ x_data = torch.randn(num_tokens, hc_mult, hidden_size, dtype=torch.bfloat16, device=get_device())
26+ kv_data = torch.randn(num_tokens, (hc_mult + 1) * hidden_size, dtype=torch.bfloat16, device=get_device())
27+ wh_data = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=get_device())
28+ we_data = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=get_device())
29+ weight_fused = wh_data.float() * we_data.float()
30+ return x_data, kv_data, wh_data, we_data, weight_fused, eps, clamp_value
31+ 
32+ 
33+def generate_test_params(level: int) -> list[dict]:
34+ return [
35+ {'num_tokens': t, 'hc': hc, 'hidden': hidden_size}
36+ for t in generate_num_tokens(level)
37+ for hc in (4,)
38+ for hidden_size in generate_hidden_sizes(256)
39+ ]
40+ 
41+ 
42+@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id)
43+def test_engram_gate_fwd(params):
44+ x_data, kv_data, wh_data, we_data, weight_fused, eps, clamp_value = generate_test_data(params)
45+ 
46+ out_ref, dot_ref, gate_score_ref, rstd_x_ref, rstd_k_ref = engram_gate_ref(
47+ x_data,
48+ kv_data,
49+ wh_data,
50+ we_data,
51+ clamp_value,
52+ eps,
53+ save_for_backward=True,
54+ )
55+ 
56+ out_save, dot, gate_score, rstd_x, rstd_k = cann_ops_tilelang.engram_gate_fwd(
57+ x_data,
58+ kv_data,
59+ weight_fused,
60+ eps,
61+ clamp_value,
62+ save_for_backward=True,
63+ )
64+ assert dot is not None and gate_score is not None and rstd_x is not None and rstd_k is not None
65+ diff_out = calc_diff(out_save, out_ref)
66+ assert diff_out < 2e-10, f'out_save mismatch: {diff_out:.6e}'
67+ diff_dot = calc_diff(dot, dot_ref)
68+ assert diff_dot < 2e-10, f'dot mismatch: {diff_dot:.6e}'
69+ diff_gate = calc_diff(gate_score, gate_score_ref)
70+ assert diff_gate < 2e-10, f'gate_score mismatch: {diff_gate:.6e}'
71+ diff_rstd_x = calc_diff(rstd_x, rstd_x_ref)
72+ assert diff_rstd_x < 2e-10, f'rstd_x mismatch: {diff_rstd_x:.6e}'
73+ diff_rstd_k = calc_diff(rstd_k, rstd_k_ref)
74+ assert diff_rstd_k < 2e-10, f'rstd_k mismatch: {diff_rstd_k:.6e}'
75+ 
76+ out_no_save, dot_n, gate_score_n, rstd_x_n, rstd_k_n = cann_ops_tilelang.engram_gate_fwd(
77+ x_data,
78+ kv_data,
79+ weight_fused,
80+ eps,
81+ clamp_value,
82+ save_for_backward=False,
83+ )
84+ assert dot_n is None and gate_score_n is None and rstd_x_n is None and rstd_k_n is None
85+ diff_out = calc_diff(out_no_save, out_ref)
86+ assert diff_out < 2e-10, f'out_no_save mismatch: {diff_out:.6e}'
87+ assert_equal(out_no_save, out_save)
88+ 
89+ 
90+@pytest.mark.benchmark
91+@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)
92+def test_engram_gate_fwd_benchmark(benchmark_timer, benchmark_record, params):
93+ x_data, kv_data, _, _, weight_fused, eps, clamp_value = generate_test_data(params)
94+ 
95+ out_save, dot, gate_score, rstd_x, rstd_k = cann_ops_tilelang.engram_gate_fwd(
96+ x_data,
97+ kv_data,
98+ weight_fused,
99+ eps,
100+ clamp_value,
101+ save_for_backward=True,
102+ )
103+ t_save_us = benchmark_timer(
104+ lambda: cann_ops_tilelang.engram_gate_fwd(
105+ x_data,
106+ kv_data,
107+ weight_fused,
108+ eps,
109+ clamp_value,
110+ save_for_backward=True,
111+ )
112+ )
113+ num_bytes_save = count_bytes(x_data, kv_data, weight_fused, out_save, dot, gate_score, rstd_x, rstd_k)
114+ benchmark_record(
115+ kernel='engram_gate_fwd',
116+ operation='fwd',
117+ params={**params, 'save': True},
118+ time_us=t_save_us,
119+ bandwidth_gbs=num_bytes_save / t_save_us / 1e3,
120+ )
121+ 
122+ out_no_save = cann_ops_tilelang.engram_gate_fwd(
123+ x_data,
124+ kv_data,
125+ weight_fused,
126+ eps,
127+ clamp_value,
128+ save_for_backward=False,
129+ )[0]
130+ t_no_save_us = benchmark_timer(
131+ lambda: cann_ops_tilelang.engram_gate_fwd(
132+ x_data,
133+ kv_data,
134+ weight_fused,
135+ eps,
136+ clamp_value,
137+ save_for_backward=False,
138+ )
139+ )
140+ num_bytes_no_save = count_bytes(x_data, kv_data, weight_fused, out_no_save)
141+ benchmark_record(
142+ kernel='engram_gate_fwd',
143+ operation='fwd',
144+ params={**params, 'save': False},
145+ time_us=t_no_save_us,
146+ bandwidth_gbs=num_bytes_no_save / t_no_save_us / 1e3,
147+ )
@@ -3,17 +3,17 @@ import os
3import pytest3import pytest
4import torch4import torch
5 5 
6-import cann_ops_tilelang6+from cann_ops_tilelang import engram_grad_w_reduce
7-from cann_ops_tilelang.config import get_device, get_num_vector_cores7+from cann_ops_tilelang.config import clear_runtime_caches, get_device
8from tests.testing.bench import make_param_id8from tests.testing.bench import make_param_id
9-from tests.testing.generator import generate_hidden_sizes, get_test_level9+from tests.testing.generator import generate_hidden_sizes, generate_num_sms, get_test_level
10from tests.testing.numeric import calc_diff, count_bytes10from tests.testing.numeric import calc_diff, count_bytes
11 11 
12os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0'12os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0'
13pytestmark = pytest.mark.npu13pytestmark = pytest.mark.npu
14 14 
15 15 
16-def engram_grad_w_reduce_ref(16+def grad_w_reduce_ref(
17 grad_w_partial,17 grad_w_partial,
18 weight_hidden,18 weight_hidden,
19 weight_embed,19 weight_embed,
@@ -21,82 +21,78 @@ def engram_grad_w_reduce_ref(
21 grad_weight_embed,21 grad_weight_embed,
22):22):
23 grad_w_sum = grad_w_partial.sum(0)23 grad_w_sum = grad_w_partial.sum(0)
24- grad_weight_hidden.add_(grad_w_sum * weight_embed.float())24+ grad_weight_hidden += grad_w_sum * weight_embed.float()
25- grad_weight_embed.add_(grad_w_sum * weight_hidden.float())25+ grad_weight_embed += grad_w_sum * weight_hidden.float()
26- 
27- 
28-def generate_num_partials(level: int) -> list[int]:
29- gate_num_partials = (get_num_vector_cores() - 12) // 2
30- values = [gate_num_partials]
31- if level >= 1:
32- values.insert(0, max(gate_num_partials - 9, 1))
33- if level >= 2:
34- values.insert(0, 1)
35- return list(dict.fromkeys(values))
36- 
37- 
38-def generate_test_params(level: int) -> list[dict]:
39- return [
40- {'hidden': hidden_size, 'num_partials': num_partials}
41- for hidden_size in generate_hidden_sizes(256)
42- for num_partials in generate_num_partials(level)
43- ]
44 26 
45 27 
46def generate_test_data(params):28def generate_test_data(params):
47- torch.manual_seed(0)
48 hidden_size = params['hidden']29 hidden_size = params['hidden']
49- num_partials = params['num_partials']
50 hc_mult = 430 hc_mult = 4
31+ num_persistent_blocks = params['num_sms']
51 device = get_device()32 device = get_device()
52- grad_w_partial = torch.randn(num_partials, hc_mult, hidden_size, dtype=torch.float32, device=device)33+ grad_w_partial = torch.randn(
34+ num_persistent_blocks,
35+ hc_mult,
36+ hidden_size,
37+ dtype=torch.float32,
38+ device=device,
39+ )
53 weight_hidden = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=device)40 weight_hidden = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=device)
54 weight_embed = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=device)41 weight_embed = torch.randn(hc_mult, hidden_size, dtype=torch.bfloat16, device=device)
55 return grad_w_partial, weight_hidden, weight_embed42 return grad_w_partial, weight_hidden, weight_embed
56 43 
57 44 
58-@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id)45+def generate_test_params(level: int) -> list[dict]:
59-def test_engram_grad_w_reduce(params):46+ return [
60- grad_w_partial, weight_hidden, weight_embed = generate_test_data(params)47+ {
61- output_shape = weight_hidden.shape48+ 'hidden': hidden_size,
62- device = weight_hidden.device49+ 'num_sms': num_sms,
63- grad_weight_hidden_ref = torch.randn(output_shape, dtype=torch.float32, device=device)50+ }
64- grad_weight_embed_ref = torch.randn(output_shape, dtype=torch.float32, device=device)51+ for hidden_size in generate_hidden_sizes(256)
65- grad_weight_hidden = grad_weight_hidden_ref.clone()52+ for num_sms in generate_num_sms(level)
66- grad_weight_embed = grad_weight_embed_ref.clone()53+ ]
67 54 
68- engram_grad_w_reduce_ref(55+ 
69- grad_w_partial,56+def _set_num_sms(monkeypatch, num_sms: int) -> None:
70- weight_hidden,57+ monkeypatch.setenv('OPS_TILELANG_NUM_VECTOR_CORES', str(num_sms * 2))
71- weight_embed,58+ clear_runtime_caches()
72- grad_weight_hidden_ref,59+ 
73- grad_weight_embed_ref,60+ 
74- )61+@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id)
75- cann_ops_tilelang.engram_grad_w_reduce(62+def test_engram_grad_w_reduce(params, monkeypatch):
76- grad_w_partial,63+ hidden_size = params['hidden']
77- weight_hidden,64+ device = get_device()
78- weight_embed,65+ 
79- grad_weight_hidden,66+ _set_num_sms(monkeypatch, params['num_sms'])
80- grad_weight_embed,67+ grad_w_partial, weight_hidden, weight_embed = generate_test_data(params)
81- )68+ hc_mult = grad_w_partial.shape[1]
82- for name, actual, expected in (69+ 
83- ('grad_weight_hidden', grad_weight_hidden, grad_weight_hidden_ref),70+ grad_wh_ref = torch.randn(hc_mult, hidden_size, dtype=torch.float32, device=device)
84- ('grad_weight_embed', grad_weight_embed, grad_weight_embed_ref),71+ grad_we_ref = torch.randn(hc_mult, hidden_size, dtype=torch.float32, device=device)
85- ):72+ grad_weight_hidden = grad_wh_ref.clone()
86- diff = calc_diff(actual, expected)73+ grad_weight_embed = grad_we_ref.clone()
87- assert diff < 1e-10, f'{name} mismatch: {diff:.6e}'74+ grad_w_reduce_ref(grad_w_partial, weight_hidden, weight_embed, grad_wh_ref, grad_we_ref)
75+ engram_grad_w_reduce(grad_w_partial, weight_hidden, weight_embed, grad_weight_hidden, grad_weight_embed)
76+ diff_wh = calc_diff(grad_weight_hidden.cpu(), grad_wh_ref.cpu())
77+ assert diff_wh < 1e-10, f'grad_wh mismatch: {diff_wh:.6e}'
78+ diff_we = calc_diff(grad_weight_embed.cpu(), grad_we_ref.cpu())
79+ assert diff_we < 1e-10, f'grad_we mismatch: {diff_we:.6e}'
88 80 
89 81 
90@pytest.mark.benchmark82@pytest.mark.benchmark
91@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)83@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)
92-def test_engram_grad_w_reduce_benchmark(benchmark_timer, benchmark_record, params):84+def test_engram_grad_w_reduce_benchmark(benchmark_timer, benchmark_record, params, monkeypatch):
85+ hidden_size = params['hidden']
86+ device = get_device()
87+ 
88+ _set_num_sms(monkeypatch, params['num_sms'])
93 grad_w_partial, weight_hidden, weight_embed = generate_test_data(params)89 grad_w_partial, weight_hidden, weight_embed = generate_test_data(params)
94- output_shape = weight_hidden.shape90+ hc_mult = grad_w_partial.shape[1]
95- device = weight_hidden.device91+ grad_weight_hidden = torch.randn(hc_mult, hidden_size, dtype=torch.float32, device=device)
96- grad_weight_hidden = torch.randn(output_shape, dtype=torch.float32, device=device)92+ grad_weight_embed = torch.randn(hc_mult, hidden_size, dtype=torch.float32, device=device)
97- grad_weight_embed = torch.randn(output_shape, dtype=torch.float32, device=device)93+ 
98- time_us = benchmark_timer(94+ t_us = benchmark_timer(
99- lambda: cann_ops_tilelang.engram_grad_w_reduce(95+ lambda: engram_grad_w_reduce(
100 grad_w_partial,96 grad_w_partial,
101 weight_hidden,97 weight_hidden,
102 weight_embed,98 weight_embed,
@@ -104,17 +100,13 @@ def test_engram_grad_w_reduce_benchmark(benchmark_timer, benchmark_record, param
104 grad_weight_embed,100 grad_weight_embed,
105 )101 )
106 )102 )
107- num_bytes = count_bytes(103+ 
108- grad_w_partial,104+ num_bytes = count_bytes(grad_w_partial, weight_hidden, weight_embed, grad_weight_hidden, grad_weight_embed)
109- weight_hidden,105+ bandwidth_gbs = num_bytes / t_us / 1e3
110- weight_embed,
111- grad_weight_hidden,
112- grad_weight_embed,
113- )
114 benchmark_record(106 benchmark_record(
115 kernel='engram_grad_w_reduce',107 kernel='engram_grad_w_reduce',
116 operation='fwd',108 operation='fwd',
117 params=params,109 params=params,
118- time_us=time_us,110+ time_us=t_us,
119- bandwidth_gbs=num_bytes / time_us / 1e3,111+ bandwidth_gbs=bandwidth_gbs,
120 )112 )
@@ -0,0 +1,105 @@
1+import os
2+ 
3+import pytest
4+import torch
5+ 
6+from cann_ops_tilelang import engram_hash
7+from cann_ops_tilelang.config import get_device
8+from tests.engram.engram_hash_ref import engram_hash_ref, make_offsets
9+from tests.testing.bench import make_param_id
10+from tests.testing.generator import generate_num_tokens, get_test_level
11+from tests.testing.numeric import assert_equal, count_bytes
12+ 
13+os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0'
14+pytestmark = pytest.mark.npu
15+ 
16+ 
17+def generate_test_data(params):
18+ num_tokens = params['num_tokens']
19+ max_ngram_size = params['ngram']
20+ num_ngram_layers = params['layers']
21+ num_embed_table_per_ngram = params['tables']
22+ device = get_device()
23+ ngram_token_ids = torch.randint(
24+ 0,
25+ 100000,
26+ (num_tokens, max_ngram_size),
27+ dtype=torch.int32,
28+ device=device,
29+ )
30+ multipliers = torch.randint(
31+ 0,
32+ 100000,
33+ (num_ngram_layers, max_ngram_size),
34+ dtype=torch.int64,
35+ device=device,
36+ )
37+ vocab_sizes = torch.randint(
38+ 100000,
39+ 1000000,
40+ (num_ngram_layers, max_ngram_size - 1, num_embed_table_per_ngram),
41+ dtype=torch.int32,
42+ device=device,
43+ )
44+ offsets = make_offsets(vocab_sizes)
45+ return ngram_token_ids, multipliers, vocab_sizes, offsets
46+ 
47+ 
48+def generate_test_params(level: int) -> list[dict]:
49+ return [{'num_tokens': t} for t in generate_num_tokens(level)]
50+ 
51+ 
52+@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id)
53+def test_engram_hash(params):
54+ num_tokens = params['num_tokens']
55+ max_ngram_size = 3
56+ num_ngram_layers = 2
57+ num_embed_table_per_ngram = 8
58+ 
59+ ngram_token_ids, multipliers, vocab_sizes, offsets = generate_test_data(
60+ {
61+ 'num_tokens': num_tokens,
62+ 'ngram': max_ngram_size,
63+ 'layers': num_ngram_layers,
64+ 'tables': num_embed_table_per_ngram,
65+ }
66+ )
67+ 
68+ output = engram_hash(ngram_token_ids, multipliers, vocab_sizes, offsets)
69+ output_ref = engram_hash_ref(ngram_token_ids, multipliers, vocab_sizes, offsets)
70+ assert_equal(output, output_ref)
71+ 
72+ 
73+@pytest.mark.benchmark
74+@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)
75+def test_engram_hash_benchmark(benchmark_timer, benchmark_record, params):
76+ max_ngram_size = 3
77+ num_ngram_layers = 2
78+ num_embed_table_per_ngram = 8
79+ 
80+ ngram_token_ids, multipliers, vocab_sizes, offsets = generate_test_data(
81+ {
82+ **params,
83+ 'ngram': max_ngram_size,
84+ 'layers': num_ngram_layers,
85+ 'tables': num_embed_table_per_ngram,
86+ }
87+ )
88+ output = engram_hash(ngram_token_ids, multipliers, vocab_sizes, offsets)
89+ 
90+ t_us = benchmark_timer(lambda: engram_hash(ngram_token_ids, multipliers, vocab_sizes, offsets))
91+ 
92+ num_bytes = count_bytes(ngram_token_ids, multipliers, vocab_sizes, offsets, output)
93+ bandwidth_gbs = num_bytes / t_us / 1e3
94+ benchmark_record(
95+ kernel='engram_hash',
96+ operation='fwd',
97+ params={
98+ **params,
99+ 'ngram': max_ngram_size,
100+ 'layers': num_ngram_layers,
101+ 'tables': num_embed_table_per_ngram,
102+ },
103+ time_us=t_us,
104+ bandwidth_gbs=bandwidth_gbs,
105+ )
@@ -0,0 +1,174 @@
1+import os
2+ 
3+import pytest
4+import torch
5+ 
6+from cann_ops_tilelang import (
7+ engram_sinkhorn_finalize,
8+ engram_sinkhorn_momentum_update,
9+ engram_sinkhorn_step,
10+)
11+from cann_ops_tilelang.config import get_device
12+from tests.engram.engram_sinkhorn_ref import (
13+ engram_sinkhorn_finalize_ref,
14+ engram_sinkhorn_momentum_update_ref,
15+ engram_sinkhorn_step_ref,
16+)
17+from tests.testing.bench import make_param_id
18+from tests.testing.generator import get_test_level
19+from tests.testing.numeric import calc_diff, count_bytes
20+ 
21+os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0'
22+pytestmark = pytest.mark.npu
23+ 
24+_DTYPES = {
25+ 'bf16': torch.bfloat16,
26+ 'fp32': torch.float32,
27+}
28+ 
29+ 
30+def generate_momentum_test_data(params):
31+ numel = params['numel']
32+ grad_dtype = _DTYPES[params['grad_dtype']]
33+ momentum_dtype = _DTYPES[params['momentum_dtype']]
34+ nesterov = params['nesterov']
35+ beta1 = params['beta1']
36+ 
37+ grad = torch.randn(numel, dtype=grad_dtype, device=get_device())
38+ momentum_buffer = torch.randn(numel, dtype=momentum_dtype, device=get_device())
39+ return grad, momentum_buffer, beta1, nesterov
40+ 
41+ 
42+def generate_momentum_test_params(level: int) -> list[dict]:
43+ sizes = [1001 * 256, 10001 * 256, 100001 * 512, 1000001 * 512]
44+ grad_dtypes = ('fp32',) if level == 0 else ('bf16', 'fp32')
45+ return [
46+ {
47+ 'numel': numel,
48+ 'grad_dtype': grad_dtype,
49+ 'momentum_dtype': momentum_dtype,
50+ 'nesterov': nesterov,
51+ 'beta1': beta1,
52+ }
53+ for numel in sizes
54+ for grad_dtype in grad_dtypes
55+ for momentum_dtype in ('bf16', 'fp32')
56+ for nesterov in (False, True)
57+ for beta1 in (0.95,)
58+ ]
59+ 
60+ 
61+@pytest.mark.parametrize('params', generate_momentum_test_params(get_test_level()), ids=make_param_id)
62+def test_engram_sinkhorn_momentum_update(params):
63+ grad, momentum_buffer, beta1, nesterov = generate_momentum_test_data(params)
64+ momentum_buffer_ref = momentum_buffer.clone()
65+ 
66+ sinkhorn_input_ref = engram_sinkhorn_momentum_update_ref(grad, momentum_buffer_ref, beta1, nesterov)
67+ sinkhorn_input = engram_sinkhorn_momentum_update(grad, momentum_buffer, beta1, nesterov)
68+ 
69+ diff_momentum = calc_diff(momentum_buffer, momentum_buffer_ref)
70+ diff_sinkhorn = calc_diff(sinkhorn_input, sinkhorn_input_ref)
71+ assert diff_momentum < 5e-8, f'momentum_buffer mismatch: {diff_momentum:.6e}'
72+ assert diff_sinkhorn < 5e-8, f'sinkhorn_input mismatch: {diff_sinkhorn:.6e}'
73+ 
74+ 
75+@pytest.mark.benchmark
76+@pytest.mark.parametrize('params', generate_momentum_test_params(0), ids=make_param_id)
77+def test_engram_sinkhorn_momentum_update_benchmark(benchmark_timer, benchmark_record, params):
78+ grad, momentum_buffer, beta1, nesterov = generate_momentum_test_data(params)
79+ sinkhorn_input = engram_sinkhorn_momentum_update(grad, momentum_buffer, beta1, nesterov)
80+ 
81+ time_us = benchmark_timer(lambda: engram_sinkhorn_momentum_update(grad, momentum_buffer, beta1, nesterov))
82+ 
83+ num_bytes = count_bytes(grad, momentum_buffer, momentum_buffer, sinkhorn_input)
84+ bandwidth_gbs = num_bytes / time_us / 1e3
85+ benchmark_record(
86+ kernel='engram_sinkhorn',
87+ operation='momentum_update',
88+ params=params,
89+ time_us=time_us,
90+ bandwidth_gbs=bandwidth_gbs,
91+ )
92+ 
93+ 
94+def generate_row_step_test_data(params):
95+ num_rows = params['num_rows']
96+ head_dim = params['head_dim']
97+ matrix = torch.randn(num_rows, head_dim, dtype=torch.float32, device=get_device())
98+ row_scale = torch.rand(num_rows, dtype=torch.float32, device=get_device())
99+ col_scale = torch.rand(head_dim, dtype=torch.float32, device=get_device())
100+ return matrix, row_scale, col_scale, params['eps']
101+ 
102+ 
103+def generate_row_step_test_params(level: int) -> list[dict]:
104+ return [
105+ {'num_rows': num_rows, 'head_dim': head_dim, 'eps': eps}
106+ for num_rows in [1001, 10001, 100001]
107+ for head_dim in [256, 5120]
108+ for eps in (1e-20,)
109+ ]
110+ 
111+ 
112+@pytest.mark.parametrize('params', generate_row_step_test_params(get_test_level()), ids=make_param_id)
113+def test_engram_sinkhorn_step(params):
114+ matrix, row_scale, col_scale, eps = generate_row_step_test_data(params)
115+ row_scale_ref = row_scale.clone()
116+ 
117+ row_norm_ref, col_sumsq_ref = engram_sinkhorn_step_ref(matrix, row_scale_ref, col_scale, eps)
118+ row_norm, col_sumsq = engram_sinkhorn_step(matrix, row_scale, col_scale, eps)
119+ 
120+ assert calc_diff(row_norm, row_norm_ref) < 1e-10
121+ assert calc_diff(row_scale, row_scale_ref) < 1e-10
122+ assert calc_diff(col_sumsq, col_sumsq_ref) < 1e-10
123+ 
124+ 
125+@pytest.mark.parametrize('params', generate_row_step_test_params(get_test_level()), ids=make_param_id)
126+def test_engram_sinkhorn_finalize(params):
127+ matrix, row_scale, col_scale, eps = generate_row_step_test_data(params)
128+ alpha = params['head_dim'] ** 0.5
129+ 
130+ out_ref = engram_sinkhorn_finalize_ref(matrix, row_scale, col_scale, eps, alpha)
131+ out = engram_sinkhorn_finalize(matrix, row_scale, col_scale, eps, alpha)
132+ 
133+ assert calc_diff(out, out_ref) < 1e-10
134+ 
135+ 
136+@pytest.mark.benchmark
137+@pytest.mark.parametrize('params', generate_row_step_test_params(0), ids=make_param_id)
138+def test_engram_sinkhorn_step_benchmark(benchmark_timer, benchmark_record, params):
139+ matrix, row_scale, col_scale, eps = generate_row_step_test_data(params)
140+ 
141+ row_norm, col_sumsq = engram_sinkhorn_step(matrix, row_scale, col_scale, eps)
142+ 
143+ time_us = benchmark_timer(lambda: engram_sinkhorn_step(matrix, row_scale, col_scale, eps))
144+ 
145+ num_bytes = count_bytes(matrix, row_scale, col_scale, row_scale, row_norm, col_sumsq)
146+ bandwidth_gbs = num_bytes / time_us / 1e3
147+ benchmark_record(
148+ kernel='engram_sinkhorn',
149+ operation='step',
150+ params=params,
151+ time_us=time_us,
152+ bandwidth_gbs=bandwidth_gbs,
153+ )
154+ 
155+ 
156+@pytest.mark.benchmark
157+@pytest.mark.parametrize('params', generate_row_step_test_params(0), ids=make_param_id)
158+def test_engram_sinkhorn_finalize_benchmark(benchmark_timer, benchmark_record, params):
159+ matrix, row_scale, col_scale, eps = generate_row_step_test_data(params)
160+ alpha = params['head_dim'] ** 0.5
161+ out = torch.empty_like(matrix)
162+ engram_sinkhorn_finalize(matrix, row_scale, col_scale, eps, alpha, out)
163+ 
164+ time_us = benchmark_timer(lambda: engram_sinkhorn_finalize(matrix, row_scale, col_scale, eps, alpha, out))
165+ 
166+ num_bytes = count_bytes(matrix, row_scale, col_scale, out)
167+ bandwidth_gbs = num_bytes / time_us / 1e3
168+ benchmark_record(
169+ kernel='engram_sinkhorn',
170+ operation='finalize',
171+ params=params,
172+ time_us=time_us,
173+ bandwidth_gbs=bandwidth_gbs,
174+ )
@@ -1,165 +0,0 @@
1-import os
2- 
3-import pytest
4-import torch
5- 
6-import cann_ops_tilelang
7-from cann_ops_tilelang.config import get_device
8-from tests.engram.engram_sinkhorn_step_ref import engram_sinkhorn_step_ref
9-from tests.testing.bench import make_param_id
10-from tests.testing.generator import get_test_level
11-from tests.testing.numeric import calc_diff, count_bytes
12- 
13-os.environ['TILELANG_PRINT_ON_COMPILATION'] = '0'
14-pytestmark = pytest.mark.npu
15- 
16- 
17-def generate_test_params(level: int) -> list[dict]:
18- source_cases = [
19- {'num_rows': num_rows, 'head_dim': head_dim, 'eps': 1e-20}
20- for num_rows in (1001, 10001, 100001)
21- for head_dim in (256, 5120)
22- ]
23- if level == 0:
24- return [source_cases[0], source_cases[3]]
25- if level == 1:
26- return source_cases
27- return [
28- {'num_rows': 0, 'head_dim': 256, 'eps': 1e-20},
29- {'num_rows': 1, 'head_dim': 256, 'eps': 0.0},
30- {'num_rows': 17, 'head_dim': 512, 'eps': 1e-6},
31- *source_cases,
32- ]
33- 
34- 
35-def generate_test_data(params):
36- device = get_device()
37- matrix = torch.randn(
38- params['num_rows'],
39- params['head_dim'],
40- dtype=torch.float32,
41- device=device,
42- )
43- row_scale = torch.rand(params['num_rows'], dtype=torch.float32, device=device)
44- col_scale = torch.rand(params['head_dim'], dtype=torch.float32, device=device)
45- return matrix, row_scale, col_scale
46- 
47- 
48-@pytest.mark.parametrize('params', generate_test_params(get_test_level()), ids=make_param_id)
49-def test_engram_sinkhorn_step(params):
50- matrix, row_scale, col_scale = generate_test_data(params)
51- row_scale_ref = row_scale.clone()
52- 
53- row_norm_ref, col_sumsq_ref = engram_sinkhorn_step_ref(
54- matrix,
55- row_scale_ref,
56- col_scale,
57- params['eps'],
58- )
59- row_norm, col_sumsq = cann_ops_tilelang.engram_sinkhorn_step(
60- matrix,
61- row_scale,
62- col_scale,
63- params['eps'],
64- )
65- 
66- assert row_norm.shape == row_scale.shape
67- assert col_sumsq.shape == col_scale.shape
68- assert row_norm.dtype == col_sumsq.dtype == torch.float32
69- assert row_norm.device == col_sumsq.device == matrix.device
70- assert calc_diff(row_norm, row_norm_ref) < 1e-10
71- assert calc_diff(row_scale, row_scale_ref) < 1e-10
72- assert calc_diff(col_sumsq, col_sumsq_ref) < 1e-10
73- 
74- 
75-def test_engram_sinkhorn_step_empty_rows():
76- params = {'num_rows': 0, 'head_dim': 256, 'eps': 1e-20}
77- matrix, row_scale, col_scale = generate_test_data(params)
78- 
79- row_norm, col_sumsq = cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale, col_scale, params['eps'])
80- 
81- assert row_norm.shape == (0,)
82- assert torch.count_nonzero(col_sumsq).item() == 0
83- 
84- 
85-def test_engram_sinkhorn_step_rejects_invalid_inputs():
86- device = get_device()
87- matrix = torch.randn((17, 256), dtype=torch.float32, device=device)
88- row_scale = torch.rand((17,), dtype=torch.float32, device=device)
89- col_scale = torch.rand((256,), dtype=torch.float32, device=device)
90- 
91- with pytest.raises(TypeError, match='torch.Tensor'):
92- cann_ops_tilelang.engram_sinkhorn_step(None, row_scale, col_scale, 1e-20)
93- with pytest.raises(ValueError, match='NPU tensor'):
94- cann_ops_tilelang.engram_sinkhorn_step(matrix.cpu(), row_scale, col_scale, 1e-20)
95- with pytest.raises(ValueError, match='NPU tensor'):
96- cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale.cpu(), col_scale, 1e-20)
97- with pytest.raises(TypeError, match='torch.float32'):
98- cann_ops_tilelang.engram_sinkhorn_step(matrix.bfloat16(), row_scale, col_scale, 1e-20)
99- with pytest.raises(TypeError, match='torch.float32'):
100- cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale, col_scale.bfloat16(), 1e-20)
101- with pytest.raises(ValueError, match='shape'):
102- cann_ops_tilelang.engram_sinkhorn_step(matrix.view(-1), row_scale, col_scale, 1e-20)
103- with pytest.raises(ValueError, match='row_scale'):
104- cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale[:-1], col_scale, 1e-20)
105- with pytest.raises(ValueError, match='col_scale'):
106- cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale, col_scale[:-1], 1e-20)
107- with pytest.raises(ValueError, match='multiple of 256'):
108- cann_ops_tilelang.engram_sinkhorn_step(matrix[:, :-1].contiguous(), row_scale, col_scale[:-1], 1e-20)
109- wide_matrix = torch.randn((1, 6144), dtype=torch.float32, device=device)
110- with pytest.raises(ValueError, match='shared UB capacity'):
111- cann_ops_tilelang.engram_sinkhorn_step(
112- wide_matrix,
113- torch.rand((1,), device=device),
114- torch.rand((6144,), device=device),
115- 1e-20,
116- )
117- with pytest.raises(ValueError, match='contiguous'):
118- cann_ops_tilelang.engram_sinkhorn_step(matrix.T, torch.rand(256, device=device), row_scale, 1e-20)
119- with pytest.raises(ValueError, match='contiguous'):
120- non_contiguous_row_scale = torch.rand((17, 2), device=device)[:, 0]
121- cann_ops_tilelang.engram_sinkhorn_step(matrix, non_contiguous_row_scale, col_scale, 1e-20)
122- with pytest.raises(ValueError, match='contiguous'):
123- non_contiguous_col_scale = torch.rand((256, 2), device=device)[:, 0]
124- cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale, non_contiguous_col_scale, 1e-20)
125- with pytest.raises(TypeError, match='real number'):
126- cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale, col_scale, None)
127- with pytest.raises(ValueError, match='finite and non-negative'):
128- cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale, col_scale, -1.0)
129- for invalid_eps in (float('inf'), float('nan')):
130- with pytest.raises(ValueError, match='finite and non-negative'):
131- cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale, col_scale, invalid_eps)
132- 
133- 
134-def test_engram_sinkhorn_step_rejects_input_aliases():
135- device = get_device()
136- col_scale = torch.rand((256,), dtype=torch.float32, device=device)
137- storage = torch.empty((17 * 256,), dtype=torch.float32, device=device)
138- aliased_matrix = storage.view(17, 256)
139- aliased_row_scale = storage[:17]
140- with pytest.raises(ValueError, match='must not share storage'):
141- cann_ops_tilelang.engram_sinkhorn_step(aliased_matrix, aliased_row_scale, col_scale, 1e-20)
142- 
143- 
144-@pytest.mark.benchmark
145-@pytest.mark.parametrize('params', generate_test_params(0), ids=make_param_id)
146-def test_engram_sinkhorn_step_benchmark(benchmark_timer, benchmark_record, params):
147- matrix, row_scale, col_scale = generate_test_data(params)
148- row_norm, col_sumsq = cann_ops_tilelang.engram_sinkhorn_step(
149- matrix,
150- row_scale,
151- col_scale,
152- params['eps'],
153- )
154- 
155- time_us = benchmark_timer(
156- lambda: cann_ops_tilelang.engram_sinkhorn_step(matrix, row_scale, col_scale, params['eps'])
157- )
158- num_bytes = count_bytes(matrix, row_scale, col_scale, row_scale, row_norm, col_sumsq)
159- benchmark_record(
160- kernel='engram_sinkhorn_step',
161- operation='step',
162- params=params,
163- time_us=time_us,
164- bandwidth_gbs=num_bytes / time_us / 1e3,
165- )