#!/usr/bin/env python3
"""Benchmark: resample_torch (conv1d) vs alternative approaches for upsampling.

Supports multiple configurations via run_benchmark(B, input_time, factor).
"""

import os

os.environ["OMP_NUM_THREADS"] = "1"
os.environ["MKL_NUM_THREADS"] = "1"

import ctypes  # noqa: E402
import time  # noqa: E402

import numpy as np  # noqa: E402
import torch  # noqa: E402

torch.set_num_threads(1)
from numpy.lib.stride_tricks import sliding_window_view  # noqa: E402
from scipy.signal import correlate  # noqa: E402
from torch.nn.functional import conv1d as Fconv1d  # noqa: E402

from sgnl_cpu_interp import (  # noqa: E402
    upsample_transposed as _sgnl_upsample_transposed,
)

# ── load C shared library ────────────────────────────────────────────────
_lib = ctypes.CDLL(os.path.join(os.path.dirname(__file__), "upsample_c.so"))
_lib.upsample_fast_c.restype = None
_lib.upsample_fast_c.argtypes = [
    ctypes.c_void_p,
    ctypes.c_void_p,
    ctypes.c_void_p,
    ctypes.c_int,
    ctypes.c_int,
    ctypes.c_int,
    ctypes.c_int,
    ctypes.c_int,
]
_lib.upsample_fast_c_v2.restype = None
_lib.upsample_fast_c_v2.argtypes = [
    ctypes.c_void_p,
    ctypes.c_void_p,
    ctypes.c_void_p,
    ctypes.c_int,
    ctypes.c_int,
    ctypes.c_int,
    ctypes.c_int,
    ctypes.c_int,
]
_lib.upsample_fast_c_v3.restype = None
_lib.upsample_fast_c_v3.argtypes = [
    ctypes.c_void_p,
    ctypes.c_void_p,
    ctypes.c_void_p,
    ctypes.c_int,
    ctypes.c_int,
    ctypes.c_int,
    ctypes.c_int,
    ctypes.c_int,
]

UP_HALF_LENGTH = 8


# ── kernel generation (extracted from resampler.py) ─────────────────────
def upkernel(half_length, factor):
    """Sinc-windowed-sinc sub-kernels, identical to sgnts Resampler.upkernel"""
    kernel_length = int(2 * half_length * factor + 1)
    sub_kernel_length = int(2 * half_length + 1)
    c = kernel_length // 2
    x = np.arange(-c, c + 1)
    out = np.sinc(x / factor) * np.sinc(x / c)
    out = np.pad(out, (0, factor - 1))
    vecs = out.reshape(-1, factor).T[:, ::-1]
    return vecs.reshape(int(factor), 1, sub_kernel_length)


# ── 1. current sgn-ts torch approach: conv1d ────────────────────────────
def resample_conv1d(data0, kernel):
    """Current resample_torch: Fconv1d + mT + reshape"""
    data = data0.view(-1, 1, data0.shape[-1])
    out = Fconv1d(data, kernel)
    out = out.mT.reshape(data.shape[0], -1)
    return out


# ── 2. unfold + matmul (batched gemm) ───────────────────────────────────
def resample_unfold_matmul(data0, kernel, klen):
    """unfold (zero-copy view) + batched BLAS gemm.
    kernel shape: (K, factor) — pre-transposed."""
    data = data0.view(-1, data0.shape[-1])
    windows = data.unfold(-1, klen, 1)  # (batch, n_win, K) strided view
    out = torch.matmul(windows, kernel)  # (batch, n_win, factor)
    return out.reshape(data.shape[0], -1)


# ── 3. unfold + flat gemm (single large matmul) ─────────────────────────
def resample_unfold_flat_gemm(data0, kernel, klen):
    """unfold + reshape to one big matrix, single BLAS gemm.
    kernel shape: (K, factor) — pre-transposed."""
    data = data0.view(-1, data0.shape[-1])
    B = data.shape[0]
    windows = data.unfold(-1, klen, 1)  # (B, n_win, K) strided view
    n_win = windows.shape[1]  # noqa: F841
    flat = windows.reshape(-1, klen)  # (B*n_win, K)  — may copy
    out = torch.mm(flat, kernel)  # (B*n_win, factor)
    return out.reshape(B, -1)


