Download tests/test_sim.py from jburtoft/gte-encoder-neuron-kernels: direct link, hf CLI and curl.
- Browser
- Download file 7.87 kB
-
https://huggingface.co/kernels/jburtoft/gte-encoder-neuron-kernels/resolve/v1/tests/test_sim.py
- Command line
-
hf download hf://jburtoft/gte-encoder-neuron-kernels@v1/tests/test_sim.py
-
curl -L -o test_sim.py https://huggingface.co/kernels/jburtoft/gte-encoder-neuron-kernels/resolve/v1/tests/test_sim.py
7.87 kB
| """nki.simulate parity of the three kernels vs NumPy fp32 references. Simulator only: NOT device evidence | |
| (the simulator does not check SBUF/PSUM capacity and has known DMA-pattern fidelity gaps).""" | |
| import numpy as np | |
| import pytest | |
| nki = pytest.importorskip("nki") | |
| ml_dtypes = pytest.importorskip("ml_dtypes") | |
| from conftest import EPS, attn_block_ref, gated_mlp_ln_ref, layer_norm, rel_l2, rope_table # noqa: E402 | |
| from attn_block_kernel import attn_block_kernel # noqa: E402 | |
| from attn_block_pack import pack # noqa: E402 | |
| from gated_mlp_kernel import gated_mlp_ln_kernel, gated_mlp_ln_stream_kernel, mlp_weights_fit # noqa: E402 | |
| from res_ln_kernel import res_ln_kernel # noqa: E402 | |
| bf16 = ml_dtypes.bfloat16 | |
| TOL = 1e-2 # bf16 in/out; observed ~3-5e-3 | |
| pytestmark = pytest.mark.sim | |
| def _bc(v): | |
| return np.tile(np.asarray(v, np.float32), (128, 1)) | |
| # ---------------------------------------------------------------- residual + LayerNorm | |
| def test_res_ln(R, lnc, has_res, H): | |
| rng = np.random.default_rng(R + H) | |
| x = (rng.standard_normal((R, H)) * 3 + 0.5).astype(bf16) | |
| r = rng.standard_normal((R, H)).astype(bf16) | |
| g = 1 + 0.1 * rng.standard_normal(H) | |
| b = 0.1 * rng.standard_normal(H) | |
| y = nki.simulate(res_ln_kernel[lnc])(x, r, _bc(g), _bc(b), EPS, has_res) | |
| s = x.astype(np.float32) + (r.astype(np.float32) if has_res else 0) | |
| assert rel_l2(y, layer_norm(s, g, b)) < TOL | |
| # ---------------------------------------------------------------- gated MLP + residual + LayerNorm | |
| def _mlp_case(R, K, I, seed): | |
| rng = np.random.default_rng(seed) | |
| x = rng.standard_normal((R, K)).astype(bf16) | |
| wu = (rng.standard_normal((I, K)) / np.sqrt(K)).astype(bf16) | |
| wg = (rng.standard_normal((I, K)) / np.sqrt(K)).astype(bf16) | |
| wd = (rng.standard_normal((K, I)) / np.sqrt(I)).astype(bf16) | |
| bd, g, b = 0.1 * rng.standard_normal(K), 1 + 0.1 * rng.standard_normal(K), 0.1 * rng.standard_normal(K) | |
| args = (x, np.ascontiguousarray(wu.T), np.ascontiguousarray(wg.T), np.ascontiguousarray(wd.T), | |
| _bc(bd), _bc(g), _bc(b), EPS) | |
| ref = gated_mlp_ln_ref(x, wu.astype(np.float32), wg.astype(np.float32), wd.astype(np.float32), bd, g, b) | |
| return args, ref | |
| def test_gated_mlp_ln(R, lnc): | |
| assert mlp_weights_fit(768, 3072) | |
| args, ref = _mlp_case(R, 768, 3072, R) | |
| assert rel_l2(nki.simulate(gated_mlp_ln_kernel[lnc])(*args), ref) < TOL | |
| # streaming variant: 1024/4096 (gte-large) does not fit resident; also 768/3072 for a direct comparison, | |
| # and R > STREAM_SB (two super-blocks) with a partial last super-block. | |
| def test_gated_mlp_ln_stream(R, lnc, K, I): | |
| if K == 1024: | |
| assert not mlp_weights_fit(K, I) | |
| args, ref = _mlp_case(R, K, I, R + K) | |
| assert rel_l2(nki.simulate(gated_mlp_ln_stream_kernel[lnc])(*args), ref) < TOL | |
| # ---------------------------------------------------------------- fused attention block | |
| H = 768 | |
| def _attn_case(B, L, seed, lengths=None, H=H): | |
| rng = np.random.default_rng(seed) | |
| x = rng.standard_normal((B, L, H)).astype(bf16) | |
| wqkv = (rng.standard_normal((3 * H, H)) * 0.06).astype(bf16).astype(np.float32) | |
| bqkv = (0.1 * rng.standard_normal(3 * H)).astype(np.float32) | |
| wo = (rng.standard_normal((H, H)) / np.sqrt(H)).astype(bf16).astype(np.float32) | |
| bo = (0.1 * rng.standard_normal(H)).astype(np.float32) | |
| g = (1 + 0.1 * rng.standard_normal(H)).astype(np.float32) | |
| b = (0.1 * rng.standard_normal(H)).astype(np.float32) | |
| cos, sin = rope_table(L) | |
| if lengths is None: # ragged right padding, every row different | |
| lengths = [max(1, L - 29 * (i + 1)) for i in range(B)] | |
| mask = np.zeros((B, L), np.float32) | |
| for i, n in enumerate(lengths): | |
| mask[i, :n] = 1 | |
| kb = ((1 - mask) * -30000.0).astype(bf16) | |
| pk = pack(wqkv, bqkv, wo, bo, g, b, cos, sin, H, 1.0 / np.sqrt(64)) | |
| args = [x.reshape(B * L, H), pk["wqk"].astype(bf16), pk["bqk"], pk["wv"].astype(bf16), pk["bv"], | |
| pk["wo"].astype(bf16), pk["bo"], pk["g"], pk["b"], pk["cs"], kb] | |
| ref = (x.astype(np.float32), wqkv, bqkv, wo, bo, g, b, cos, sin, mask) | |
| return args, ref | |
| # (B, L, lnc): packing factor SP = 4 / 2 / 1 at L = 128 / 256 / >=384; B not divisible by SP falls back to | |
| # SP = 1; LNC2 with odd B uses the query split (needs (L/128) % 2 == 0). | |
| ATTN_CASES = [ | |
| (4, 128, 1), # SP=4 | |
| (2, 128, 1), # SP=4 does not divide -> SP=1 | |
| (3, 128, 1), # odd B, SP=1 | |
| (2, 256, 1), # SP=2 | |
| (1, 384, 1), # L=384, SP=1 | |
| (1, 512, 1), | |
| (8, 128, 2), # LNC2, SP=4 per core | |
| (4, 256, 2), # LNC2, SP=2 per core | |
| (2, 384, 2), # LNC2, L=384 | |
| (1, 256, 2), # LNC2 query split, B=1 | |
| (1, 512, 2), # LNC2 query split, B=1 | |
| (3, 256, 2), # LNC2 query split, odd B | |
| ] | |
| def test_attn_block(B, L, lnc): | |
| args, ref = _attn_case(B, L, seed=B * 1000 + L + lnc) | |
| y = np.asarray(nki.simulate(attn_block_kernel[lnc])(*args, EPS)).astype(np.float32).reshape(B, L, H) | |
| assert rel_l2(y, attn_block_ref(*ref)) < TOL | |
| # hidden 1024 / 16 heads (gte-large-en-v1.5) | |
| def test_attn_block_h1024(B, L, lnc): | |
| args, ref = _attn_case(B, L, seed=B * 1000 + L + lnc + 7, H=1024) | |
| y = np.asarray(nki.simulate(attn_block_kernel[lnc])(*args, EPS)).astype(np.float32).reshape(B, L, 1024) | |
| assert rel_l2(y, attn_block_ref(*ref)) < TOL | |
| # long sequences (key chunks of 512, grouped LayerNorm epilogue), H 768 and 1024, ragged padding incl. inside the | |
| # last key chunk; LNC2 with odd B uses the query split. | |
| def test_attn_block_long(B, L, lnc, Hm): | |
| lengths = [L - 29 * (i + 1) for i in range(B)] | |
| lengths[0] = L - 600 if L == 1024 else lengths[0] # padding reaches into the first key chunk | |
| args, ref = _attn_case(B, L, seed=B * 1000 + L + lnc + Hm, lengths=lengths, H=Hm) | |
| y = np.asarray(nki.simulate(attn_block_kernel[lnc])(*args, EPS)).astype(np.float32).reshape(B, L, Hm) | |
| assert rel_l2(y, attn_block_ref(*ref)) < TOL | |
| def test_attn_block_short_sequence(): | |
| """One valid token (all other keys padded) and one full-length sequence in the same pass.""" | |
| args, ref = _attn_case(2, 256, seed=7, lengths=[1, 256]) | |
| y = np.asarray(nki.simulate(attn_block_kernel[1])(*args, EPS)).astype(np.float32).reshape(2, 256, H) | |
| assert rel_l2(y, attn_block_ref(*ref)) < TOL | |
| def test_attn_block_padding_isolation(B, L, lnc): | |
| """Sequence 0's output must be bit-identical when every other sequence in the batch changes.""" | |
| args, _ = _attn_case(B, L, seed=11) | |
| y0 = np.asarray(nki.simulate(attn_block_kernel[lnc])(*args, EPS)).view(np.uint16).reshape(B, L, H) | |
| rng = np.random.default_rng(12) | |
| x2 = args[0].reshape(B, L, H).copy() | |
| x2[1:] = rng.standard_normal(x2[1:].shape).astype(bf16) | |
| kb2 = args[-1].copy() | |
| kb2[1:] = 0 | |
| kb2[1:, L // 2:] = -30000.0 | |
| args2 = [x2.reshape(B * L, H)] + args[1:-1] + [kb2] | |
| y1 = np.asarray(nki.simulate(attn_block_kernel[lnc])(*args2, EPS)).view(np.uint16).reshape(B, L, H) | |
| assert np.array_equal(y0[0], y1[0]) | |