from collections.abc import Callable
from typing import Any

import pytest
import torch

from cann_ops_tilelang.attention.cache import (
    load_index_kv_cache,
    load_kv_cache,
    load_kv_cache_llama,
    store_index_kv_cache,
    store_kv_cache,
    store_kv_cache_llama,
)
from cann_ops_tilelang.config import get_device, get_num_vector_cores
from cann_ops_tilelang.quant import per_token_cast
from tests.quant.cast_ref import cast as torch_cast

pytestmark = pytest.mark.npu


@pytest.fixture(autouse=True)
def _restore_default_device():
    """Keep cache tests from changing tensor placement in other test modules."""
    yield
    torch.set_default_device('cpu')


@pytest.mark.parametrize('use_fp8_rope,num_per_channels', [(False, 64), (True, 32)])
@pytest.mark.parametrize('block_size,num_blocks,n', [(64, 2333, 65536), (32, 4666, 65536), (129, 2333, 65536)])
def test_kvcache(block_size: int, num_blocks: int, n: int, use_fp8_rope: bool, num_per_channels: int) -> None:
    assert num_blocks * block_size > n
    torch.set_default_device('npu')
    if not use_fp8_rope:
        bytes_per_token = 448 + 64 * 2 + 8
        alignment_size = 576
    else:
        bytes_per_token = 512 + 16
        alignment_size = 512
    kv_cache_byte_per_block = (block_size * bytes_per_token + alignment_size - 1) // alignment_size * alignment_size
    kv_cache = torch.empty((num_blocks, kv_cache_byte_per_block), dtype=torch.uint8)
    kv_cache = kv_cache[:, : block_size * bytes_per_token].view(num_blocks, block_size, 1, bytes_per_token)
    slot_mapping = torch.randperm(num_blocks * block_size, dtype=torch.int32)[:n]
    store_kv = torch.randn((n, 1, 512), dtype=torch.bfloat16)
    store_kv_cache(store_kv, kv_cache, slot_mapping, use_fp8_rope=use_fp8_rope, num_per_channels=num_per_channels)
    cache_flat = kv_cache.view(num_blocks, block_size * bytes_per_token)
    cache_block = slot_mapping // block_size
    cache_offset = slot_mapping % block_size
    ref_quant = per_token_cast(store_kv[:, 0, :], 'e4m3', num_per_channels, round_sf=True, use_packed_ue8m0=True)
    ref_fp8, ref_sf = ref_quant
    ref_sf_u8 = ref_sf.view(torch.uint8)
    if not use_fp8_rope:
        cache_value = cache_flat[:, : block_size * (448 + 64 * 2)].view(num_blocks, block_size, 448 + 64 * 2)
        cache_fp8_u8 = cache_value[:, :, :448]
        cache_bf16_u8 = cache_value[:, :, 448:]
        cache_bf16 = cache_bf16_u8.view(*cache_bf16_u8.shape[:-1], -1).view(torch.bfloat16)
        cache_sf = cache_flat[:, block_size * (448 + 64 * 2) :].view(num_blocks, block_size, 8)
        assert torch.equal(cache_fp8_u8[cache_block, cache_offset], ref_fp8[:, :448].view(torch.uint8))
        assert torch.equal(cache_bf16[cache_block, cache_offset], store_kv[:, 0, 448:])
        assert torch.equal(cache_sf[cache_block, cache_offset], ref_sf_u8)
    else:
        cache_fp8_u8 = cache_flat[:, : block_size * 512].view(num_blocks, block_size, 512)
        cache_sf = cache_flat[:, block_size * 512 :].view(num_blocks, block_size, 16)
        assert torch.equal(cache_fp8_u8[cache_block, cache_offset], ref_fp8.view(torch.uint8))
        assert torch.equal(cache_sf[cache_block, cache_offset], ref_sf_u8)
    load_kv = load_kv_cache(kv_cache, slot_mapping, use_fp8_rope=use_fp8_rope, num_per_channels=num_per_channels)
    ref_fp8_cpu = ref_fp8.cpu().float().view(n, 512 // num_per_channels, num_per_channels)
    ref_scale_bits_cpu = ref_sf_u8.cpu().to(torch.int32) << 23
    ref_scale_cpu = ref_scale_bits_cpu.view(torch.float32)
    ref_load_kv = (ref_fp8_cpu * ref_scale_cpu.unsqueeze(-1)).to(torch.bfloat16).view(n, 1, 512)
    load_kv_cpu = load_kv.cpu()
    if not use_fp8_rope:
        assert torch.equal(ref_load_kv[:, :, :448], load_kv_cpu[:, :, :448])
        assert torch.equal(store_kv[:, :, 448:].cpu(), load_kv_cpu[:, :, 448:])
    else:
        assert torch.equal(ref_load_kv, load_kv_cpu)


@pytest.mark.parametrize('mode', ['fp8', 'mxfp8', 'fp4'])
@pytest.mark.parametrize('block_size,num_blocks,n', [(64, 2333, 65536), (32, 4666, 65536)])
def test_index_kvcache(block_size: int, num_blocks: int, n: int, mode: str) -> None:
    assert num_blocks * block_size > n
    torch.set_default_device('npu')
    is_fp4 = mode == 'fp4'
    is_mxfp8 = mode == 'mxfp8'
    bytes_per_token = 64 + 4 if is_fp4 else 128 + 4
    kv_cache = torch.empty((num_blocks, block_size, 1, bytes_per_token), dtype=torch.uint8)
    slot_mapping = torch.randperm(num_blocks * block_size, dtype=torch.int32)[:n]
    store_kv = torch.randn((n, 1, 128), dtype=torch.bfloat16)
    store_index_kv_cache(store_kv, kv_cache, slot_mapping, is_fp4=is_fp4, is_mxfp8=is_mxfp8)
    load_kv = load_index_kv_cache(kv_cache, slot_mapping, is_fp4=is_fp4, is_mxfp8=is_mxfp8)
    if mode == 'fp8':
        ref = per_token_cast(store_kv.contiguous().view(n, 128), 'e4m3', 128, round_sf=True)
        ref[1].squeeze_(-1)
        assert torch.equal(ref[0].view(torch.uint8).cpu(), load_kv[0].view(torch.uint8).cpu())
        assert torch.equal(ref[1].cpu(), load_kv[1].cpu())
    elif mode == 'mxfp8':
        ref = per_token_cast(
            store_kv[:, 0, :], 'e4m3', 32, round_sf=True, use_tma_aligned_col_major_sf=False, use_packed_ue8m0=True
        )
        assert torch.equal(ref[0].view(torch.uint8).cpu(), load_kv[0].view(torch.uint8).cpu())
        assert torch.equal(ref[1].contiguous().view(torch.uint8).flatten().cpu(), load_kv[1].view(torch.uint8).cpu())
    else:
        ref = per_token_cast(
            store_kv[:, 0, :], 'e2m1', 32, round_sf=True, use_tma_aligned_col_major_sf=True, use_packed_ue8m0=True
        )
        assert torch.equal(ref[0].view(torch.uint8).cpu(), load_kv[0].view(torch.uint8).cpu())
        assert torch.equal(ref[1].contiguous().view(torch.uint8).flatten().cpu(), load_kv[1].view(torch.uint8).cpu())


@pytest.mark.parametrize('mode', ['fp8', 'mxfp8', 'fp4'])
@pytest.mark.parametrize(
    'block_size,num_blocks,n',
    [
        pytest.param(8, 4, 19, id='small-block-tail'),
        pytest.param(32, 2, 33, id='block-cross-boundary'),
        pytest.param(32, 9, 257, id='persistent-multi-wave'),
    ],
)
def test_store_index_kvcache_ascend(block_size: int, num_blocks: int, n: int, mode: str) -> None:
    """Validate quantization, cache layout, slot scatter, and padding writes."""
    capacity = num_blocks * block_size
    assert n <= capacity
    is_fp4 = mode == 'fp4'
    is_mxfp8 = mode == 'mxfp8'
    data_bytes = 64 if is_fp4 else 128
    bytes_per_token = data_bytes + 4
    fmt = 'e2m1' if is_fp4 else 'e4m3'
    group_size = 32 if is_fp4 or is_mxfp8 else 128
    cpu_generator = torch.Generator().manual_seed(20240728)
    store_kv_cpu = torch.randn((n, 1, 128), dtype=torch.bfloat16, generator=cpu_generator, device='cpu')
    store_kv_cpu[0].zero_()
    store_kv_cpu[1, 0] = torch.linspace(-512, 512, 128, dtype=torch.bfloat16, device='cpu')
    store_kv = store_kv_cpu.to('npu')
    slot_mapping_cpu = torch.randperm(capacity, generator=cpu_generator, dtype=torch.int32, device='cpu')[:n]
    slot_mapping_cpu[2::7] = -1
    slot_mapping = slot_mapping_cpu.to('npu')
    sentinel = 165
    kv_cache = torch.full((num_blocks, block_size, 1, bytes_per_token), sentinel, dtype=torch.uint8, device='npu')
    store_index_kv_cache(store_kv, kv_cache, slot_mapping, is_fp4=is_fp4, is_mxfp8=is_mxfp8)
    ref_data, ref_sf = torch_cast(
        store_kv[:, 0, :],
        fmt,
        block_size=(1, group_size),
        round_sf=True,
        use_packed_ue8m0=is_fp4 or is_mxfp8,
    )
    torch.npu.synchronize()
    ref_data_bytes = ref_data.contiguous().view(torch.uint8).cpu().view(n, data_bytes)
    ref_sf_bytes = ref_sf.contiguous().view(torch.uint8).cpu().view(n, 4)
    expected = torch.full((num_blocks, block_size * bytes_per_token), sentinel, dtype=torch.uint8, device='cpu')
    for token_idx, slot_idx in enumerate(slot_mapping_cpu.tolist()):
        if slot_idx == -1:
            continue
        cache_block = slot_idx // block_size
        cache_offset = slot_idx % block_size
        data_begin = cache_offset * data_bytes
        sf_begin = block_size * data_bytes + cache_offset * 4
        expected[cache_block, data_begin : data_begin + data_bytes] = ref_data_bytes[token_idx]
        expected[cache_block, sf_begin : sf_begin + 4] = ref_sf_bytes[token_idx]
    actual = kv_cache.cpu().view(num_blocks, block_size * bytes_per_token)
    mismatch = torch.nonzero(actual != expected)
    assert mismatch.numel() == 0, (
        f'{mode} cache bytes differ at {mismatch[:8].tolist()}; '
        'this includes untouched slots and slot_mapping=-1 validation'
    )


@pytest.mark.parametrize('mode', ['fp8', 'mxfp8', 'fp4'])
@pytest.mark.parametrize(
    'block_size,num_blocks,n,padded_block_stride',
    [
        pytest.param(8, 4, 19, False, id='small-contiguous-tail'),
        pytest.param(32, 2, 33, True, id='block-cross-boundary'),
        pytest.param(32, 9, 257, True, id='persistent-multi-wave'),
        pytest.param(64, 2, 65, False, id='block-size-64'),
    ],
)
def test_load_index_kvcache_ascend(
    block_size: int, num_blocks: int, n: int, mode: str, padded_block_stride: bool
) -> None:
    """Validate byte-exact gathers, padded block strides, duplicate slots, and -1."""
    capacity = num_blocks * block_size
    assert n <= capacity
    is_fp4 = mode == 'fp4'
    is_mxfp8 = mode == 'mxfp8'
    data_bytes = 64 if is_fp4 else 128
    bytes_per_token = data_bytes + 4
    logical_block_bytes = block_size * bytes_per_token
    block_stride = (logical_block_bytes + 575) // 576 * 576 if padded_block_stride else logical_block_bytes
    cache_bytes = torch.arange(num_blocks * block_stride, dtype=torch.int64, device='cpu')
    cache_bytes = ((cache_bytes * 73 + 19) % 256).to(torch.uint8).view(num_blocks, block_stride)
    cache_storage = cache_bytes.to('npu')
    kv_cache = cache_storage[:, :logical_block_bytes].view(num_blocks, block_size, 1, bytes_per_token)
    assert kv_cache.stride(0) == block_stride
    assert (block_stride > logical_block_bytes) == padded_block_stride
    slot_mapping_cpu = ((torch.arange(n, dtype=torch.int32, device='cpu') * 17 + 3) % capacity).to(torch.int32)
    slot_mapping_cpu[0] = -1
    slot_mapping_cpu[2] = capacity - 1
    slot_mapping_cpu[3] = slot_mapping_cpu[1]
    slot_mapping_cpu[5::7] = -1
    slot_mapping_cpu[-1] = -1
    slot_mapping = slot_mapping_cpu.to('npu')
    loaded_data, loaded_sf = load_index_kv_cache(kv_cache, slot_mapping, is_fp4=is_fp4, is_mxfp8=is_mxfp8)
    torch.npu.synchronize()
    expected_data_dtype = torch.int8 if is_fp4 else torch.float8_e4m3fn
    expected_sf_dtype = torch.int32 if is_fp4 or is_mxfp8 else torch.float32
    assert loaded_data.shape == (n, data_bytes)
    assert loaded_data.dtype == expected_data_dtype
    assert loaded_sf.shape == (n,)
    assert loaded_sf.dtype == expected_sf_dtype
    expected_data = torch.zeros((n, data_bytes), dtype=torch.uint8, device='cpu')
    expected_sf = torch.zeros((n, 4), dtype=torch.uint8, device='cpu')
    logical_cache = cache_bytes[:, :logical_block_bytes]
    for token_idx, slot_idx in enumerate(slot_mapping_cpu.tolist()):
        if slot_idx == -1:
            continue
        cache_block = slot_idx // block_size
        cache_offset = slot_idx % block_size
        data_begin = cache_offset * data_bytes
        sf_begin = block_size * data_bytes + cache_offset * 4
        expected_data[token_idx] = logical_cache[cache_block, data_begin : data_begin + data_bytes]
        expected_sf[token_idx] = logical_cache[cache_block, sf_begin : sf_begin + 4]
    actual_data = loaded_data.view(torch.uint8).cpu().view(n, data_bytes)
    actual_sf = loaded_sf.view(torch.uint8).cpu().view(n, 4)
    assert torch.equal(actual_data, expected_data)
    assert torch.equal(actual_sf, expected_sf)


@pytest.mark.parametrize(
    'block_size,num_blocks,n_spec,n_head_kv,input_padding,cache_padding',
    [
        pytest.param(128, 2, 0, 2, 16, 32, id='empty-input'),
        pytest.param(8, 4, 19, 1, 0, 0, id='single-head-small-tail'),
        pytest.param(32, 2, 33, 4, 16, 32, id='multi-head-cross-block-padded-strides'),
        pytest.param(128, 4, 'all-aiv-cores', 2, 16, 32, id='all-aiv-cores-one-token'),
        pytest.param(128, 4, 'front-aiv-core-extra-token', 2, 16, 32, id='front-aiv-core-extra-token'),
        pytest.param(128, 16, 'simt-second-wave', 8, 32, 64, id='simt-second-wave'),
    ],
)
def test_store_kv_cache_llama_ascend(
    block_size: int, num_blocks: int, n_spec: int | str, n_head_kv: int, input_padding: int, cache_padding: int
) -> None:
    """Validate exact scatter, -1 skips, dynamic strides, and SIMT tails."""
    if isinstance(n_spec, str):
        num_vec_cores = get_num_vector_cores()
        n = {
            'all-aiv-cores': num_vec_cores,
            'front-aiv-core-extra-token': num_vec_cores + 1,
            'simt-second-wave': num_vec_cores * 8 + 1,
        }[n_spec]
    else:
        n = n_spec
    capacity = num_blocks * block_size
    element_per_token = n_head_kv * 128
    logical_block_elements = block_size * element_per_token
    assert n <= capacity
    cpu_generator = torch.Generator().manual_seed(20240731)
    store_storage_cpu = torch.full((n, element_per_token + input_padding), -123.0, dtype=torch.bfloat16, device='cpu')
    store_values_cpu = torch.randn((n, element_per_token), dtype=torch.bfloat16, generator=cpu_generator, device='cpu')
    store_storage_cpu[:, :element_per_token] = store_values_cpu
    store_storage = store_storage_cpu.to('npu')
    store_kv = store_storage[:, :element_per_token].view(n, n_head_kv, 128)
    sentinel = -77.0
    cache_storage = torch.full(
        (num_blocks, logical_block_elements + cache_padding), sentinel, dtype=torch.bfloat16, device='npu'
    )
    kv_cache = cache_storage[:, :logical_block_elements].view(num_blocks, block_size, n_head_kv, 128)
    if n > 0:
        assert store_kv.stride(0) == element_per_token + input_padding
    assert kv_cache.stride(0) == logical_block_elements + cache_padding
    slot_mapping_cpu = torch.randperm(capacity, generator=cpu_generator, dtype=torch.int32, device='cpu')[:n]
    if n > 0:
        slot_mapping_cpu[1::6] = -1
        slot_mapping_cpu[-1] = -1
    slot_mapping = slot_mapping_cpu.to('npu')
    store_kv_cache_llama(store_kv, kv_cache, slot_mapping)
    torch.npu.synchronize()
    expected = torch.full(
        (num_blocks, logical_block_elements + cache_padding), sentinel, dtype=torch.bfloat16, device='cpu'
    )
    expected_cache = expected[:, :logical_block_elements].view(num_blocks, block_size, n_head_kv, 128)
    store_values = store_values_cpu.view(n, n_head_kv, 128)
    for token_idx, slot_idx in enumerate(slot_mapping_cpu.tolist()):
        if slot_idx == -1:
            continue
        expected_cache[slot_idx // block_size, slot_idx % block_size] = store_values[token_idx]
    actual = cache_storage.cpu()
    mismatch = torch.nonzero(actual != expected)
    assert mismatch.numel() == 0, (
        f'LLaMA cache differs at {mismatch[:8].tolist()}; '
        'this includes untouched slots, -1 entries, and block-stride padding'
    )
    assert torch.equal(store_storage.cpu(), store_storage_cpu), 'store_kv input was modified'


@pytest.mark.parametrize('block_size,num_blocks,n', [(128, 2333, 65536), (64, 2333, 32768), (32, 4666, 32768)])
def test_llama_kv_cache(block_size: int, num_blocks: int, n: int) -> None:
    assert num_blocks * block_size > n
    torch.set_default_device('npu')
    head_n = 4
    head_size = 128
    kv_cache = torch.empty((num_blocks, block_size, head_n, head_size), dtype=torch.bfloat16)
    slot_mapping = torch.randperm(num_blocks * block_size, dtype=torch.int32)[:n]
    positions = torch.randperm(n, dtype=torch.int32)[:4]
    slot_mapping[positions] = -1
    store_kv = torch.randn((n, head_n, head_size), dtype=torch.bfloat16)
    store_kv_cache_llama(store_kv, kv_cache, slot_mapping)
    load_kv = load_kv_cache_llama(kv_cache, head_n, slot_mapping)
    store_kv[positions] = torch.zeros((head_n, head_size), dtype=torch.bfloat16)
    assert torch.equal(load_kv, store_kv)


@pytest.mark.parametrize(
    'block_size,num_blocks,n,head_n',
    [
        pytest.param(128, 2, 5, 1, id='single-head-boundaries'),
        pytest.param(64, 3, 67, 4, id='multi-head-cross-block'),
        pytest.param(32, 7, 193, 8, id='persistent-multi-wave'),
        pytest.param(128, 2, 17, 32, id='full-mha-wide-token'),
    ],
)
def test_load_kv_cache_llama_ascend(block_size: int, num_blocks: int, n: int, head_n: int) -> None:
    """Validate NPU gather, page boundaries, repeated slots, and -1 padding."""
    capacity = num_blocks * block_size
    assert n <= capacity
    cpu_generator = torch.Generator().manual_seed(20240731)
    kv_cache_cpu = torch.randn(
        (num_blocks, block_size, head_n, 128), dtype=torch.bfloat16, device='cpu', generator=cpu_generator
    )
    slot_mapping_cpu = ((torch.arange(n, dtype=torch.int64, device='cpu') * 37 + 11) % capacity).to(torch.int32)
    slot_mapping_cpu[0] = 0
    slot_mapping_cpu[1] = block_size - 1
    slot_mapping_cpu[2] = block_size
    slot_mapping_cpu[3] = capacity - 1
    slot_mapping_cpu[4] = -1
    slot_mapping_cpu[5::7] = -1
    slot_mapping_cpu[6::11] = 0
    expected = torch.zeros((n, head_n, 128), dtype=torch.bfloat16, device='cpu')
    valid = slot_mapping_cpu != -1
    expected[valid] = kv_cache_cpu.view(capacity, head_n, 128)[slot_mapping_cpu[valid].long()]
    actual = load_kv_cache_llama(kv_cache_cpu.to('npu'), head_n, slot_mapping_cpu.to('npu'))
    assert actual.shape == expected.shape
    assert actual.dtype == torch.bfloat16
    assert actual.device.type == 'npu'
    assert torch.equal(actual.cpu(), expected)


def _benchmark_device() -> torch.device:
    return get_device()


def _make_kv_cache_4d(
    num_blocks: int, block_size: int, bytes_per_token: int, alignment_size: int = 576
) -> torch.Tensor:
    """Helper to create properly aligned 4D kv_cache for benchmarks."""
    kv_cache_byte_per_block = (block_size * bytes_per_token + alignment_size - 1) // alignment_size * alignment_size
    kv_cache = torch.zeros((num_blocks, kv_cache_byte_per_block), dtype=torch.uint8, device=_benchmark_device())
    return kv_cache[:, : block_size * bytes_per_token].view(num_blocks, block_size, 1, bytes_per_token)


@pytest.mark.benchmark
@pytest.mark.parametrize('use_fp8_rope,num_per_channels', [(False, 64), (True, 32)])
@pytest.mark.parametrize('block_size,num_blocks,n', [(64, 2333, 65536), (32, 4666, 65536), (129, 2333, 65536)])
def test_store_kv_cache_benchmark(
    block_size: int,
    num_blocks: int,
    n: int,
    use_fp8_rope: bool,
    num_per_channels: int,
    benchmark_timer: Callable[..., Any],
    benchmark_record: Callable[..., Any],
) -> Any:
    if not use_fp8_rope:
        bytes_per_token = 448 + 64 * 2 + 8
        alignment_size = 576
    else:
        bytes_per_token = 512 + 16
        alignment_size = 512
    kv_cache = _make_kv_cache_4d(num_blocks, block_size, bytes_per_token, alignment_size=alignment_size)
    slot_mapping = torch.randperm(num_blocks * block_size, dtype=torch.int32, device=_benchmark_device())[:n]
    store_kv = torch.randn(n, 1, 512, dtype=torch.bfloat16, device=_benchmark_device())
    t_us = benchmark_timer(
        lambda: store_kv_cache(
            store_kv, kv_cache, slot_mapping, use_fp8_rope=use_fp8_rope, num_per_channels=num_per_channels
        )
    )
    total_bytes = store_kv.numel() * store_kv.element_size() + n * bytes_per_token
    benchmark_record(
        kernel='cache',
        operation='store_kv_cache',
        params={'block_size': block_size, 'n': n, 'num_per_channels': num_per_channels},
        time_us=t_us,
        extras={'bandwidth_gbs': total_bytes / 1000000000.0 / (t_us / 1000000.0)},
    )


@pytest.mark.benchmark
@pytest.mark.parametrize('use_fp8_rope,num_per_channels', [(False, 64), (True, 32)])
@pytest.mark.parametrize('block_size,num_blocks,n', [(64, 2333, 65536), (32, 4666, 65536), (129, 2333, 65536)])
def test_load_kv_cache_benchmark(
    block_size: int,
    num_blocks: int,
    n: int,
    use_fp8_rope: bool,
    num_per_channels: int,
    benchmark_timer: Callable[..., Any],
    benchmark_record: Callable[..., Any],
) -> Any:
    if not use_fp8_rope:
        bytes_per_token = 448 + 64 * 2 + 8
        alignment_size = 576
    else:
        bytes_per_token = 512 + 16
        alignment_size = 512
    kv_cache = _make_kv_cache_4d(num_blocks, block_size, bytes_per_token, alignment_size=alignment_size)
    slot_mapping = torch.randperm(num_blocks * block_size, dtype=torch.int32, device=_benchmark_device())[:n]
    store_kv = torch.randn(n, 1, 512, dtype=torch.bfloat16, device=_benchmark_device())
    store_kv_cache(store_kv, kv_cache, slot_mapping, use_fp8_rope=use_fp8_rope, num_per_channels=num_per_channels)
    t_us = benchmark_timer(
        lambda: load_kv_cache(kv_cache, slot_mapping, use_fp8_rope=use_fp8_rope, num_per_channels=num_per_channels)
    )
    total_bytes = n * bytes_per_token + n * 512 * 2
    benchmark_record(
        kernel='cache',
        operation='load_kv_cache',
        params={'block_size': block_size, 'n': n, 'num_per_channels': num_per_channels},
        time_us=t_us,
        extras={'bandwidth_gbs': total_bytes / 1000000000.0 / (t_us / 1000000.0)},
    )


@pytest.mark.benchmark
@pytest.mark.parametrize('mode', ['fp8', 'mxfp8', 'fp4'])
@pytest.mark.parametrize('block_size,num_blocks,n', [(64, 2333, 65536), (32, 4666, 65536)])
def test_store_index_kv_cache_benchmark(
    block_size: int,
    num_blocks: int,
    n: int,
    mode: str,
    benchmark_timer: Callable[..., Any],
    benchmark_record: Callable[..., Any],
) -> Any:
    is_fp4 = mode == 'fp4'
    is_mxfp8 = mode == 'mxfp8'
    bytes_per_token = 64 + 4 if is_fp4 else 128 + 4
    kv_cache = torch.empty((num_blocks, block_size, 1, bytes_per_token), dtype=torch.uint8, device=_benchmark_device())
    slot_mapping = torch.randperm(num_blocks * block_size, dtype=torch.int32, device=_benchmark_device())[:n]
    store_kv = torch.randn(n, 1, 128, dtype=torch.bfloat16, device=_benchmark_device())
    t_us = benchmark_timer(
        lambda: store_index_kv_cache(store_kv, kv_cache, slot_mapping, is_fp4=is_fp4, is_mxfp8=is_mxfp8)
    )
    total_bytes = n * 128 * 2 + n * bytes_per_token
    benchmark_record(
        kernel='cache',
        operation=f'store_index_kv_cache_{mode}',
        params={'block_size': block_size, 'n': n},
        time_us=t_us,
        extras={'bandwidth_gbs': total_bytes / 1000000000.0 / (t_us / 1000000.0)},
    )


@pytest.mark.benchmark
@pytest.mark.parametrize('mode', ['fp8', 'mxfp8', 'fp4'])
@pytest.mark.parametrize('block_size,num_blocks,n', [(64, 2333, 65536), (32, 4666, 65536)])
def test_load_index_kv_cache_benchmark(
    block_size: int,
    num_blocks: int,
    n: int,
    mode: str,
    benchmark_timer: Callable[..., Any],
    benchmark_record: Callable[..., Any],
) -> Any:
    is_fp4 = mode == 'fp4'
    is_mxfp8 = mode == 'mxfp8'
    bytes_per_token = 64 + 4 if is_fp4 else 128 + 4
    kv_cache = torch.empty((num_blocks, block_size, 1, bytes_per_token), dtype=torch.uint8, device=_benchmark_device())
    slot_mapping = torch.randperm(num_blocks * block_size, dtype=torch.int32, device=_benchmark_device())[:n]
    store_kv = torch.randn(n, 1, 128, dtype=torch.bfloat16, device=_benchmark_device())
    store_index_kv_cache(store_kv, kv_cache, slot_mapping, is_fp4=is_fp4, is_mxfp8=is_mxfp8)
    t_us = benchmark_timer(lambda: load_index_kv_cache(kv_cache, slot_mapping, is_fp4=is_fp4, is_mxfp8=is_mxfp8))
    total_bytes = n * bytes_per_token + n * 128 * 2
    benchmark_record(
        kernel='cache',
        operation=f'load_index_kv_cache_{mode}',
        params={'block_size': block_size, 'n': n},
        time_us=t_us,
        extras={'bandwidth_gbs': total_bytes / 1000000000.0 / (t_us / 1000000.0)},
    )


@pytest.mark.benchmark
@pytest.mark.parametrize('block_size,num_blocks,n', [(128, 2333, 65536)])
def test_store_kv_cache_llama_benchmark(
    block_size: int, num_blocks: int, n: int, benchmark_timer: Callable[..., Any], benchmark_record: Callable[..., Any]
) -> Any:
    head_n, head_size = (4, 128)
    kv_cache = torch.zeros(
        (num_blocks, block_size, head_n, head_size), dtype=torch.bfloat16, device=_benchmark_device()
    )
    slot_mapping = torch.randperm(num_blocks * block_size, dtype=torch.int32, device=_benchmark_device())[:n]
    store_kv = torch.randn(n, head_n, head_size, dtype=torch.bfloat16, device=_benchmark_device())
    t_us = benchmark_timer(lambda: store_kv_cache_llama(store_kv, kv_cache, slot_mapping))
    total_bytes = store_kv.numel() * 2 * 2
    benchmark_record(
        kernel='cache',
        operation='store_kv_cache_LLaMA',
        params={'block_size': block_size, 'n': n},
        time_us=t_us,
        extras={'bandwidth_gbs': total_bytes / 1000000000.0 / (t_us / 1000000.0)},
    )


@pytest.mark.benchmark
@pytest.mark.parametrize('block_size,num_blocks,n', [(128, 2333, 65536)])
def test_load_kv_cache_llama_benchmark(
    block_size: int, num_blocks: int, n: int, benchmark_timer: Callable[..., Any], benchmark_record: Callable[..., Any]
) -> Any:
    head_n, head_size = (4, 128)
    kv_cache = torch.zeros(
        (num_blocks, block_size, head_n, head_size), dtype=torch.bfloat16, device=_benchmark_device()
    )
    slot_mapping = torch.randperm(num_blocks * block_size, dtype=torch.int32, device=_benchmark_device())[:n]
    store_kv = torch.randn(n, head_n, head_size, dtype=torch.bfloat16, device=_benchmark_device())
    store_kv_cache_llama(store_kv, kv_cache, slot_mapping)
    t_us = benchmark_timer(lambda: load_kv_cache_llama(kv_cache, head_n, slot_mapping))
    total_bytes = store_kv.numel() * 2 * 2
    benchmark_record(
        kernel='cache',
        operation='load_kv_cache_LLaMA',
        params={'block_size': block_size, 'n': n},
        time_us=t_us,
        extras={'bandwidth_gbs': total_bytes / 1000000000.0 / (t_us / 1000000.0)},
    )


@torch.inference_mode()
def test_per_token_cast_compile_cache_layout():
    # Compile quantization only; cache APIs themselves are outside this change.
    from tests.testing.numeric import assert_equal

    x = torch.randn((31, 512), dtype=torch.bfloat16, device=get_device())
    slots = torch.arange(31, dtype=torch.int32, device=x.device)
    cache = torch.empty((4, 32, 1, 528), dtype=torch.uint8, device=x.device)

    def quantize(value):
        return per_token_cast(value, 'e4m3', 32, round_sf=True, use_packed_ue8m0=True)

    compiled = torch.compile(quantize, backend='npugraph_ex', fullgraph=True, dynamic=False)
    for step in range(3):
        if step == 1:
            x.neg_().add_(0.125)
        elif step == 2:
            x = torch.randn_like(x)
        cache.fill_(0x5A)
        expected_data, expected_sf = torch_cast(
            x,
            'e4m3',
            (1, 32),
            round_sf=True,
            use_packed_ue8m0=True,
        )
        data, scale = compiled(x)
        assert_equal(data, expected_data)
        assert_equal(scale, expected_sf)
        store_kv_cache(x.unsqueeze(1), cache, slots, use_fp8_rope=True, num_per_channels=32)
        raw = cache.view(4, 32 * 528)
        data_bytes_per_block = 32 * 512
        stored_data = raw[:, :data_bytes_per_block].view(4, 32, 512)
        stored_sf = raw[:, data_bytes_per_block:].view(4, 32, 16)
        assert_equal(stored_data[0, :31], data.view(torch.uint8))
        assert_equal(stored_sf[0, :31], scale.view(torch.uint8))
        assert torch.all(stored_data[0, 31] == 0x5A)
        assert torch.all(stored_sf[0, 31] == 0x5A)
        del data, scale