# ── 4. numpy sliding_window_view + matmul ───────────────────────────────
def resample_numpy_swv(data0, kernel, klen):
    """numpy sliding_window_view + single matmul.
    kernel shape: (K, factor)."""
    data = data0.reshape(-1, data0.shape[-1])
    windows = sliding_window_view(data, klen, axis=-1)  # (B, n_win, K) view
    out = windows @ kernel  # (B, n_win, factor)
    return out.reshape(data.shape[0], -1)


# ── 5. current sgn-ts numpy approach: scipy correlate loop ──────────────
def resample_scipy_correlate(data0, kernel, factor):
    """Current resample_numpy: loop of scipy.signal.correlate.
    kernel shape: (factor, 1, K)."""
    data = data0.reshape(-1, data0.shape[-1])
    os = []
    for i in range(factor):
        os.append(correlate(data, kernel[i], mode="valid"))
    out = np.vstack(os)
    out = np.moveaxis(out, -1, -2)
    return out.reshape(data.shape[0], -1)


# ── 6. fast scalar×slice accumulation with identity shortcut ───────────
def resample_fast(data, kernel_2d, half_length, factor):
    """Explicit scalar×slice loop. Sub-kernel 0 is delta → direct copy.
    kernel_2d shape: (factor, klen) where klen = 2*half_length+1."""
    n_win = data.shape[-1] - 2 * half_length  # valid output positions
    klen = 2 * half_length + 1
    output = torch.empty(data.shape[0], n_win * factor, dtype=data.dtype)

    # Sub-kernel 0 is identity (delta): copy centre-aligned input samples
    output[:, 0::factor] = data[:, half_length : half_length + n_win]

    # Remaining sub-kernels: scalar × slice accumulation
    for f in range(1, factor):
        acc = kernel_2d[f, 0] * data[:, 0:n_win]
        for k in range(1, klen):
            acc = acc + kernel_2d[f, k] * data[:, k : k + n_win]
        output[:, f::factor] = acc

    return output


# ── 7. fast v2: in-place fused multiply-add, no temporaries ────────────
def resample_fast_v2(data, kernel_vals, half_length, factor):
    """Fused in-place add_(alpha=) for all sub-kernels — no identity shortcut.
    kernel_vals: list of lists of Python floats, [factor][klen]."""
    n_win = data.shape[-1] - 2 * half_length
    klen = 2 * half_length + 1
    B = data.shape[0]
    output = torch.empty(B, n_win * factor, dtype=data.dtype)

    acc = torch.empty(B, n_win, dtype=data.dtype)
    for f in range(factor):
        kv = kernel_vals[f]
        torch.mul(data[:, 0:n_win], kv[0], out=acc)
        for k in range(1, klen):
            acc.add_(data[:, k : k + n_win], alpha=kv[k])
        output[:, f::factor] = acc

    return output


# ── 7b. fast v2 + identity shortcut for sub-kernel 0 ──────────────────
def resample_fast_v2_identity(data, kernel_vals, half_length, factor):
    """Fused add_(alpha=) with identity shortcut: sub-kernel 0 is delta,
    so output[:, 0::factor] = direct copy. Only convolve sub-kernels 1..factor-1.
    kernel_vals: list of lists of Python floats, [factor][klen]."""
    n_win = data.shape[-1] - 2 * half_length
    klen = 2 * half_length + 1
    B = data.shape[0]
    output = torch.empty(B, n_win * factor, dtype=data.dtype)

    # Sub-kernel 0 is delta: direct copy from centre-aligned input
    output[:, 0::factor] = data[:, half_length : half_length + n_win]

    # Remaining sub-kernels: fused multiply-add
    acc = torch.empty(B, n_win, dtype=data.dtype)
    for f in range(1, factor):
        kv = kernel_vals[f]
        torch.mul(data[:, 0:n_win], kv[0], out=acc)
        for k in range(1, klen):
            acc.add_(data[:, k : k + n_win], alpha=kv[k])
        output[:, f::factor] = acc

    return output


