mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
- Introduced for deterministic corpus framing, CRC checks, and regression evidence for NPU-P6. - Added to validate NPU-P6 reference execution. - Created a new RTL model for gate-level NPU transactions. - Updated to instantiate and streamline the testbench. - Enhanced with capability checks and added NPU-P6 reference contract tests. - Modified to include in the source list and refactor rejection logic. - Updated README.md to document new test scripts and their purposes.
985 lines
44 KiB
Python
985 lines
44 KiB
Python
"""NPU-P4 operator differential RTL tests (Icarus and Verilator).
|
|
|
|
``test_npu_operator`` drives the production job controller (npu_core with
|
|
ExecutionReady=1 + npu_dma over a byte-memory BFM) with descriptor jobs whose
|
|
stored output bytes are compared byte-for-byte against the pinned Python
|
|
numerical reference (scripts/npu_reference.py) of the descriptor's own
|
|
quantization records. Descriptor jobs, operand tensors, parameter records,
|
|
golden windows and expected counters are generated here with a deterministic
|
|
LCG (recorded seeds). Validation-rejection cases reuse
|
|
scripts/npu_descriptors.py as the golden first-fault oracle and prove that no
|
|
side effects reach memory. Covers the NPU-V011/NPU-V012 operator lists and
|
|
the fixed models' operator shapes (KWS/VWW).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
import sys
|
|
import zlib
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
sys.path.insert(0, str(ROOT / "tests"))
|
|
|
|
from npu_descriptors import ( # noqa: E402
|
|
DESCRIPTOR_BYTES,
|
|
Descriptor,
|
|
DescriptorError,
|
|
full_reduction,
|
|
used_span,
|
|
validate_descriptor,
|
|
)
|
|
from npu_reference import Tensor # noqa: E402
|
|
import npu_reference # noqa: E402
|
|
from test_npu_rtl_p3 import ( # noqa: E402
|
|
CORE_SOURCES,
|
|
F_ARITHMETIC,
|
|
MEM_BASE,
|
|
_build_sim,
|
|
_run,
|
|
_tools,
|
|
)
|
|
|
|
OPERATOR_TB = ROOT / "tests/rtl" / "npu_operator_tb.sv"
|
|
P4_SOURCES = [
|
|
*CORE_SOURCES[:-2],
|
|
ROOT / "rtl/ip/multimedia" / "npu_mac_array.sv",
|
|
ROOT / "rtl/ip/multimedia" / "npu_accumulator.sv",
|
|
ROOT / "rtl/ip/multimedia" / "npu_vector.sv",
|
|
ROOT / "rtl/ip/multimedia" / "npu_requantizer.sv",
|
|
*CORE_SOURCES[-2:],
|
|
ROOT / "physical/smoke/syn/yosys" / "npu_block_top.sv",
|
|
]
|
|
|
|
CORE_MEM_BYTES = 0x80000
|
|
GAP_FILL = 0xA5
|
|
|
|
OP_CONV2D = 1
|
|
OP_DEPTHWISE3X3 = 2
|
|
OP_FULLY_CONNECTED = 3
|
|
OP_ADD = 4
|
|
OP_MAX_POOL = 5
|
|
OP_AVERAGE_POOL = 6
|
|
OP_GLOBAL_AVERAGE_POOL = 7
|
|
OP_CLAMP = 8
|
|
|
|
R_DONE = 1
|
|
R_ERROR = 2
|
|
|
|
SCEN_COMPUTE = 0
|
|
SCEN_REJECT = 1
|
|
SCEN_ARITH = 2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Deterministic LCG (the repo's Numerical Recipes idiom)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _Rng:
|
|
def __init__(self, seed: int) -> None:
|
|
self.state = seed & 0xFFFFFFFF
|
|
|
|
def next(self) -> int:
|
|
self.state = (self.state * 1664525 + 1013904223) & 0xFFFFFFFF
|
|
return self.state
|
|
|
|
def byte(self) -> int:
|
|
return (self.next() >> 16) & 0xFF
|
|
|
|
def int8(self) -> int:
|
|
value = self.byte()
|
|
return value - 256 if value >= 128 else value
|
|
|
|
def weight(self) -> int:
|
|
value = self.int8()
|
|
return 127 if value == -128 else value
|
|
|
|
def int32(self) -> int:
|
|
return struct.unpack("<i", struct.pack("<I", self.next()))[0]
|
|
|
|
def nonneg31(self) -> int:
|
|
return self.next() & 0x7FFFFFFF
|
|
|
|
def shift(self) -> int:
|
|
return (self.next() % 62) - 31
|
|
|
|
def pick(self, seq):
|
|
return seq[self.next() % len(seq)]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Descriptor job construction (tensors, parameter records, golden, counters)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _Job:
|
|
"""One descriptor job: memory image, golden output window, expectations."""
|
|
|
|
def __init__(self, name: str, scen: int) -> None:
|
|
self.name = name
|
|
self.scen = scen
|
|
self.descs: list[Descriptor] = []
|
|
self.mem = bytearray([GAP_FILL] * CORE_MEM_BYTES)
|
|
self.gold = bytearray([GAP_FILL] * CORE_MEM_BYTES)
|
|
self._gold_spans: list[tuple[int, int]] = []
|
|
self.gold_lo = CORE_MEM_BYTES
|
|
self.gold_hi = 0
|
|
self.macs = 0
|
|
self.fcode = 0
|
|
self.fdesc = 0xFFFFFFFF
|
|
self.faddr = 0
|
|
self.completed = 0
|
|
self.alloc = 0x4000
|
|
|
|
def region(self, size: int, align: int = 64) -> int:
|
|
base = (self.alloc + align - 1) & ~(align - 1)
|
|
self.alloc = base + max(size, 1)
|
|
return base
|
|
|
|
def place(self, base: int, payload: bytes) -> None:
|
|
self.mem[base : base + len(payload)] = payload
|
|
|
|
def place_golden(self, base: int, payload: bytes) -> None:
|
|
self.gold[base : base + len(payload)] = payload
|
|
self._gold_spans.append((base, base + len(payload)))
|
|
self.gold_lo = min(self.gold_lo, base)
|
|
self.gold_hi = max(self.gold_hi, base + len(payload))
|
|
|
|
def finish(self) -> None:
|
|
# Gap bytes inside the golden window (between the descriptors' output
|
|
# spans) keep the initial memory image: a store outside the output
|
|
# spans is a side effect and fails the byte-exact comparison.
|
|
covered = bytearray(CORE_MEM_BYTES)
|
|
for lo, hi in self._gold_spans:
|
|
covered[lo:hi] = b"\x01" * (hi - lo)
|
|
for index in range(CORE_MEM_BYTES):
|
|
if not covered[index]:
|
|
self.gold[index] = self.mem[index]
|
|
if self.gold_hi == 0:
|
|
self.gold_lo = 0
|
|
self.gold_hi = 1
|
|
|
|
|
|
|
|
def _tile(th: int, tw: int, oh: int, ow: int) -> tuple[int, int]:
|
|
"""Clamp a tile choice to the descriptor limits (<=8 positions, <= output)."""
|
|
rows = max(1, min(th, oh, 8))
|
|
cols = max(1, min(tw, ow, 8 // rows))
|
|
return (rows, cols)
|
|
|
|
|
|
def _degrade_shifts(shifts: list[int]) -> list[int]:
|
|
"""Clamp QM shifts to nonpositive so the golden can never fault (compute cases)."""
|
|
return [min(value, 0) for value in shifts]
|
|
|
|
|
|
def _golden_conv(rng, inputs, weights, bias, mults, shifts, sh, sw, pads, zout, act,
|
|
allow_fault=False):
|
|
try:
|
|
ref = npu_reference.conv2d(
|
|
inputs, weights, bias, mults, shifts,
|
|
stride_h=sh, stride_w=sw,
|
|
pad_top=pads[0], pad_bottom=pads[1], pad_left=pads[2], pad_right=pads[3],
|
|
output_scale=1.0, output_zero_point=zout, act_min=act[0], act_max=act[1])
|
|
return ref, shifts
|
|
except npu_reference.ArithmeticFault:
|
|
if allow_fault:
|
|
oh = (inputs.height + pads[0] + pads[1] - len(weights[0])) // sh + 1
|
|
ow = (inputs.width + pads[2] + pads[3] - len(weights[0][0])) // sw + 1
|
|
zero = Tensor((oh, ow, len(weights)), tuple([0] * (oh * ow * len(weights))),
|
|
1.0, zout)
|
|
return zero, shifts
|
|
shifts = _degrade_shifts(shifts)
|
|
ref = npu_reference.conv2d(
|
|
inputs, weights, bias, mults, shifts,
|
|
stride_h=sh, stride_w=sw,
|
|
pad_top=pads[0], pad_bottom=pads[1], pad_left=pads[2], pad_right=pads[3],
|
|
output_scale=1.0, output_zero_point=zout, act_min=act[0], act_max=act[1])
|
|
return ref, shifts
|
|
|
|
|
|
def _golden_dw(rng, inputs, weights, bias, mults, shifts, sh, sw, pads, zout, act):
|
|
try:
|
|
ref = npu_reference.depthwise_conv2d(
|
|
inputs, weights, bias, mults, shifts,
|
|
stride_h=sh, stride_w=sw,
|
|
pad_top=pads[0], pad_bottom=pads[1], pad_left=pads[2], pad_right=pads[3],
|
|
output_scale=1.0, output_zero_point=zout, act_min=act[0], act_max=act[1])
|
|
except npu_reference.ArithmeticFault:
|
|
shifts = _degrade_shifts(shifts)
|
|
ref = npu_reference.depthwise_conv2d(
|
|
inputs, weights, bias, mults, shifts,
|
|
stride_h=sh, stride_w=sw,
|
|
pad_top=pads[0], pad_bottom=pads[1], pad_left=pads[2], pad_right=pads[3],
|
|
output_scale=1.0, output_zero_point=zout, act_min=act[0], act_max=act[1])
|
|
return ref, shifts
|
|
|
|
|
|
def _golden_add(rng, in0, in1, z0, z1, zout, quant, act):
|
|
m0, s0, m1, s1, mout, sout = quant
|
|
try:
|
|
ref = npu_reference.add(
|
|
Tensor(in0[0], in0[1], 1.0, z0), Tensor(in1[0], in1[1], 1.0, z1),
|
|
input0_multiplier=m0, input0_shift=s0, input1_multiplier=m1, input1_shift=s1,
|
|
output_multiplier=mout, output_shift=sout, output_scale=1.0,
|
|
output_zero_point=zout, act_min=act[0], act_max=act[1])
|
|
except npu_reference.ArithmeticFault:
|
|
m0, s0, m1, s1, mout, sout = (m0, min(s0, 0), m1, min(s1, 0), mout, min(sout, 0))
|
|
ref = npu_reference.add(
|
|
Tensor(in0[0], in0[1], 1.0, z0), Tensor(in1[0], in1[1], 1.0, z1),
|
|
input0_multiplier=m0, input0_shift=s0, input1_multiplier=m1, input1_shift=s1,
|
|
output_multiplier=mout, output_shift=sout, output_scale=1.0,
|
|
output_zero_point=zout, act_min=act[0], act_max=act[1])
|
|
return ref, (m0, s0, m1, s1, mout, sout)
|
|
|
|
def _tensor_bytes(ref: Tensor) -> bytes:
|
|
return bytes(value & 0xFF for value in ref.data)
|
|
|
|
|
|
def _out_words(desc: Descriptor, ref: Tensor, job: _Job) -> None:
|
|
raw = _tensor_bytes(ref)
|
|
cout = desc.cout
|
|
out = bytearray(used_span(desc.oh, desc.ow, cout, desc.output_row_bytes))
|
|
for oy in range(desc.oh):
|
|
for ox in range(desc.ow):
|
|
for channel in range(cout):
|
|
out[oy * desc.output_row_bytes + ox * cout + channel] = raw[
|
|
(oy * desc.ow + ox) * cout + channel
|
|
]
|
|
job.place_golden(desc.output_base - MEM_BASE, bytes(out))
|
|
|
|
|
|
def _pack_channel_params(bias: list[int], mults: list[int], shifts: list[int]) -> bytes:
|
|
return b"".join(struct.pack("<iiiI", b_value, m_value, s_value, 0)
|
|
for b_value, m_value, s_value in zip(bias, mults, shifts))
|
|
|
|
|
|
def _pack_conv_weights(weights, cout: int, full_k: int) -> bytes:
|
|
"""Pack (Cout,Kh,Kw,Cin) nested weights as group-of-8 rows of lanes."""
|
|
packed = bytearray()
|
|
for group in range(0, cout, 8):
|
|
lanes = min(8, cout - group)
|
|
for k in range(full_k):
|
|
for lane in range(8):
|
|
value = 0
|
|
if lane < lanes:
|
|
flat = weights[group + lane]
|
|
value = flat[k]
|
|
packed.append(value & 0xFF)
|
|
return bytes(packed)
|
|
|
|
|
|
def _flat_conv_weights(weights, cout: int, kh: int, kw: int, cin: int) -> list[list[int]]:
|
|
"""Flatten nested (Cout,Kh,Kw,Cin) weights in the kh,kw,cin reduction order."""
|
|
flat: list[list[int]] = []
|
|
for channel in range(cout):
|
|
row: list[int] = []
|
|
for dkh in range(kh):
|
|
for dkw in range(kw):
|
|
for ci in range(cin):
|
|
row.append(weights[channel][dkh][dkw][ci])
|
|
flat.append(row)
|
|
return flat
|
|
|
|
|
|
def _conv_like(rng: _Rng, job: _Job, *, opcode: int, h: int, w: int, cin: int, cout: int,
|
|
kh: int, kw: int, sh: int, sw: int, pads: tuple[int, int, int, int],
|
|
zin: int, zout: int, tile: tuple[int, int], k_slice: int,
|
|
act: tuple[int, int], irow: int | None = None,
|
|
orow: int | None = None, params: tuple | None = None,
|
|
allow_fault: bool = False) -> Descriptor:
|
|
"""Build a CONV2D/FC job descriptor with reference golden content."""
|
|
in_data = tuple(rng.int8() for _ in range(h * w * cin))
|
|
weights = [[[[rng.weight() for _ in range(cin)] for _ in range(kw)] for _ in range(kh)]
|
|
for _ in range(cout)]
|
|
if params is None:
|
|
bias = [max(-(1 << 28), min((1 << 28) - 1, rng.int32())) for _ in range(cout)]
|
|
mults = [rng.nonneg31() for _ in range(cout)]
|
|
shifts = [max(-31, min(3, rng.shift())) for _ in range(cout)]
|
|
else:
|
|
bias, mults, shifts = params
|
|
if opcode == OP_FULLY_CONNECTED:
|
|
kh, kw, sh, sw = 1, 1, 1, 1
|
|
pads = (0, 0, 0, 0)
|
|
ref, shifts = _golden_conv(rng, Tensor((h, w, cin), in_data, 1.0, zin), weights,
|
|
bias, mults, shifts, sh, sw, pads, zout, act,
|
|
allow_fault=allow_fault)
|
|
oh, ow = ref.height, ref.width
|
|
irow = irow if irow is not None else w * cin
|
|
orow = orow if orow is not None else ow * cout
|
|
desc = Descriptor()
|
|
desc.version_opcode = 0x0100_0000 | opcode
|
|
desc.input_hw = (w << 16) | h
|
|
desc.channels = (cout << 16) | cin
|
|
desc.output_hw = (ow << 16) | oh
|
|
desc.input0_row_bytes = irow
|
|
desc.output_row_bytes = orow
|
|
desc.kernel_stride = (sw << 24) | (sh << 16) | (kw << 8) | kh
|
|
desc.padding = (pads[3] << 24) | (pads[2] << 16) | (pads[1] << 8) | pads[0]
|
|
desc.tile_hw = (tile[1] << 8) | tile[0]
|
|
desc.k_slice = k_slice
|
|
full_k = full_reduction(desc)
|
|
in_span = used_span(h, w, cin, irow)
|
|
out_span = used_span(oh, ow, cout, orow)
|
|
desc.input0_base = MEM_BASE + job.region(in_span)
|
|
desc.output_base = MEM_BASE + job.region(out_span)
|
|
desc.param_base = MEM_BASE + job.region(cout * 16)
|
|
desc.weight_base = MEM_BASE + job.region(((cout + 7) // 8) * full_k * 8)
|
|
desc.input0_bytes = in_span
|
|
desc.output_bytes = out_span
|
|
desc.param_bytes = cout * 16
|
|
desc.input0_zero = zin
|
|
desc.output_zero = zout
|
|
desc.act_min = act[0]
|
|
desc.act_max = act[1]
|
|
desc.weight_bytes = ((cout + 7) // 8) * full_k * 8
|
|
validate_descriptor(desc, descriptor_region=(MEM_BASE, 8 * DESCRIPTOR_BYTES))
|
|
job.descs.append(desc)
|
|
job.place(desc.input0_base - MEM_BASE, bytes(value & 0xFF for value in in_data))
|
|
job.place(desc.param_base - MEM_BASE, _pack_channel_params(bias, mults, shifts))
|
|
flat = _flat_conv_weights(weights, cout, kh, kw, cin)
|
|
job.place(desc.weight_base - MEM_BASE, _pack_conv_weights(flat, cout, full_k))
|
|
_out_words(desc, ref, job)
|
|
job.macs += oh * ow * cout * full_k
|
|
return desc
|
|
|
|
|
|
def _dw_like(rng: _Rng, job: _Job, *, h: int, w: int, cin: int, sh: int, sw: int,
|
|
pads: tuple[int, int, int, int], zin: int, zout: int,
|
|
tile: tuple[int, int], k_slice: int, act: tuple[int, int],
|
|
params: tuple | None = None) -> Descriptor:
|
|
in_data = tuple(rng.int8() for _ in range(h * w * cin))
|
|
weights = [[[rng.weight() for _ in range(cin)] for _ in range(3)] for _ in range(3)]
|
|
if params is None:
|
|
bias = [max(-(1 << 28), min((1 << 28) - 1, rng.int32())) for _ in range(cin)]
|
|
mults = [rng.nonneg31() for _ in range(cin)]
|
|
shifts = [max(-31, min(3, rng.shift())) for _ in range(cin)]
|
|
else:
|
|
bias, mults, shifts = params
|
|
ref, shifts = _golden_dw(rng, Tensor((h, w, cin), in_data, 1.0, zin), weights,
|
|
bias, mults, shifts, sh, sw, pads, zout, act)
|
|
oh, ow = ref.height, ref.width
|
|
irow = w * cin
|
|
orow = ow * cin
|
|
desc = Descriptor()
|
|
desc.version_opcode = 0x0100_0000 | OP_DEPTHWISE3X3
|
|
desc.input_hw = (w << 16) | h
|
|
desc.channels = (cin << 16) | cin
|
|
desc.output_hw = (ow << 16) | oh
|
|
desc.input0_row_bytes = irow
|
|
desc.output_row_bytes = orow
|
|
desc.kernel_stride = (sw << 24) | (sh << 16) | (3 << 8) | 3
|
|
desc.padding = (pads[3] << 24) | (pads[2] << 16) | (pads[1] << 8) | pads[0]
|
|
desc.tile_hw = (tile[1] << 8) | tile[0]
|
|
desc.k_slice = k_slice
|
|
in_span = used_span(h, w, cin, irow)
|
|
out_span = used_span(oh, ow, cin, orow)
|
|
desc.input0_base = MEM_BASE + job.region(in_span)
|
|
desc.output_base = MEM_BASE + job.region(out_span)
|
|
desc.param_base = MEM_BASE + job.region(cin * 16)
|
|
desc.weight_base = MEM_BASE + job.region(((cin + 7) // 8) * 72)
|
|
desc.input0_bytes = in_span
|
|
desc.output_bytes = out_span
|
|
desc.param_bytes = cin * 16
|
|
desc.input0_zero = zin
|
|
desc.output_zero = zout
|
|
desc.act_min = act[0]
|
|
desc.act_max = act[1]
|
|
desc.weight_bytes = ((cin + 7) // 8) * 72
|
|
validate_descriptor(desc, descriptor_region=(MEM_BASE, 8 * DESCRIPTOR_BYTES))
|
|
job.descs.append(desc)
|
|
job.place(desc.input0_base - MEM_BASE, bytes(value & 0xFF for value in in_data))
|
|
job.place(desc.param_base - MEM_BASE, _pack_channel_params(bias, mults, shifts))
|
|
flat = [[weights[dkh][dkw][c] for dkh in range(3) for dkw in range(3)] for c in range(cin)]
|
|
packed = bytearray()
|
|
for group in range(0, cin, 8):
|
|
lanes = min(8, cin - group)
|
|
for kk in range(9):
|
|
for lane in range(8):
|
|
value = flat[group + lane][kk] if lane < lanes else 0
|
|
packed.append(value & 0xFF)
|
|
job.place(desc.weight_base - MEM_BASE, bytes(packed))
|
|
_out_words(desc, ref, job)
|
|
job.macs += oh * ow * cin * 9
|
|
return desc
|
|
|
|
|
|
def _pool_like(rng: _Rng, job: _Job, *, opcode: int, h: int, w: int, cin: int,
|
|
kh: int, kw: int, sh: int, sw: int, pads: tuple[int, int, int, int],
|
|
zp: int, tile: tuple[int, int], k_slice: int,
|
|
act: tuple[int, int]) -> Descriptor:
|
|
in_data = tuple(rng.int8() for _ in range(h * w * cin))
|
|
call = npu_reference.max_pool if opcode == OP_MAX_POOL else npu_reference.average_pool
|
|
ref = call(
|
|
Tensor((h, w, cin), in_data, 1.0, zp),
|
|
kernel_h=kh, kernel_w=kw, stride_h=sh, stride_w=sw,
|
|
pad_top=pads[0], pad_bottom=pads[1], pad_left=pads[2], pad_right=pads[3],
|
|
act_min=act[0], act_max=act[1],
|
|
)
|
|
oh, ow = ref.height, ref.width
|
|
irow = w * cin
|
|
orow = ow * cin
|
|
desc = Descriptor()
|
|
desc.version_opcode = 0x0100_0000 | opcode
|
|
desc.input_hw = (w << 16) | h
|
|
desc.channels = (cin << 16) | cin
|
|
desc.output_hw = (ow << 16) | oh
|
|
desc.input0_row_bytes = irow
|
|
desc.output_row_bytes = orow
|
|
desc.kernel_stride = (sw << 24) | (sh << 16) | (kw << 8) | kh
|
|
desc.padding = (pads[3] << 24) | (pads[2] << 16) | (pads[1] << 8) | pads[0]
|
|
desc.tile_hw = (tile[1] << 8) | tile[0]
|
|
desc.k_slice = k_slice
|
|
in_span = used_span(h, w, cin, irow)
|
|
out_span = used_span(oh, ow, cin, orow)
|
|
desc.input0_base = MEM_BASE + job.region(in_span)
|
|
desc.output_base = MEM_BASE + job.region(out_span)
|
|
desc.input0_bytes = in_span
|
|
desc.output_bytes = out_span
|
|
desc.input0_zero = zp
|
|
desc.output_zero = zp
|
|
desc.act_min = act[0]
|
|
desc.act_max = act[1]
|
|
validate_descriptor(desc, descriptor_region=(MEM_BASE, 8 * DESCRIPTOR_BYTES))
|
|
job.descs.append(desc)
|
|
job.place(desc.input0_base - MEM_BASE, bytes(value & 0xFF for value in in_data))
|
|
_out_words(desc, ref, job)
|
|
return desc
|
|
|
|
|
|
def _gap_like(rng: _Rng, job: _Job, *, h: int, w: int, cin: int, zp: int,
|
|
tile: tuple[int, int], k_slice: int, act: tuple[int, int]) -> Descriptor:
|
|
in_data = tuple(rng.int8() for _ in range(h * w * cin))
|
|
ref = npu_reference.global_average_pool(
|
|
Tensor((h, w, cin), in_data, 1.0, zp), act_min=act[0], act_max=act[1])
|
|
desc = Descriptor()
|
|
desc.version_opcode = 0x0100_0000 | OP_GLOBAL_AVERAGE_POOL
|
|
desc.input_hw = (w << 16) | h
|
|
desc.channels = (cin << 16) | cin
|
|
desc.output_hw = (1 << 16) | 1
|
|
desc.input0_row_bytes = w * cin
|
|
desc.output_row_bytes = cin
|
|
desc.tile_hw = (tile[1] << 8) | tile[0]
|
|
desc.k_slice = k_slice
|
|
in_span = used_span(h, w, cin, w * cin)
|
|
desc.input0_base = MEM_BASE + job.region(in_span)
|
|
desc.output_base = MEM_BASE + job.region(cin)
|
|
desc.input0_bytes = in_span
|
|
desc.output_bytes = cin
|
|
desc.input0_zero = zp
|
|
desc.output_zero = zp
|
|
desc.act_min = act[0]
|
|
desc.act_max = act[1]
|
|
validate_descriptor(desc, descriptor_region=(MEM_BASE, 8 * DESCRIPTOR_BYTES))
|
|
job.descs.append(desc)
|
|
job.place(desc.input0_base - MEM_BASE, bytes(value & 0xFF for value in in_data))
|
|
_out_words(desc, ref, job)
|
|
return desc
|
|
|
|
|
|
def _add_like(rng: _Rng, job: _Job, *, h: int, w: int, cin: int,
|
|
z0: int, z1: int, zout: int, tile: tuple[int, int], k_slice: int,
|
|
act: tuple[int, int], alias: bool = False,
|
|
multipliers: tuple | None = None) -> Descriptor:
|
|
in0 = tuple(rng.int8() for _ in range(h * w * cin))
|
|
in1 = in0 if alias else tuple(rng.int8() for _ in range(h * w * cin))
|
|
if multipliers is None:
|
|
m0, m1, mout = rng.nonneg31(), rng.nonneg31(), rng.nonneg31()
|
|
s0, s1, sout = (max(-31, min(3, rng.shift())) for _ in range(3))
|
|
else:
|
|
m0, s0, m1, s1, mout, sout = multipliers
|
|
ref, (m0, s0, m1, s1, mout, sout) = _golden_add(
|
|
rng, ((h, w, cin), in0), ((h, w, cin), in1), z0, z1, zout,
|
|
(m0, s0, m1, s1, mout, sout), act)
|
|
oh, ow = h, w
|
|
irow = w * cin
|
|
orow = ow * cin
|
|
desc = Descriptor()
|
|
desc.version_opcode = 0x0100_0000 | OP_ADD
|
|
desc.input_hw = (w << 16) | h
|
|
desc.channels = (cin << 16) | cin
|
|
desc.output_hw = (ow << 16) | oh
|
|
desc.input0_row_bytes = irow
|
|
desc.output_row_bytes = orow
|
|
desc.input1_row_bytes = irow
|
|
desc.kernel_stride = 0x0101_0101
|
|
desc.tile_hw = (tile[1] << 8) | tile[0]
|
|
desc.k_slice = k_slice
|
|
in_span = used_span(h, w, cin, irow)
|
|
out_span = used_span(oh, ow, cin, orow)
|
|
desc.input0_base = MEM_BASE + job.region(in_span)
|
|
desc.input1_base = desc.input0_base if alias else MEM_BASE + job.region(in_span)
|
|
desc.output_base = MEM_BASE + job.region(out_span)
|
|
desc.param_base = MEM_BASE + job.region(32)
|
|
desc.input0_bytes = in_span
|
|
desc.input1_bytes = in_span
|
|
desc.output_bytes = out_span
|
|
desc.param_bytes = 32
|
|
desc.input0_zero = z0
|
|
desc.input1_zero = z1
|
|
desc.output_zero = zout
|
|
desc.act_min = act[0]
|
|
desc.act_max = act[1]
|
|
validate_descriptor(desc, descriptor_region=(MEM_BASE, 8 * DESCRIPTOR_BYTES))
|
|
job.descs.append(desc)
|
|
job.place(desc.input0_base - MEM_BASE, bytes(value & 0xFF for value in in0))
|
|
if not alias:
|
|
job.place(desc.input1_base - MEM_BASE, bytes(value & 0xFF for value in in1))
|
|
job.place(desc.param_base - MEM_BASE, struct.pack("<IiiiiiiI", 20, m0, s0, m1, s1,
|
|
mout, sout, 0))
|
|
_out_words(desc, ref, job)
|
|
return desc
|
|
|
|
|
|
def _clamp_like(rng: _Rng, job: _Job, *, h: int, w: int, cin: int, zp: int,
|
|
tile: tuple[int, int], k_slice: int, act: tuple[int, int]) -> Descriptor:
|
|
in_data = tuple(rng.int8() for _ in range(h * w * cin))
|
|
ref = npu_reference.clamp(Tensor((h, w, cin), in_data, 1.0, zp), act[0], act[1])
|
|
desc = Descriptor()
|
|
desc.version_opcode = 0x0100_0000 | OP_CLAMP
|
|
desc.input_hw = (w << 16) | h
|
|
desc.channels = (cin << 16) | cin
|
|
desc.output_hw = (w << 16) | h
|
|
desc.input0_row_bytes = w * cin
|
|
desc.output_row_bytes = w * cin
|
|
desc.kernel_stride = 0x0101_0101
|
|
desc.tile_hw = (tile[1] << 8) | tile[0]
|
|
desc.k_slice = k_slice
|
|
in_span = used_span(h, w, cin, w * cin)
|
|
desc.input0_base = MEM_BASE + job.region(in_span)
|
|
desc.output_base = MEM_BASE + job.region(in_span)
|
|
desc.input0_bytes = in_span
|
|
desc.output_bytes = in_span
|
|
desc.input0_zero = zp
|
|
desc.output_zero = zp
|
|
desc.act_min = act[0]
|
|
desc.act_max = act[1]
|
|
validate_descriptor(desc, descriptor_region=(MEM_BASE, 8 * DESCRIPTOR_BYTES))
|
|
job.descs.append(desc)
|
|
job.place(desc.input0_base - MEM_BASE, bytes(value & 0xFF for value in in_data))
|
|
_out_words(desc, ref, job)
|
|
return desc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Case emission
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _image_hex(path: Path, blob: bytes) -> None:
|
|
pad = (-len(blob)) % 4
|
|
words = struct.unpack(f"<{(len(blob) + pad) // 4}I", blob + bytes(pad))
|
|
path.write_text("\n".join(f"{word & 0xFFFFFFFF:08x}" for word in words) + "\n",
|
|
encoding="utf-8")
|
|
|
|
|
|
def _emit(tmp_path: Path, job: _Job, cases: list[list[str]]) -> None:
|
|
job.finish()
|
|
for index, desc in enumerate(job.descs):
|
|
job.place(index * DESCRIPTOR_BYTES, desc.to_bytes())
|
|
image = tmp_path / f"op_{job.name}.hex"
|
|
_image_hex(image, bytes(job.mem))
|
|
golden = tmp_path / f"op_{job.name}_gold.hex"
|
|
gold = bytes(job.gold[job.gold_lo : job.gold_hi])
|
|
_image_hex(golden, gold)
|
|
cases.append([
|
|
f"+IMG={image}",
|
|
f"+GOLD={golden}",
|
|
f"+BASE={MEM_BASE:x}",
|
|
f"+COUNT={len(job.descs)}",
|
|
"+JOBID=5a5b5c5d",
|
|
"+TIMEOUT=20000000",
|
|
f"+SCEN={job.scen}",
|
|
f"+FCODE={job.fcode}",
|
|
f"+FDESC={job.fdesc:x}",
|
|
f"+FADDR={job.faddr:x}",
|
|
"+CODE=1",
|
|
f"+MACS={job.macs}",
|
|
f"+GOLD_BASE={(MEM_BASE + job.gold_lo):x}",
|
|
f"+GOLD_BYTES={len(gold)}",
|
|
f"+NAME={job.name}",
|
|
])
|
|
|
|
|
|
def _reject(tmp_path: Path, cases: list[list[str]], name: str, opcode: int,
|
|
mutate) -> None:
|
|
"""One invalid descriptor: the golden validator is the first-fault oracle."""
|
|
rng = _Rng(zlib.crc32(name.encode()))
|
|
job = _Job(name, SCEN_REJECT)
|
|
if opcode == OP_CONV2D:
|
|
desc = _conv_like(rng, job, opcode=opcode, h=4, w=4, cin=1, cout=1, kh=1, kw=1,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=0, zout=0, tile=(1, 1),
|
|
k_slice=1, act=(-128, 127))
|
|
elif opcode == OP_DEPTHWISE3X3:
|
|
desc = _dw_like(rng, job, h=4, w=4, cin=8, sh=1, sw=1, pads=(0, 0, 0, 0), zin=0,
|
|
zout=0, tile=(1, 1), k_slice=9, act=(-128, 127))
|
|
elif opcode == OP_ADD:
|
|
desc = _add_like(rng, job, h=2, w=2, cin=1, z0=0, z1=0, zout=0, tile=(1, 1),
|
|
k_slice=1, act=(-128, 127))
|
|
else:
|
|
desc = _clamp_like(rng, job, h=2, w=2, cin=1, zp=0, tile=(1, 1), k_slice=1,
|
|
act=(-128, 127))
|
|
words = list(struct.unpack(f"<{DESCRIPTOR_BYTES // 4}I", desc.to_bytes()))
|
|
mutate(words)
|
|
record = Descriptor.from_bytes(struct.pack(f"<{len(words)}I", *words))
|
|
try:
|
|
validate_descriptor(record, descriptor_region=(MEM_BASE, DESCRIPTOR_BYTES))
|
|
raise AssertionError(f"rejection case {name} unexpectedly validates")
|
|
except DescriptorError as error:
|
|
job.fcode = error.code
|
|
job.faddr = MEM_BASE + (error.word * 4 if error.word is not None else 0)
|
|
job.descs = [record]
|
|
job.gold_lo = desc.output_base - MEM_BASE
|
|
job.gold_hi = job.gold_lo + desc.output_bytes
|
|
job.fdesc = 0
|
|
_emit(tmp_path, job, cases)
|
|
|
|
|
|
def _operator_cases(tmp_path: Path, selected: set[str] | None = None) -> list[list[str]]:
|
|
cases: list[list[str]] = []
|
|
|
|
def compute(name: str, build) -> None:
|
|
if (selected is not None) and (name not in selected):
|
|
return
|
|
job = _Job(name, SCEN_COMPUTE)
|
|
build(_Rng(zlib.crc32(name.encode()) ^ 0x5EED), job)
|
|
_emit(tmp_path, job, cases)
|
|
|
|
def reject(name: str, opcode: int, mutate) -> None:
|
|
if (selected is None) or (name in selected):
|
|
_reject(tmp_path, cases, name, opcode, mutate)
|
|
|
|
# -- V011: conv kernels {1,3,4,10,16} x strides {1,2}
|
|
for kernel in (1, 3, 4, 10, 16):
|
|
for stride in (1, 2):
|
|
compute(f"conv_k{kernel}s{stride}",
|
|
lambda rng, job, kernel=kernel, stride=stride: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=kernel + 3, w=kernel + 2, cin=3,
|
|
cout=5, kh=kernel, kw=kernel, sh=stride, sw=stride,
|
|
pads=(0, 0, 0, 0), zin=0, zout=0, tile=(2, 2),
|
|
k_slice=max(1, (kernel * kernel * 3) // 2), act=(-128, 127)))
|
|
# -- KWS conv10x4 stride 2 asymmetric padding (4,5,1,1)
|
|
compute("conv_kws_10x4",
|
|
lambda rng, job: _conv_like(rng, job, opcode=OP_CONV2D, h=13, w=8, cin=1,
|
|
cout=8, kh=10, kw=4, sh=2, sw=2, pads=(4, 5, 1, 1),
|
|
zin=-5, zout=17, tile=(2, 2), k_slice=40,
|
|
act=(-128, 127)))
|
|
# -- zero-point extremes on a padded conv
|
|
for zp in (-128, 0, 83, 127):
|
|
compute(f"conv_zp{zp & 0xFF:02x}",
|
|
lambda rng, job, zp=zp: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=6, w=5, cin=4, cout=6, kh=3, kw=3,
|
|
sh=2, sw=2, pads=(2, 1, 1, 2), zin=zp, zout=-zp if zp != -128 else 0,
|
|
tile=(2, 3), k_slice=13, act=(-128, 127)))
|
|
# -- depthwise 3x3 multiplier 1, strides and tails
|
|
compute("dw33_s1", lambda rng, job: _dw_like(rng, job, h=6, w=7, cin=8, sh=1, sw=1,
|
|
pads=(1, 1, 1, 1), zin=0, zout=0,
|
|
tile=(2, 2), k_slice=9, act=(-128, 127)))
|
|
compute("dw33_s2_c7", lambda rng, job: _dw_like(rng, job, h=9, w=5, cin=7, sh=2, sw=2,
|
|
pads=(0, 1, 1, 0), zin=9, zout=-9,
|
|
tile=(3, 1), k_slice=4, act=(-64, 63)))
|
|
compute("dw33_c1", lambda rng, job: _dw_like(rng, job, h=4, w=4, cin=1, sh=1, sw=1,
|
|
pads=(1, 1, 1, 1), zin=-128, zout=127,
|
|
tile=(1, 1), k_slice=9, act=(-128, 127)))
|
|
# -- fully connected
|
|
compute("fc_64_12", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_FULLY_CONNECTED, h=1, w=1, cin=64, cout=12, kh=1, kw=1,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=3, zout=-7, tile=(1, 1), k_slice=64,
|
|
act=(-128, 127)))
|
|
compute("fc_tail", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_FULLY_CONNECTED, h=1, w=1, cin=37, cout=9, kh=1, kw=1,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=-11, zout=23, tile=(1, 1), k_slice=19,
|
|
act=(-128, 100)))
|
|
# -- shift boundaries with guaranteed-safe accumulators
|
|
compute("fc_shift30", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_FULLY_CONNECTED, h=1, w=1, cin=1, cout=4, kh=1, kw=1,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=0, zout=0, tile=(1, 1), k_slice=1,
|
|
act=(-128, 127),
|
|
params=([1, 0, -1, 2], [0x7FFFFFFF] * 4, [30, 30, 30, 30])))
|
|
compute("fc_shiftm31", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_FULLY_CONNECTED, h=1, w=1, cin=1, cout=4, kh=1, kw=1,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=0, zout=0, tile=(1, 1), k_slice=1,
|
|
act=(-128, 127),
|
|
params=([1000, -1000, 32768, -32768], [0x7FFFFFFF] * 4, [-31, -31, -31, -31])))
|
|
compute("conv_mult_flush", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=3, w=3, cin=2, cout=4, kh=2, kw=2, sh=1, sw=1,
|
|
pads=(0, 0, 0, 0), zin=0, zout=42, tile=(1, 1), k_slice=4, act=(-128, 127),
|
|
params=([7, -9, 13, -3], [0, 0, 0, 0], [0, 0, 0, 0])))
|
|
# -- ADD with independent scales/zeros and the input alias case
|
|
compute("add_basic", lambda rng, job: _add_like(
|
|
rng, job, h=4, w=5, cin=6, z0=-9, z1=11, zout=5, tile=(2, 2), k_slice=1,
|
|
act=(-128, 127)))
|
|
compute("add_alias", lambda rng, job: _add_like(
|
|
rng, job, h=3, w=3, cin=3, z0=0, z1=0, zout=-20, tile=(1, 3), k_slice=1,
|
|
act=(-128, 127), alias=True))
|
|
compute("add_flush", lambda rng, job: _add_like(
|
|
rng, job, h=2, w=2, cin=2, z0=0, z1=0, zout=127, tile=(1, 1), k_slice=1,
|
|
act=(-128, 127), multipliers=(0, 0, 0, 0, 0, 0)))
|
|
# -- pooling with padding exclusion and tie counts
|
|
compute("maxpool_pad", lambda rng, job: _pool_like(
|
|
rng, job, opcode=OP_MAX_POOL, h=7, w=6, cin=5, kh=3, kw=3, sh=2, sw=2,
|
|
pads=(1, 1, 1, 1), zp=-3, tile=(2, 2), k_slice=9, act=(-128, 127)))
|
|
compute("avgpool_ties", lambda rng, job: _pool_like(
|
|
rng, job, opcode=OP_AVERAGE_POOL, h=5, w=5, cin=4, kh=2, kw=2, sh=1, sw=1,
|
|
pads=(0, 1, 0, 1), zp=0, tile=(2, 2), k_slice=4, act=(-128, 127)))
|
|
compute("avgpool_neg", lambda rng, job: _pool_like(
|
|
rng, job, opcode=OP_AVERAGE_POOL, h=4, w=4, cin=3, kh=3, kw=3, sh=1, sw=1,
|
|
pads=(2, 2, 2, 2), zp=127, tile=(1, 2), k_slice=9, act=(-128, 127)))
|
|
compute("maxpool_k16", lambda rng, job: _pool_like(
|
|
rng, job, opcode=OP_MAX_POOL, h=18, w=17, cin=2, kh=16, kw=16, sh=1, sw=1,
|
|
pads=(0, 0, 0, 0), zp=-128, tile=(1, 1), k_slice=256, act=(-128, 127)))
|
|
# -- GAP
|
|
compute("gap_25x5", lambda rng, job: _gap_like(
|
|
rng, job, h=25, w=5, cin=64, zp=-2, tile=(1, 1), k_slice=125, act=(-128, 127)))
|
|
compute("gap_1x1", lambda rng, job: _gap_like(
|
|
rng, job, h=1, w=1, cin=9, zp=83, tile=(1, 1), k_slice=1, act=(-128, 127)))
|
|
# -- CLAMP
|
|
compute("clamp_basic", lambda rng, job: _clamp_like(
|
|
rng, job, h=6, w=6, cin=8, zp=0, tile=(2, 2), k_slice=1, act=(0, 100)))
|
|
compute("clamp_relu6", lambda rng, job: _clamp_like(
|
|
rng, job, h=3, w=4, cin=5, zp=-128, tile=(3, 1), k_slice=1, act=(0, 6)))
|
|
|
|
# -- V012: M/N tails and channel edges
|
|
for tail in (1, 7, 8, 9):
|
|
compute(f"conv_tail{tail}",
|
|
lambda rng, job, tail=tail: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=5, w=max(3, tail), cin=tail, cout=tail,
|
|
kh=2, kw=2, sh=1, sw=1, pads=(0, 1, 1, 0), zin=0, zout=0,
|
|
tile=(2, 2), k_slice=min(7, 4 * tail), act=(-128, 127)))
|
|
# -- K multi-chunk carry: full reductions 1023/1024/1025 with 1024-sized slices
|
|
compute("conv_k1023", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=12, w=12, cin=31, cout=4, kh=3, kw=11, sh=1, sw=1,
|
|
pads=(0, 0, 0, 0), zin=0, zout=0, tile=(2, 2), k_slice=1023, act=(-128, 127)))
|
|
compute("conv_k1024", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=12, w=12, cin=32, cout=4, kh=4, kw=8, sh=1, sw=1,
|
|
pads=(0, 0, 0, 0), zin=0, zout=0, tile=(2, 4), k_slice=1024, act=(-128, 127)))
|
|
compute("conv_k1025", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=24, w=24, cin=41, cout=4, kh=5, kw=5, sh=1, sw=1,
|
|
pads=(0, 0, 0, 0), zin=0, zout=0, tile=(2, 4), k_slice=1024, act=(-128, 127)))
|
|
# -- K below/above the A-half reuse boundary
|
|
compute("conv_reuse_edge", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=16, w=16, cin=8, cout=24, kh=4, kw=4, sh=1, sw=1,
|
|
pads=(1, 1, 1, 1), zin=0, zout=0, tile=(2, 2), k_slice=128, act=(-128, 127)))
|
|
compute("conv_noreuse", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=8, w=8, cin=32, cout=16, kh=5, kw=5, sh=1, sw=1,
|
|
pads=(0, 0, 0, 0), zin=0, zout=0, tile=(2, 2), k_slice=200, act=(-128, 127)))
|
|
# -- asymmetric zero-point padding
|
|
compute("conv_asym_zp", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=5, w=4, cin=3, cout=3, kh=3, kw=3, sh=1, sw=1,
|
|
pads=(2, 0, 1, 2), zin=127, zout=-128, tile=(2, 2), k_slice=9, act=(-128, 127)))
|
|
# -- 4096-class maximum legal geometries that fit the private storage
|
|
compute("gap_c4096", lambda rng, job: _gap_like(
|
|
rng, job, h=1, w=1, cin=4096, zp=0, tile=(1, 1), k_slice=1, act=(-128, 127)))
|
|
compute("clamp_c4096", lambda rng, job: _clamp_like(
|
|
rng, job, h=1, w=1, cin=4096, zp=127, tile=(1, 1), k_slice=1, act=(0, 127)))
|
|
compute("conv_c4096_w16k", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=1, w=1, cin=4, cout=4096, kh=1, kw=1, sh=1, sw=1,
|
|
pads=(0, 0, 0, 0), zin=0, zout=0, tile=(1, 1), k_slice=4, act=(-128, 127)))
|
|
# The 64 KiB parameter tensor spans eight local windows. A second staging
|
|
# pass must rewind to group zero instead of reusing the final window.
|
|
compute("conv_c4096_m3", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=1, w=3, cin=4, cout=4096, kh=1, kw=1, sh=1, sw=1,
|
|
pads=(0, 0, 0, 0), zin=0, zout=0, tile=(1, 3), k_slice=4, act=(-128, 127)))
|
|
# More than the two 8 KiB W halves must stream by group/K slice without
|
|
# later external chunks overwriting the weights of earlier groups.
|
|
compute("conv_pw_w32k", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=1, w=2, cin=128, cout=256, kh=1, kw=1,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=-128, zout=-128, tile=(1, 2),
|
|
k_slice=37, act=(-128, 127)))
|
|
compute("gap_multichunk", lambda rng, job: _gap_like(
|
|
rng, job, h=64, w=32, cin=8, zp=0, tile=(1, 1), k_slice=1000, act=(-128, 127)))
|
|
# -- strided rows (bank-heavy access patterns)
|
|
compute("conv_strided", lambda rng, job: _conv_like(
|
|
rng, job, opcode=OP_CONV2D, h=5, w=5, cin=4, cout=4, kh=3, kw=3, sh=1, sw=1,
|
|
pads=(1, 1, 1, 1), zin=0, zout=0, tile=(2, 2), k_slice=36, act=(-128, 127),
|
|
irow=5 * 4 + 13, orow=5 * 4 + 7))
|
|
# -- equivalent-legal-tiling: same tensor, different TILE_HW/K_SLICE
|
|
def _tiling_pair(rng: _Rng, job: _Job, tile, k_slice) -> None:
|
|
shape = dict(opcode=OP_CONV2D, h=6, w=6, cin=4, cout=8, kh=3, kw=3, sh=1, sw=1,
|
|
pads=(1, 1, 1, 1), zin=0, zout=0, k_slice=k_slice, act=(-128, 127))
|
|
saved = rng.state
|
|
_conv_like(rng, job, tile=tile, **shape)
|
|
rng.state = saved
|
|
compute("tile_2x2", lambda rng, job: _tiling_pair(rng, job, (2, 2), 36))
|
|
compute("tile_1x6", lambda rng, job: _tiling_pair(rng, job, (1, 6), 36))
|
|
compute("tile_6x1_ks7", lambda rng, job: _tiling_pair(rng, job, (6, 1), 7))
|
|
|
|
# -- fixed-model operator shapes: KWS conv10x4 + DW3x3/PW1x1 pair + GAP25x5 + FC64->12
|
|
def _kws_model(rng: _Rng, job: _Job) -> None:
|
|
_conv_like(rng, job, opcode=OP_CONV2D, h=13, w=8, cin=1, cout=8, kh=10, kw=4,
|
|
sh=2, sw=2, pads=(0, 0, 0, 0), zin=-5, zout=17, tile=(2, 2),
|
|
k_slice=40, act=(-128, 127))
|
|
_dw_like(rng, job, h=2, w=3, cin=8, sh=1, sw=1, pads=(1, 1, 1, 1), zin=-5,
|
|
zout=11, tile=(2, 2), k_slice=9, act=(-128, 127))
|
|
_conv_like(rng, job, opcode=OP_CONV2D, h=2, w=3, cin=8, cout=16, kh=1, kw=1,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=-5, zout=3, tile=(2, 2), k_slice=8,
|
|
act=(-128, 127))
|
|
_gap_like(rng, job, h=25, w=5, cin=64, zp=-2, tile=(1, 1), k_slice=125,
|
|
act=(-128, 127))
|
|
_conv_like(rng, job, opcode=OP_FULLY_CONNECTED, h=1, w=1, cin=64, cout=12, kh=1,
|
|
kw=1, sh=1, sw=1, pads=(0, 0, 0, 0), zin=3, zout=-7, tile=(1, 1),
|
|
k_slice=64, act=(-128, 127))
|
|
compute("kws_model", _kws_model)
|
|
|
|
# -- fixed-model operator shapes: VWW conv3x3/s2 + DW/PW ladder + GAP3x3 + FC256->2
|
|
def _vww_model(rng: _Rng, job: _Job) -> None:
|
|
_conv_like(rng, job, opcode=OP_CONV2D, h=96, w=96, cin=3, cout=8, kh=3, kw=3,
|
|
sh=2, sw=2, pads=(1, 1, 1, 1), zin=-1, zout=9, tile=(2, 4), k_slice=27,
|
|
act=(-128, 127))
|
|
_dw_like(rng, job, h=48, w=48, cin=8, sh=1, sw=1, pads=(1, 1, 1, 1), zin=-1,
|
|
zout=5, tile=(2, 4), k_slice=9, act=(-128, 127))
|
|
_conv_like(rng, job, opcode=OP_CONV2D, h=48, w=48, cin=8, cout=16, kh=1, kw=1,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=-1, zout=13, tile=(2, 2), k_slice=8,
|
|
act=(-128, 127))
|
|
_gap_like(rng, job, h=3, w=3, cin=16, zp=0, tile=(1, 1), k_slice=9,
|
|
act=(-128, 127))
|
|
_conv_like(rng, job, opcode=OP_FULLY_CONNECTED, h=1, w=1, cin=256, cout=2, kh=1,
|
|
kw=1, sh=1, sw=1, pads=(0, 0, 0, 0), zin=0, zout=0, tile=(1, 1),
|
|
k_slice=256, act=(-128, 127))
|
|
compute("vww_model", _vww_model)
|
|
|
|
# -- randomized legal shapes with recorded seeds
|
|
for index in range(12):
|
|
seed = 0xC0FFEE00 + index * 7919
|
|
rng = _Rng(seed)
|
|
opcode = rng.pick((OP_CONV2D, OP_DEPTHWISE3X3, OP_MAX_POOL, OP_AVERAGE_POOL,
|
|
OP_CLAMP, OP_ADD))
|
|
|
|
def _rand(rng: _Rng, job: _Job, opcode=opcode, seed=seed) -> None:
|
|
rng.state = seed
|
|
if opcode == OP_CONV2D:
|
|
kh = 1 + rng.next() % 5
|
|
kw = 1 + rng.next() % 5
|
|
sh = 1 + rng.next() % 2
|
|
sw = 1 + rng.next() % 2
|
|
pads = (rng.next() % kh, rng.next() % kh, rng.next() % kw, rng.next() % kw)
|
|
h = kh + rng.next() % 8
|
|
w = kw + rng.next() % 8
|
|
oh = (h + pads[0] + pads[1] - kh) // sh + 1
|
|
ow = (w + pads[2] + pads[3] - kw) // sw + 1
|
|
if oh < 1 or ow < 1:
|
|
h, w = kh, kw
|
|
oh, ow = 1, 1
|
|
pads = (0, 0, 0, 0)
|
|
tile = _tile(1 + rng.next() % 3, 1 + rng.next() % 3, oh, ow)
|
|
cin = 1 + rng.next() % 17
|
|
_conv_like(rng, job, opcode=opcode, h=h, w=w, cin=cin,
|
|
cout=1 + rng.next() % 17, kh=kh, kw=kw, sh=sh, sw=sw,
|
|
pads=pads, zin=rng.int8(), zout=rng.int8(),
|
|
tile=tile,
|
|
k_slice=min(1 + rng.next() % 40, kh * kw * cin),
|
|
act=(-128, 127))
|
|
elif opcode == OP_DEPTHWISE3X3:
|
|
sh = 1 + rng.next() % 2
|
|
sw = 1 + rng.next() % 2
|
|
pads = (rng.next() % 3, rng.next() % 3, rng.next() % 3, rng.next() % 3)
|
|
h = 3 + rng.next() % 8
|
|
w = 3 + rng.next() % 8
|
|
oh = (h + pads[0] + pads[1] - 3) // sh + 1
|
|
ow = (w + pads[2] + pads[3] - 3) // sw + 1
|
|
tile = _tile(1 + rng.next() % 3, 1 + rng.next() % 3, oh, ow)
|
|
_dw_like(rng, job, h=h, w=w,
|
|
cin=1 + rng.next() % 17, sh=sh, sw=sw, pads=pads,
|
|
zin=rng.int8(), zout=rng.int8(),
|
|
tile=tile,
|
|
k_slice=1 + rng.next() % 9, act=(-128, 127))
|
|
elif opcode in (OP_MAX_POOL, OP_AVERAGE_POOL):
|
|
kh = 1 + rng.next() % 4
|
|
kw = 1 + rng.next() % 4
|
|
sh = 1 + rng.next() % 2
|
|
sw = 1 + rng.next() % 2
|
|
pads = (rng.next() % kh, rng.next() % kh, rng.next() % kw, rng.next() % kw)
|
|
zp = rng.int8()
|
|
h = kh + rng.next() % 8
|
|
w = kw + rng.next() % 8
|
|
oh = (h + pads[0] + pads[1] - kh) // sh + 1
|
|
ow = (w + pads[2] + pads[3] - kw) // sw + 1
|
|
tile = _tile(1 + rng.next() % 3, 1 + rng.next() % 3, oh, ow)
|
|
_pool_like(rng, job, opcode=opcode, h=h,
|
|
w=w, cin=1 + rng.next() % 17, kh=kh, kw=kw,
|
|
sh=sh, sw=sw, pads=pads, zp=zp,
|
|
tile=tile,
|
|
k_slice=min(1 + rng.next() % 16, kh * kw), act=(-128, 127))
|
|
elif opcode == OP_ADD:
|
|
h = 1 + rng.next() % 6
|
|
w = 1 + rng.next() % 6
|
|
_add_like(rng, job, h=h, w=w,
|
|
cin=1 + rng.next() % 17, z0=rng.int8(), z1=rng.int8(),
|
|
zout=rng.int8(),
|
|
tile=_tile(1 + rng.next() % 3, 1 + rng.next() % 3, h, w),
|
|
k_slice=1, act=(-128, 127))
|
|
else:
|
|
zp = rng.int8()
|
|
h = 1 + rng.next() % 8
|
|
w = 1 + rng.next() % 8
|
|
_clamp_like(rng, job, h=h, w=w,
|
|
cin=1 + rng.next() % 33, zp=zp,
|
|
tile=_tile(1 + rng.next() % 3, 1 + rng.next() % 3, h, w),
|
|
k_slice=1,
|
|
act=(rng.pick((-128, -20, 0)), rng.pick((6, 100, 127))))
|
|
compute(f"rand_{index:02d}_{seed:08x}", _rand)
|
|
|
|
# -- validation rejections: fail BEFORE execution with correct codes
|
|
reject("rej_stride3", OP_CONV2D,
|
|
lambda w: w.__setitem__(12, (w[12] & 0xFF00_FFFF) | (3 << 16)))
|
|
reject("rej_dw_mult", OP_DEPTHWISE3X3,
|
|
lambda w: w.__setitem__(7, w[7] + (1 << 16)))
|
|
reject("rej_add_bcast", OP_ADD, lambda w: w.__setitem__(8, w[8] + 1))
|
|
reject("rej_clamp_shape", OP_CLAMP, lambda w: w.__setitem__(8, w[8] + 1))
|
|
reject("rej_pool_zp", OP_CLAMP, lambda w: w.__setitem__(22, (w[22] + 1) & 0xFF))
|
|
|
|
# -- arithmetic faults: checked INT32 accumulate overflow (code 7)
|
|
def _arith_overflow(rng: _Rng, job: _Job) -> None:
|
|
from npu_reference import INT32_MAX
|
|
bias = [INT32_MAX] + [rng.int32() for _ in range(7)]
|
|
mults = [rng.nonneg31() for _ in range(8)]
|
|
shifts = [rng.shift() for _ in range(8)]
|
|
_conv_like(rng, job, opcode=OP_CONV2D, h=4, w=4, cin=4, cout=8, kh=2, kw=2,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=-128, zout=0, tile=(2, 2),
|
|
k_slice=16, act=(-128, 127), params=(bias, mults, shifts),
|
|
allow_fault=True)
|
|
job.fcode = F_ARITHMETIC
|
|
job.fdesc = 0
|
|
job.faddr = 0
|
|
job.scen = SCEN_ARITH
|
|
compute("arith_acc_overflow", _arith_overflow)
|
|
|
|
# -- arithmetic fault: checked requantizer left shift (code 7)
|
|
def _arith_shift(rng: _Rng, job: _Job) -> None:
|
|
bias = [1 << 20] * 8
|
|
mults = [1 << 20] * 8
|
|
shifts = [30] * 8
|
|
_conv_like(rng, job, opcode=OP_CONV2D, h=3, w=3, cin=2, cout=8, kh=2, kw=2,
|
|
sh=1, sw=1, pads=(0, 0, 0, 0), zin=-128, zout=0, tile=(1, 1),
|
|
k_slice=8, act=(-128, 127), params=(bias, mults, shifts),
|
|
allow_fault=True)
|
|
job.fcode = F_ARITHMETIC
|
|
job.fdesc = 0
|
|
job.faddr = 0
|
|
job.scen = SCEN_ARITH
|
|
compute("arith_shift_overflow", _arith_shift)
|
|
|
|
return cases
|
|
|
|
|
|
@pytest.mark.parametrize("simulator", ("iverilog", "verilator"))
|
|
def test_npu_operator(tmp_path: Path, simulator: str) -> None:
|
|
tools = _tools()
|
|
command = _build_sim(
|
|
tools,
|
|
simulator,
|
|
tmp_path,
|
|
f"npu_operator_{simulator}",
|
|
"npu_operator_tb",
|
|
OPERATOR_TB,
|
|
P4_SOURCES,
|
|
)
|
|
for plusargs in _operator_cases(tmp_path):
|
|
_run(command, plusargs, "NPU operator test passed")
|