Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion mojo_opset/backends/ttx/kernels/npu/a2/quant.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import torch
import triton
import triton.language as tl
import triton.language.extra.cann.libdevice as libdevice

from mojo_opset.backends.ttx.kernels.npu.utils import get_num_cores

Expand Down Expand Up @@ -150,7 +151,8 @@ def scale_dynamic_quant_kernel(
else:
scaled_vals = input_vals
quant_vals = scaled_vals / current_quant_scale[:, None]
quant_vals = tl.where(quant_vals < 0, quant_vals - 0.5, quant_vals + 0.5)
# Round-half-to-even (banker's rounding) to match torch.round.
quant_vals = libdevice.rint(quant_vals)
quant_vals_int8 = tl.cast(quant_vals, dtype=tl.int8)

tl.store(output_ptr, quant_vals_int8, mask=block_mask)
29 changes: 18 additions & 11 deletions mojo_opset/experimental/operators/indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,24 @@ def forward(
device=query.device,
)

for batch_id in range(batch_size):
key_batch = key[batch_id].to(torch.float32) # [N, K]
key_scale_batch = key_scale[batch_id] # [N]

for i in range(q_seq_len):
q_slice = query[batch_id, i].to(torch.float32) # [H, K]
dot_product = torch.matmul(q_slice, key_batch.transpose(0, 1)) # [H, N]
relu_out = torch.maximum(dot_product, torch.tensor(0.0))
q_scale_slice = query_scale[batch_id, i].unsqueeze(-1) # [H, 1]
scaled_out = relu_out * q_scale_slice
index_score[batch_id, i] = torch.sum(scaled_out, dim=0) * key_scale_batch
# Chunked over M with a single reused workspace for the [Mc, H, N] fp32
# matmul result. Everything runs under no_grad: inputs may carry grad
# (e.g. scales computed from trainable projections), and an autograd
# graph over B*M chunks would pin every intermediate (OOM at 32x4096).
chunk = max(1, min(q_seq_len, 2**28 // max(1, head_num * k_seq_len)))
with torch.no_grad():
dot_buf = torch.empty((chunk, head_num, k_seq_len), dtype=torch.float32, device=query.device)
for batch_id in range(batch_size):
key_batch = key[batch_id].to(torch.float32) # [N, K]
key_scale_batch = key_scale[batch_id] # [N]
q_batch = query[batch_id].to(torch.float32) # [M, H, K]

for m0 in range(0, q_seq_len, chunk):
m1 = min(m0 + chunk, q_seq_len)
dot_product = torch.matmul(q_batch[m0:m1], key_batch.transpose(0, 1), out=dot_buf[: m1 - m0]) # [Mc, H, N]
dot_product.relu_()
dot_product.mul_(query_scale[batch_id, m0:m1].unsqueeze(-1)) # [Mc, H, 1]
index_score[batch_id, m0:m1] = dot_product.sum(dim=1).mul_(key_scale_batch)

return index_score

Expand Down
58 changes: 40 additions & 18 deletions mojo_opset/tests/accuracy/operators/test_indexer.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import gc

import pytest
import torch

from mojo_opset.experimental import MojoLightningIndexer
from mojo_opset.experimental import MojoIndexer
from mojo_opset.utils.acc import check_tol_diff
from mojo_opset.utils.platform import get_torch_device
from mojo_opset.tests.utils import auto_switch_platform, bypass_not_implemented

Expand Down Expand Up @@ -51,13 +54,8 @@ def test_lightning_indexer(B, M, N, H, K, dtype):
@auto_switch_platform()
@bypass_not_implemented
def test_indexer(batch, q_seq_len, head_dim, dim, q_lora_rank, dtype):
torch.manual_seed(42)
device = get_torch_device()
map_tol = {
"bfloat16": (1.6e-2, 1e-5, 1.0),
"float16": (1e-3, 1e-5, 1.0),
"float32": (1.3e-6, 1e-5, 1.0),
}
atol, rtol, ptol = map_tol[dtype]
dtype = dtype_str_map[dtype]

rope_head_dim = 32
Expand All @@ -69,7 +67,16 @@ def test_indexer(batch, q_seq_len, head_dim, dim, q_lora_rank, dtype):
topk = 2048 if q_seq_len >= 4096 else q_seq_len // 2
freqs_cis = precompute_freqs_cis(q_seq_len, rope_head_dim, device=device)

init_kwargs = dict(n_heads=n_heads, head_dim=head_dim, qk_rope_head_dim=rope_head_dim, topk=topk)
init_kwargs = dict(
n_heads=n_heads,
head_dim=head_dim,
qk_rope_head_dim=rope_head_dim,
topk=topk,
# size the k_cache buffers to this case instead of the 128x32768
# defaults (~284MB per instance), to bound memory over the sweep
max_batch_size=batch,
max_seq_len=q_seq_len,
)

indexer_ref = MojoIndexer._registry.get("torch")(**init_kwargs)
indexer_ref.to(dtype=dtype, device=device)
Expand All @@ -91,17 +98,32 @@ def test_indexer(batch, q_seq_len, head_dim, dim, q_lora_rank, dtype):
indexer.to(dtype=dtype, device=device)
indexer.load_state_dict(indexer_ref.state_dict(), strict=False)

indexer.forward_diff_with(
indexer_ref,
x,
query_scale,
start_pos,
freqs_cis,
None,
atol=atol,
rtol=rtol,
ptol=ptol,
)
# NOTE: index_score comes from an int8-quantized pipeline. One quantization
# step is ~1/127 (~0.8%) of the per-token max abs value, and upstream bf16
# ulp differences between two implementations flip a sparse set of int8
# values, shifting scores by O(1-100) on |score|~1e4. Comparing the integer
# topk indices element-wise would require bit-identical score rankings and
# is not achievable across implementations, so compare score and selection
# separately with quantization-step-scale tolerances.
res_indices, res_score = indexer.forward(x, query_scale, start_pos, freqs_cis, None)
ref_indices, ref_score = indexer_ref.forward(x, query_scale, start_pos, freqs_cis, None)

# 1) score check with int8-quantization-step-scale tolerances.
check_tol_diff(res_score, ref_score, atol=1e-2, rtol=2e-2, ptol=0.98)

# 2) topk selection check, order-invariant: the selected indices must score
# (under the reference's own scoring) as high as the reference selection.
if ref_indices.numel() > 0:
ref_sorted = ref_score.gather(-1, ref_indices).sort(dim=-1, descending=True).values
res_sorted = ref_score.gather(-1, res_indices).sort(dim=-1, descending=True).values
check_tol_diff(res_sorted, ref_sorted, atol=1.0, rtol=1e-2, ptol=0.999)

# release the big tensors deterministically before the next parametrized case
del res_indices, res_score, ref_indices, ref_score, x, query_scale, indexer, indexer_ref
gc.collect()
empty_cache = getattr(getattr(torch, device, None), "empty_cache", None)
if empty_cache is not None:
empty_cache()


def precompute_freqs_cis(seqlen, dim, device) -> torch.Tensor:
Expand Down
Loading