# ── 7b2. torch.compile'd fast v2 + identity ──────────────────────────
#    torch.compile unrolls the Python loop and fuses 17 add_ calls into
#    a single optimised kernel — eliminates per-call dispatch overhead.
def _fast_v2_identity_inner(data, kv, half_length, n_win):
    """Inner convolution for a single sub-kernel (compilable)."""
    klen = 2 * half_length + 1
    acc = kv[0] * data[:, 0:n_win]
    for k in range(1, klen):
        acc = acc + kv[k] * data[:, k : k + n_win]
    return acc


_fast_v2_identity_compiled = torch.compile(_fast_v2_identity_inner)


def resample_fast_v2_compiled(data, kernel_t, half_length, factor):
    """torch.compile'd fast v2 + identity.
    kernel_t: (factor, klen) torch tensor."""
    n_win = data.shape[-1] - 2 * half_length
    output = torch.empty(data.shape[0], n_win * factor, dtype=data.dtype)
    output[:, 0::factor] = data[:, half_length : half_length + n_win]
    for f in range(1, factor):
        output[:, f::factor] = _fast_v2_identity_compiled(
            data, kernel_t[f], half_length, n_win
        )
    return output


# ── 7b3. torch.compile'd fast v2 (no identity) ────────────────────────
def resample_fast_v2_compiled_no_id(data, kernel_t, half_length, factor):
    """torch.compile'd fast v2 without identity — convolves all sub-kernels.
    kernel_t: (factor, klen) torch tensor."""
    n_win = data.shape[-1] - 2 * half_length
    output = torch.empty(data.shape[0], n_win * factor, dtype=data.dtype)
    for f in range(factor):
        output[:, f::factor] = _fast_v2_identity_compiled(
            data, kernel_t[f], half_length, n_win
        )
    return output


# ── 8. ctypes C fused loop ─────────────────────────────────────────────
def resample_ctypes(data_np, kernel_np_2d, half_length, factor):
    """Call the C fused scalar×slice loop via ctypes.
    data_np: (B, T) contiguous float32 numpy array.
    kernel_np_2d: (factor, klen) contiguous float32 numpy array."""
    B, T = data_np.shape
    n_win = T - 2 * half_length
    klen = 2 * half_length + 1
    output = np.empty((B, n_win * factor), dtype=np.float32)
    _lib.upsample_fast_c(
        output.ctypes.data,
        data_np.ctypes.data,
        kernel_np_2d.ctypes.data,
        B,
        T,
        n_win,
        klen,
        factor,
    )
    return output


# ── 9. ctypes C v2: shared reads, C-side interleave (factor=2 only) ────
def resample_ctypes_v2(data_np, kernel_np_2d, half_length, factor):
    B, T = data_np.shape
    n_win = T - 2 * half_length
    klen = 2 * half_length + 1
    output = np.empty((B, n_win * factor), dtype=np.float32)
    _lib.upsample_fast_c_v2(
        output.ctypes.data,
        data_np.ctypes.data,
        kernel_np_2d.ctypes.data,
        B,
        T,
        n_win,
        klen,
        factor,
    )
    return output


# ── 9b. ctypes C v3: v2 + identity shortcut (factor=2 only) ────────────
def resample_ctypes_v3(data_np, kernel_np_2d, half_length, factor):
    B, T = data_np.shape
    n_win = T - 2 * half_length
    klen = 2 * half_length + 1
    output = np.empty((B, n_win * factor), dtype=np.float32)
    _lib.upsample_fast_c_v3(
        output.ctypes.data,
        data_np.ctypes.data,
        kernel_np_2d.ctypes.data,
        B,
        T,
        n_win,
        klen,
        factor,
    )
    return output


# ── 10. sgnl_cpu_interp C extension (gstlal-style, AVX-512) ──────────────
def resample_sgnl_cpu_interp(data_np, half_length, factor):
    return _sgnl_upsample_transposed(data_np, factor=factor, half_length=half_length)


# ── 11. depthwise conv1d (no identity) — any factor ─────────────────────
def resample_depthwise(data, kernel_dws, half_length, factor):
    """Depthwise conv1d for all sub-kernels (groups=B, 1 out per group).
    kernel_dws: list of (B, 1, klen) tensors, one per sub-kernel."""
    n_win = data.shape[-1] - 2 * half_length
    B = data.shape[0]
    d = data.unsqueeze(0)  # (1, B, T)
    output = torch.empty(B, n_win * factor, dtype=data.dtype)
    for f in range(factor):
        out_f = Fconv1d(d, kernel_dws[f], groups=B)
        output[:, f::factor] = out_f.squeeze(0)
    return output


# ── 12. depthwise conv1d + identity — any factor ────────────────────────
def resample_depthwise_identity(data, kernel_dws, half_length, factor):
    """Depthwise conv1d for sub-kernels 1..factor-1 + identity for sub-kernel 0.
    kernel_dws: list of (B, 1, klen) tensors (only indices 1..factor-1 used)."""
    n_win = data.shape[-1] - 2 * half_length
    B = data.shape[0]
    d = data.unsqueeze(0)
    output = torch.empty(B, n_win * factor, dtype=data.dtype)
    output[:, 0::factor] = data[:, half_length : half_length + n_win]
    for f in range(1, factor):
        out_f = Fconv1d(d, kernel_dws[f], groups=B)
        output[:, f::factor] = out_f.squeeze(0)
    return output


# ── 13. interleaved sgemv (gstlal-style layout, torch.matmul) ─────────
def resample_interleaved_mv(data, kernel_2d, half_length, factor):
    """Transpose to interleaved (T, B), unfold, batched mv via torch.matmul.
    kernel_2d: (factor, klen) torch tensor."""
    klen = 2 * half_length + 1
    n_win = data.shape[-1] - 2 * half_length
    B = data.shape[0]

    # Transpose to interleaved layout: (B, T) → (T, B)
    data_t = data.T.contiguous()  # (T, B)

    # Sliding windows: (n_win, B, klen) — klen dim has stride B (non-contiguous)
    windows = data_t.unfold(0, klen, 1)  # (n_win, B, klen)

    output = torch.empty(B, n_win * factor, dtype=data.dtype)

    for f in range(factor):
        # batched mv: (n_win, B, klen) @ (klen,) → (n_win, B)
        result = torch.matmul(windows, kernel_2d[f])
        output[:, f::factor] = result.T

    return output


# ── 14. interleaved sgemv + identity ─────────────────────────────────
def resample_interleaved_mv_identity(data, kernel_2d, half_length, factor):
    """Same as above but sub-kernel 0 = identity (memcpy)."""
    klen = 2 * half_length + 1
    n_win = data.shape[-1] - 2 * half_length
    B = data.shape[0]

    data_t = data.T.contiguous()  # (T, B)
    windows = data_t.unfold(0, klen, 1)  # (n_win, B, klen)

    output = torch.empty(B, n_win * factor, dtype=data.dtype)

    # Sub-kernel 0: identity
    output[:, 0::factor] = data[:, half_length : half_length + n_win]

    for f in range(1, factor):
        result = torch.matmul(windows, kernel_2d[f])
        output[:, f::factor] = result.T

    return output


# ── 15. interleaved sgemm (flatten n_win*B, single mm call) ──────────
def resample_interleaved_mm(data, kernel_2d, half_length, factor):
    """Transpose + unfold + flatten to (n_win*B, klen), single torch.mm.
    kernel_2d: (klen, factor) torch tensor (pre-transposed)."""
    klen = 2 * half_length + 1
    n_win = data.shape[-1] - 2 * half_length
    B = data.shape[0]

    data_t = data.T.contiguous()  # (T, B)
    windows = data_t.unfold(0, klen, 1)  # (n_win, B, klen)

    # Flatten to (n_win*B, klen) and do single sgemm
    flat = windows.reshape(-1, klen)  # may copy due to non-contiguous klen stride
    out = torch.mm(flat, kernel_2d)  # (n_win*B, factor)
    out = out.reshape(n_win, B, factor)

    output = torch.empty(B, n_win * factor, dtype=data.dtype)
    for f in range(factor):
        output[:, f::factor] = out[:, :, f].T

    return output


# ── 16. interleaved sgemm + identity ─────────────────────────────────
def resample_interleaved_mm_identity(data, kernel_2d, half_length, factor):
    """Same as above but sub-kernel 0 = identity, mm only for sub-kernels 1+."""
    klen = 2 * half_length + 1
    n_win = data.shape[-1] - 2 * half_length
    B = data.shape[0]

    data_t = data.T.contiguous()  # (T, B)
    windows = data_t.unfold(0, klen, 1)  # (n_win, B, klen)

    output = torch.empty(B, n_win * factor, dtype=data.dtype)
    output[:, 0::factor] = data[:, half_length : half_length + n_win]

    if factor > 1:
        # Only convolve sub-kernels 1..factor-1
        flat = windows.reshape(-1, klen)
        out = torch.mm(flat, kernel_2d[:, 1:])  # (n_win*B, factor-1)
        out = out.reshape(n_win, B, factor - 1)
        for f in range(1, factor):
            output[:, f::factor] = out[:, :, f - 1].T

    return output


# ══════════════════════════════════════════════════════════════════════════
# Benchmark runner
# ══════════════════════════════════════════════════════════════════════════

N_WARMUP = 20
N_ITER = 200


def bench(fn, label):
    for _ in range(N_WARMUP):
        fn()
    t0 = time.perf_counter()
    for _ in range(N_ITER):
        fn()
    elapsed = time.perf_counter() - t0
    per_call_us = elapsed / N_ITER * 1e6
    print(f"  {label:35s}  {per_call_us:10.1f} µs/call")
    return per_call_us


def run_benchmark(B, input_time, factor):
    half_length = UP_HALF_LENGTH
    klen = 2 * half_length + 1  # 17
    n_win = input_time - 2 * half_length
    out_time = n_win * factor

    print("=" * 72)
    print(
        f"Config: B={B}, input=({B}, {input_time}), factor={factor}, "
        f"output=({B}, {out_time})"
    )
    print("=" * 72)

    # ── kernels ──────────────────────────────────────────────────────────
    kernel_np = upkernel(half_length, factor).astype(np.float32)  # (factor, 1, klen)

    kernel_conv1d = torch.tensor(kernel_np.copy(), dtype=torch.float32)
    kernel_mm = torch.tensor(
        np.ascontiguousarray(kernel_np.reshape(factor, klen).T),
        dtype=torch.float32,
    )  # (klen, factor)
    kernel_np_mm = np.ascontiguousarray(  # noqa: F841
        kernel_np.reshape(factor, klen).T
    )  # (klen, factor)
    kernel_fast = torch.tensor(
        kernel_np.reshape(factor, klen).copy(),
        dtype=torch.float32,
    )  # (factor, klen)
    kernel_fast_v2 = kernel_np.reshape(factor, klen).tolist()
    kernel_ctypes = np.ascontiguousarray(
        kernel_np.reshape(factor, klen), dtype=np.float32
    )

    # depthwise kernels: replicate each sub-kernel across B channels
    kernel_dws = []
    for f in range(factor):
        kd = torch.tensor(
            np.tile(kernel_np[f], (B, 1, 1)).copy(), dtype=torch.float32
        )  # (B, 1, klen)
        kernel_dws.append(kd)

    # ── input data ───────────────────────────────────────────────────────
    data_torch = torch.randn(B, input_time, dtype=torch.float32)
    data_np = data_torch.numpy().copy()

    # ── correctness check ────────────────────────────────────────────────
    ref_t = resample_conv1d(data_torch, kernel_conv1d)
    ref = ref_t.numpy()
    print(f"Input shape:  {tuple(data_torch.shape)}")
    print(f"Output shape: {tuple(ref_t.shape)}")
    print()

    checks = [
        ("conv1d (ref)", ref_t),
        ("unfold+matmul", resample_unfold_matmul(data_torch, kernel_mm, klen)),
        ("unfold+flat_gemm", resample_unfold_flat_gemm(data_torch, kernel_mm, klen)),
        (
            "fast v2 (add_+alpha)",
            resample_fast_v2(data_torch, kernel_fast_v2, half_length, factor),
        ),
        (
            "fast v2 + identity",
            resample_fast_v2_identity(data_torch, kernel_fast_v2, half_length, factor),
        ),
        (
            "torch.compile + id",
            resample_fast_v2_compiled(data_torch, kernel_fast, half_length, factor),
        ),
        (
            "torch.compile (no id)",
            resample_fast_v2_compiled_no_id(
                data_torch, kernel_fast, half_length, factor
            ),
        ),
        (
            "depthwise (no id)",
            resample_depthwise(data_torch, kernel_dws, half_length, factor),
        ),
        (
            "depthwise + identity",
            resample_depthwise_identity(data_torch, kernel_dws, half_length, factor),
        ),
        (
            "interleaved mv",
            resample_interleaved_mv(data_torch, kernel_fast, half_length, factor),
        ),
        (
            "interleaved mv + id",
            resample_interleaved_mv_identity(
                data_torch, kernel_fast, half_length, factor
            ),
        ),
        (
            "interleaved mm",
            resample_interleaved_mm(data_torch, kernel_mm, half_length, factor),
        ),
        (
            "interleaved mm + id",
            resample_interleaved_mm_identity(
                data_torch, kernel_mm, half_length, factor
            ),
        ),
        (
            "ctypes C fused",
            torch.from_numpy(
                resample_ctypes(data_np, kernel_ctypes, half_length, factor)
            ),
        ),
        (
            "sgnl_cpu_interp",
            torch.from_numpy(resample_sgnl_cpu_interp(data_np, half_length, factor)),
        ),
    ]
    if factor == 2:
        checks.append(
            (
                "ctypes C v2",
                torch.from_numpy(
                    resample_ctypes_v2(data_np, kernel_ctypes, half_length, factor)
                ),
            )
        )
        checks.append(
            (
                "ctypes C v3",
                torch.from_numpy(
                    resample_ctypes_v3(data_np, kernel_ctypes, half_length, factor)
                ),
            )
        )

    print("Correctness vs conv1d reference:")
    for label, out in checks[1:]:
        if isinstance(out, torch.Tensor):
            ok = torch.allclose(ref_t, out, atol=1e-6)
            md = (ref_t - out).abs().max().item()
        else:
            ok = np.allclose(ref, out, atol=1e-6)
            md = np.abs(ref - out).max()
        print(f"  {label:28s} {ok}  (max diff {md:.2e})")
    print()

    # ── benchmark ────────────────────────────────────────────────────────
    print(f"Benchmark ({N_ITER} iterations, {N_WARMUP} warmup):")
    print()
    results = []

    print("Torch approaches:")
    t = bench(
        lambda: resample_conv1d(data_torch, kernel_conv1d), "conv1d (current sgn-ts)"
    )
    t_conv1d = t
    results.append(("conv1d (current sgn-ts)", t))

    t = bench(
        lambda: resample_unfold_matmul(data_torch, kernel_mm, klen),
        "unfold + batched matmul",
    )
    results.append(("unfold + batched matmul", t))

    t = bench(
        lambda: resample_unfold_flat_gemm(data_torch, kernel_mm, klen),
        "unfold + flat gemm (torch.mm)",
    )
    results.append(("unfold + flat gemm", t))

    t = bench(
        lambda: resample_fast_v2(data_torch, kernel_fast_v2, half_length, factor),
        "fast v2 (add_ + alpha)",
    )
    results.append(("fast v2 (add_ + alpha)", t))

    t = bench(
        lambda: resample_fast_v2_identity(
            data_torch, kernel_fast_v2, half_length, factor
        ),
        "fast v2 + identity",
    )
    results.append(("fast v2 + identity", t))

    t = bench(
        lambda: resample_fast_v2_compiled_no_id(
            data_torch, kernel_fast, half_length, factor
        ),
        "torch.compile (no identity)",
    )
    results.append(("torch.compile (no identity)", t))

    t = bench(
        lambda: resample_fast_v2_compiled(data_torch, kernel_fast, half_length, factor),
        "torch.compile + identity",
    )
    results.append(("torch.compile + identity", t))

    t = bench(
        lambda: resample_depthwise(data_torch, kernel_dws, half_length, factor),
        "depthwise (no identity)",
    )
    results.append(("depthwise (no identity)", t))

    t = bench(
        lambda: resample_depthwise_identity(
            data_torch, kernel_dws, half_length, factor
        ),
        "depthwise + identity",
    )
    results.append(("depthwise + identity", t))

    print()
    print("Interleaved (gstlal-style) approaches:")
    t = bench(
        lambda: resample_interleaved_mv(data_torch, kernel_fast, half_length, factor),
        "interleaved mv (batched sgemv)",
    )
    results.append(("interleaved mv (batched sgemv)", t))

    t = bench(
        lambda: resample_interleaved_mv_identity(
            data_torch, kernel_fast, half_length, factor
        ),
        "interleaved mv + identity",
    )
    results.append(("interleaved mv + identity", t))

    t = bench(
        lambda: resample_interleaved_mm(data_torch, kernel_mm, half_length, factor),
        "interleaved mm (flat sgemm)",
    )
    results.append(("interleaved mm (flat sgemm)", t))

    t = bench(
        lambda: resample_interleaved_mm_identity(
            data_torch, kernel_mm, half_length, factor
        ),
        "interleaved mm + identity",
    )
    results.append(("interleaved mm + identity", t))

    print()
    print("Numpy / C approaches:")
    t = bench(
        lambda: resample_ctypes(data_np, kernel_ctypes, half_length, factor),
        "ctypes C fused loop",
    )
    results.append(("ctypes C fused loop", t))

    if factor == 2:
        t = bench(
            lambda: resample_ctypes_v2(data_np, kernel_ctypes, half_length, factor),
            "ctypes C v2 (fma+shared)",
        )
        results.append(("ctypes C v2 (fma+shared)", t))

        t = bench(
            lambda: resample_ctypes_v3(data_np, kernel_ctypes, half_length, factor),
            "ctypes C v3 (identity+fma)",
        )
        results.append(("ctypes C v3 (identity+fma)", t))

    t = bench(
        lambda: resample_sgnl_cpu_interp(data_np, half_length, factor),
        "sgnl_cpu_interp (AVX-512)",
    )
    results.append(("sgnl_cpu_interp (AVX-512)", t))

    print()
    print("Summary (vs conv1d):")
    for label, t in sorted(results, key=lambda x: -x[1]):
        ratio = t_conv1d / t
        faster = "faster" if ratio > 1 else "slower"
        print(f"  {label:35s}  {t:10.1f} µs  {ratio:6.2f}x {faster}")
    print()


# ══════════════════════════════════════════════════════════════════════════
# Run all configurations
# ══════════════════════════════════════════════════════════════════════════

run_benchmark(B=1000, input_time=1040, factor=2)
run_benchmark(B=1000, input_time=528, factor=4)
run_benchmark(B=1000, input_time=272, factor=8)
run_benchmark(B=1000, input_time=144, factor=16)
