mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
- Removed unsupported fault handling from and replaced it with a test for average pooling execution. - Updated the test for unsupported placements to validate average pooling and depthwise convolution placements. - Introduced new tests for softmax functionality in , including parameter validation and edge cases. - Added qualification tests to ensure rejection of common-mode numerical errors and incomplete runs in . - Enhanced RTL tests to cover memory downsizing and kernel burst capabilities in and . - Adjusted boot status assertions in to reflect updated expected values.
1607 lines
62 KiB
Python
1607 lines
62 KiB
Python
"""Proof-of-lowering compiler for the locked NPU-P0 workload graphs.
|
|
|
|
Lowers the pinned MLPerf Tiny KWS and VWW INT8 graphs (scripts/npu_model.py)
|
|
into Mini NPU descriptor ABI 1.0 sequences (scripts/npu_descriptors.py) with
|
|
packed weights/params, a bounded 64-byte-aligned arena, region-relative
|
|
address words plus relocations, and the P0 lowering report. The normative
|
|
rules are docs/ip/npu.md "Private storage and packing", "Operator and
|
|
numerical profile 1", "Descriptor ABI 1.0" and "Compiler artifacts and C HAL".
|
|
|
|
Lowering map (anything else raises CompilerError -- never a silent skip):
|
|
CONV_2D -> opcode 1 CONV2D (dilation 1, stride 1/2, SAME/VALID)
|
|
DEPTHWISE_CONV_2D -> opcode 2 DEPTHWISE3X3 (3x3, multiplier 1)
|
|
FULLY_CONNECTED -> opcode 3 (per-tensor or per-channel weight scale)
|
|
AVERAGE_POOL_2D -> opcode 7 GLOBAL_AVERAGE_POOL (VALID whole-input pool)
|
|
RESHAPE -> no runtime work (storage-preserving alias)
|
|
SOFTMAX -> explicit terminal CPU step (pinned integer profile)
|
|
|
|
Weight packing: dense [ceil(Cout/8)][K][8] bytes in kh,kw,cin reduction order
|
|
(FC uses Kh=Kw=1); channel c sits at group c//8 lane c%8 with zero tail
|
|
lanes; depthwise is [ceil(Cin/8)][9][8] in kh/kw order with zero channel
|
|
tails. Params are per-output-channel 16-byte records {int32 bias, int32
|
|
multiplier, int32 shift, uint32 0}; multiplier/shift come from the pinned
|
|
TFLite QuantizeMultiplier semantics, so a denormal-magnitude scale flushes
|
|
to multiplier=0/shift=0 and yields a constant-zout channel (VWW carries such
|
|
channels; they lower cleanly).
|
|
|
|
Arena: one contiguous byte arena per model. The input image is pinned for
|
|
the whole job; layer outputs come from a 64-byte-aligned first-fit free list
|
|
and are freed once their last consumer has been emitted, so an operator's
|
|
input and output allocations are always disjoint. Arena peak bytes are
|
|
reported. Descriptor address words hold region-relative offsets; the
|
|
manifest carries {descriptor_index, word_index, region, offset} relocations
|
|
for every nonzero semantic pointer, exactly one per pointer.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import math
|
|
import struct
|
|
import subprocess
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Final
|
|
|
|
from npu_descriptors import (
|
|
ABI_VERSION,
|
|
DESCRIPTOR_BYTES,
|
|
MAX_K_SLICE,
|
|
OP_ADD,
|
|
OP_AVERAGE_POOL,
|
|
OP_CLAMP,
|
|
OP_CONV2D,
|
|
OP_DEPTHWISE3X3,
|
|
OP_FULLY_CONNECTED,
|
|
OP_GLOBAL_AVERAGE_POOL,
|
|
OP_MAX_POOL,
|
|
OPCODE_NAMES,
|
|
Descriptor,
|
|
DescriptorError,
|
|
used_span,
|
|
validate_descriptor,
|
|
)
|
|
from npu_model import (
|
|
KWS_TFLITE_SHA256,
|
|
REPO_ROOT,
|
|
VWW_TFLITE_SHA256,
|
|
GraphInfo,
|
|
TensorInfo,
|
|
load_kws_model,
|
|
load_vww_model,
|
|
)
|
|
from npu_reference import (
|
|
SOFTMAX_OUTPUT_SCALE,
|
|
SOFTMAX_OUTPUT_ZERO_POINT,
|
|
Tensor,
|
|
add,
|
|
average_pool,
|
|
clamp,
|
|
conv2d,
|
|
depthwise_conv2d,
|
|
fully_connected,
|
|
global_average_pool,
|
|
max_pool,
|
|
quantize_multiplier,
|
|
softmax_int8,
|
|
softmax_parameters,
|
|
)
|
|
|
|
CONTRACT_REVISION: Final = "npu-p0/1.0.2"
|
|
NUMERIC_PROFILE: Final = 1
|
|
# CAPABILITY bit 1 EXECUTION_READY | bit 4 DOUBLE_ROUNDING (docs/ip/npu.md).
|
|
REQUIRED_CAPABILITY_MASK: Final = 0x12
|
|
REQUIRED_CAPABILITY_BITS: Final = ("EXECUTION_READY", "DOUBLE_ROUNDING")
|
|
ARENA_ALIGNMENT: Final = 64
|
|
MAX_BUFFER_HALF_BYTES: Final = 8192
|
|
PARAM_RECORD_BYTES: Final = 16
|
|
|
|
KWS_CORPUS_FIRST: Final = (
|
|
REPO_ROOT / ".cache/retrosoc/sources/apu-kws-mfcc/datasets/kws01/tst_000000_Stop_7.bin"
|
|
)
|
|
VWW_CORPUS_FIRST: Final = (
|
|
REPO_ROOT / ".cache/retrosoc/sources/npu-vww-corpus/person/000000343218.bin"
|
|
)
|
|
|
|
# The compiler self-check finalizes templates against the same documented
|
|
# region bases that npu_executor.DEFAULT_BASES defines (values duplicated on
|
|
# purpose so the two modules stay independently importable).
|
|
_SELF_CHECK_BASES: Final = {
|
|
"arena": 0x1000_0000,
|
|
"weights": 0x2000_0000,
|
|
"params": 0x3000_0000,
|
|
"descriptors": 0x4000_0000,
|
|
}
|
|
|
|
_CYCLE_MODEL: Final = (
|
|
"estimate: ceil(useful_macs/8) compute + ceil(gather_bytes/8) pack + "
|
|
"ceil((weight+param bytes)/8) load + ceil(output_write_bytes/8) store; an "
|
|
"8-MAC/cycle dense array with no DMA/compute overlap credit; labeled "
|
|
"estimate, not measured counters"
|
|
)
|
|
|
|
_OPCODE_FOR_KIND: Final = {
|
|
"conv2d": OP_CONV2D,
|
|
"depthwise3x3": OP_DEPTHWISE3X3,
|
|
"fully_connected": OP_FULLY_CONNECTED,
|
|
"add": OP_ADD,
|
|
"max_pool": OP_MAX_POOL,
|
|
"average_pool": OP_AVERAGE_POOL,
|
|
"global_average_pool": OP_GLOBAL_AVERAGE_POOL,
|
|
"clamp": OP_CLAMP,
|
|
}
|
|
|
|
|
|
class CompilerError(ValueError):
|
|
"""A graph operator or option has no supported NPU-P0 placement."""
|
|
|
|
|
|
@dataclass
|
|
class CompiledJob:
|
|
"""One lowered model: descriptor templates plus packed constant regions.
|
|
|
|
descriptors hold region-relative offsets in their address words (the
|
|
emitted TEMPLATE); relocations record {descriptor_index, word_index,
|
|
region, offset} for words 2/3/4/5/24 as applicable. The executor's
|
|
finalize step adds caller-provided physical bases with checked arithmetic.
|
|
"""
|
|
|
|
descriptors: list[Descriptor]
|
|
weights: bytes
|
|
params: bytes
|
|
arena_bytes: int
|
|
input_offset: int
|
|
relocations: list[dict]
|
|
steps: list[dict]
|
|
report: dict
|
|
|
|
|
|
def _require(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise CompilerError(message)
|
|
|
|
|
|
def _signed(data: bytes) -> list[int]:
|
|
return [value - 256 if value >= 128 else value for value in data]
|
|
|
|
|
|
def _digest(tensor: Tensor) -> str:
|
|
return hashlib.sha256(bytes(value & 0xFF for value in tensor.data)).hexdigest()
|
|
|
|
|
|
def _hwc(tensor: TensorInfo) -> tuple[int, int, int]:
|
|
"""Normalize a batch-one activation tensor to (H, W, C)."""
|
|
shape = tensor.shape
|
|
if len(shape) == 4 and shape[0] == 1:
|
|
return shape[1], shape[2], shape[3]
|
|
if len(shape) == 2 and shape[0] == 1:
|
|
return 1, 1, shape[1]
|
|
raise CompilerError(f"tensor {tensor.name!r} shape {shape} is not batch-one NHWC")
|
|
|
|
|
|
def _quantize_activation(value: float, scale: float, zero_point: int) -> int:
|
|
scaled = value / scale
|
|
rounded = int(math.floor(scaled + 0.5)) if scaled >= 0 else int(math.ceil(scaled - 0.5))
|
|
return max(-128, min(127, rounded + zero_point))
|
|
|
|
|
|
def _activation(options: dict, scale: float, zero_point: int) -> tuple[int, int]:
|
|
fused = options.get("fused_activation", "NONE")
|
|
if fused == "RELU":
|
|
return max(-128, zero_point), 127
|
|
if fused == "RELU6":
|
|
return max(-128, zero_point), _quantize_activation(6.0, scale, zero_point)
|
|
if fused == "NONE":
|
|
return -128, 127
|
|
raise CompilerError(f"fused activation {fused} has no supported NPU-P0 placement")
|
|
|
|
|
|
def _check_kernel_options(op_name: str, index: int, options: dict) -> None:
|
|
_require(
|
|
options.get("dilation_h", 1) == 1 and options.get("dilation_w", 1) == 1,
|
|
f"operator {index} ({op_name}): dilation other than 1 is unsupported",
|
|
)
|
|
_require(
|
|
options["stride_h"] in (1, 2) and options["stride_w"] in (1, 2),
|
|
f"operator {index} ({op_name}): stride must be 1 or 2",
|
|
)
|
|
|
|
|
|
def _same_axis(extent: int, kernel: int, stride: int, out_expected: int) -> tuple[int, int]:
|
|
out = -(-extent // stride)
|
|
_require(
|
|
out == out_expected,
|
|
f"SAME padding output {out_expected} does not match ceil({extent}/{stride})",
|
|
)
|
|
total = max((out - 1) * stride + kernel - extent, 0)
|
|
return total // 2, total - total // 2
|
|
|
|
|
|
def _pads(options: dict, h: int, w: int, kh: int, kw: int, oh: int, ow: int) -> tuple[int, ...]:
|
|
sh, sw = options["stride_h"], options["stride_w"]
|
|
kind = options["padding"]
|
|
if kind == "VALID":
|
|
pads = (0, 0, 0, 0)
|
|
elif kind == "SAME":
|
|
pad_top, pad_bottom = _same_axis(h, kh, sh, oh)
|
|
pad_left, pad_right = _same_axis(w, kw, sw, ow)
|
|
pads = (pad_top, pad_bottom, pad_left, pad_right)
|
|
else:
|
|
raise CompilerError(f"padding mode {kind} has no supported NPU-P0 placement")
|
|
pad_top, pad_bottom, pad_left, pad_right = pads
|
|
expect_oh = (h + pad_top + pad_bottom - kh) // sh + 1
|
|
expect_ow = (w + pad_left + pad_right - kw) // sw + 1
|
|
_require(
|
|
(expect_oh, expect_ow) == (oh, ow),
|
|
f"output extent {oh}x{ow} does not match computed {expect_oh}x{expect_ow}",
|
|
)
|
|
_require(
|
|
pad_top < kh and pad_bottom < kh and pad_left < kw and pad_right < kw,
|
|
"padding must be strictly less than the kernel dimension",
|
|
)
|
|
return pads
|
|
|
|
|
|
def _activation_quant(tensor: TensorInfo, role: str) -> tuple[float, int]:
|
|
_require(tensor.dtype == 9, f"{role} tensor {tensor.name!r} must be INT8")
|
|
_require(
|
|
len(tensor.scales) == 1 and len(tensor.zero_points) == 1,
|
|
f"{role} tensor {tensor.name!r} must use per-tensor quantization",
|
|
)
|
|
return float(tensor.scales[0]), int(tensor.zero_points[0])
|
|
|
|
|
|
def _weight_values(tensor: TensorInfo, cout: int, quantized_dimension: int) -> list[int]:
|
|
_require(tensor.dtype == 9, f"weight tensor {tensor.name!r} must be INT8")
|
|
_require(
|
|
tensor.quantized_dimension == quantized_dimension,
|
|
f"weight tensor {tensor.name!r} quantized dimension is not {quantized_dimension}",
|
|
)
|
|
_require(
|
|
len(tensor.scales) == cout and len(tensor.zero_points) == cout,
|
|
f"weight tensor {tensor.name!r} must carry per-channel scales",
|
|
)
|
|
_require(
|
|
all(zero == 0 for zero in tensor.zero_points),
|
|
f"weight tensor {tensor.name!r} must be symmetric (zero point 0)",
|
|
)
|
|
_require(tensor.data is not None, f"weight tensor {tensor.name!r} has no data")
|
|
values = _signed(tensor.data)
|
|
for value in values:
|
|
_require(
|
|
value != -128,
|
|
f"weight tensor {tensor.name!r} holds -128, which packed weights forbid",
|
|
)
|
|
return values
|
|
|
|
|
|
def _bias_values(tensor: TensorInfo, cout: int) -> list[int]:
|
|
_require(tensor.dtype == 2, f"bias tensor {tensor.name!r} must be INT32")
|
|
_require(tensor.data is not None, f"bias tensor {tensor.name!r} has no data")
|
|
_require(
|
|
len(tensor.data) == 4 * cout,
|
|
f"bias tensor {tensor.name!r} must hold {cout} INT32 values",
|
|
)
|
|
return list(struct.unpack(f"<{cout}i", tensor.data))
|
|
|
|
|
|
def _channel_quant(
|
|
input_scale: float, weight_scales: tuple[float, ...], output_scale: float, cout: int
|
|
) -> tuple[list[int], list[int]]:
|
|
"""Per-channel Q31 multiplier/shift pairs; denormal scales flush to (0, 0)."""
|
|
_require(
|
|
len(weight_scales) in (1, cout),
|
|
"weight scales must be per-tensor or per-output-channel",
|
|
)
|
|
multipliers: list[int] = []
|
|
shifts: list[int] = []
|
|
for channel in range(cout):
|
|
weight_scale = weight_scales[0] if len(weight_scales) == 1 else weight_scales[channel]
|
|
multiplier, shift = quantize_multiplier(input_scale * weight_scale / output_scale)
|
|
multipliers.append(multiplier)
|
|
shifts.append(shift)
|
|
return multipliers, shifts
|
|
|
|
|
|
def _pack_dense_weights(values: list[int], cout: int, full_k: int) -> bytes:
|
|
"""[ceil(Cout/8)][K][8] in kh,kw,cin order; channel c -> group c//8 lane c%8."""
|
|
_require(
|
|
len(values) == cout * full_k,
|
|
f"dense weights hold {len(values)} values, expected {cout * full_k}",
|
|
)
|
|
packed = bytearray(((cout + 7) // 8) * full_k * 8)
|
|
for channel in range(cout):
|
|
base = (channel // 8) * full_k * 8 + channel % 8
|
|
source = channel * full_k
|
|
for k in range(full_k):
|
|
packed[base + k * 8] = values[source + k] & 0xFF
|
|
return bytes(packed)
|
|
|
|
|
|
def _pack_depthwise_weights(values: list[int], cin: int) -> bytes:
|
|
"""[ceil(Cin/8)][9][8] in kh,kw order with zero channel tails."""
|
|
_require(
|
|
len(values) == 9 * cin,
|
|
f"depthwise weights hold {len(values)} values, expected {9 * cin}",
|
|
)
|
|
packed = bytearray(((cin + 7) // 8) * 72)
|
|
for channel in range(cin):
|
|
base = (channel // 8) * 72 + channel % 8
|
|
for k in range(9):
|
|
packed[base + k * 8] = values[k * cin + channel] & 0xFF
|
|
return bytes(packed)
|
|
|
|
|
|
def _pack_params(bias: list[int], multipliers: list[int], shifts: list[int]) -> bytes:
|
|
return b"".join(
|
|
struct.pack("<iiiI", bias[c], multipliers[c], shifts[c], 0)
|
|
for c in range(len(bias))
|
|
)
|
|
|
|
|
|
def _pack_add_params(scale0: float, scale1: float, output_scale: float) -> bytes:
|
|
common = max(scale0, scale1)
|
|
m0, s0 = quantize_multiplier(scale0 / (2.0 * common))
|
|
m1, s1 = quantize_multiplier(scale1 / (2.0 * common))
|
|
mout, sout = quantize_multiplier((2.0 * common) / ((1 << 20) * output_scale))
|
|
return struct.pack("<IiiiiiiI", 20, m0, s0, m1, s1, mout, sout, 0)
|
|
|
|
|
|
def _tile(oh: int, ow: int) -> tuple[int, int]:
|
|
return min(2, oh), min(4, ow)
|
|
|
|
|
|
def _k_sizes(full_k: int, k_slice: int) -> list[int]:
|
|
return [min(k_slice, full_k - start) for start in range(0, full_k, k_slice)]
|
|
|
|
|
|
@dataclass
|
|
class _Plan:
|
|
op_index: int
|
|
op_name: str
|
|
kind: str
|
|
input_tensor: int
|
|
output_tensor: int
|
|
input1_tensor: int | None = None
|
|
h: int = 0
|
|
w: int = 0
|
|
cin: int = 0
|
|
cout: int = 0
|
|
oh: int = 0
|
|
ow: int = 0
|
|
kh: int = 0
|
|
kw: int = 0
|
|
sh: int = 0
|
|
sw: int = 0
|
|
pads: tuple[int, ...] = (0, 0, 0, 0)
|
|
act_min: int = -128
|
|
act_max: int = 127
|
|
in_scale: float = 1.0
|
|
in_zp: int = 0
|
|
input1_scale: float = 1.0
|
|
input1_zp: int = 0
|
|
out_scale: float = 1.0
|
|
out_zp: int = 0
|
|
full_k: int = 1
|
|
k_slice: int = 1
|
|
weights: bytes | None = None
|
|
params: bytes | None = None
|
|
softmax: dict[str, int] | None = None
|
|
|
|
|
|
def _lower_conv2d(graph: GraphInfo, index: int) -> _Plan:
|
|
op = graph.operators[index]
|
|
options = op.options
|
|
_check_kernel_options(op.op_name, index, options)
|
|
_require(len(op.inputs) == 3, f"operator {index} (CONV_2D) needs input/weights/bias")
|
|
src, weight_t, bias_t = (graph.tensors[t] for t in op.inputs)
|
|
out_t = graph.tensors[op.outputs[0]]
|
|
h, w, cin = _hwc(src)
|
|
oh, ow, cout = _hwc(out_t)
|
|
_require(len(weight_t.shape) == 4, f"operator {index}: conv weights must be 4-D")
|
|
wcout, kh, kw, wcin = weight_t.shape
|
|
_require(
|
|
(wcout, wcin) == (cout, cin),
|
|
f"operator {index}: weight shape {weight_t.shape} mismatches {cin}->{cout}",
|
|
)
|
|
in_scale, in_zp = _activation_quant(src, "input")
|
|
out_scale, out_zp = _activation_quant(out_t, "output")
|
|
values = _weight_values(weight_t, cout, 0)
|
|
bias = _bias_values(bias_t, cout)
|
|
multipliers, shifts = _channel_quant(in_scale, weight_t.scales, out_scale, cout)
|
|
full_k = kh * kw * cin
|
|
return _Plan(
|
|
op_index=index,
|
|
op_name=op.op_name,
|
|
kind="conv2d",
|
|
input_tensor=op.inputs[0],
|
|
output_tensor=op.outputs[0],
|
|
h=h,
|
|
w=w,
|
|
cin=cin,
|
|
cout=cout,
|
|
oh=oh,
|
|
ow=ow,
|
|
kh=kh,
|
|
kw=kw,
|
|
sh=options["stride_h"],
|
|
sw=options["stride_w"],
|
|
pads=_pads(options, h, w, kh, kw, oh, ow),
|
|
act_min=_activation(options, out_scale, out_zp)[0],
|
|
act_max=_activation(options, out_scale, out_zp)[1],
|
|
in_scale=in_scale,
|
|
in_zp=in_zp,
|
|
out_scale=out_scale,
|
|
out_zp=out_zp,
|
|
full_k=full_k,
|
|
k_slice=min(MAX_K_SLICE, full_k),
|
|
weights=_pack_dense_weights(values, cout, full_k),
|
|
params=_pack_params(bias, multipliers, shifts),
|
|
)
|
|
|
|
|
|
def _lower_depthwise(graph: GraphInfo, index: int) -> _Plan:
|
|
op = graph.operators[index]
|
|
options = op.options
|
|
_check_kernel_options(op.op_name, index, options)
|
|
_require(
|
|
options.get("depth_multiplier", 1) == 1,
|
|
f"operator {index} (DEPTHWISE_CONV_2D): depth multiplier must be 1",
|
|
)
|
|
_require(len(op.inputs) == 3, f"operator {index} (DEPTHWISE_CONV_2D) needs 3 inputs")
|
|
src, weight_t, bias_t = (graph.tensors[t] for t in op.inputs)
|
|
out_t = graph.tensors[op.outputs[0]]
|
|
h, w, cin = _hwc(src)
|
|
oh, ow, cout = _hwc(out_t)
|
|
_require(len(weight_t.shape) == 4, f"operator {index}: depthwise weights must be 4-D")
|
|
_, kh, kw, wchannels = weight_t.shape
|
|
_require(
|
|
(kh, kw) == (3, 3),
|
|
f"operator {index}: DEPTHWISE3X3 requires a 3x3 kernel, got {kh}x{kw}",
|
|
)
|
|
_require(
|
|
wchannels == cin and cout == cin,
|
|
f"operator {index}: depthwise multiplier 1 requires Cout == Cin",
|
|
)
|
|
in_scale, in_zp = _activation_quant(src, "input")
|
|
out_scale, out_zp = _activation_quant(out_t, "output")
|
|
values = _weight_values(weight_t, cin, 3)
|
|
bias = _bias_values(bias_t, cout)
|
|
multipliers, shifts = _channel_quant(in_scale, weight_t.scales, out_scale, cout)
|
|
return _Plan(
|
|
op_index=index,
|
|
op_name=op.op_name,
|
|
kind="depthwise3x3",
|
|
input_tensor=op.inputs[0],
|
|
output_tensor=op.outputs[0],
|
|
h=h,
|
|
w=w,
|
|
cin=cin,
|
|
cout=cout,
|
|
oh=oh,
|
|
ow=ow,
|
|
kh=3,
|
|
kw=3,
|
|
sh=options["stride_h"],
|
|
sw=options["stride_w"],
|
|
pads=_pads(options, h, w, 3, 3, oh, ow),
|
|
act_min=_activation(options, out_scale, out_zp)[0],
|
|
act_max=_activation(options, out_scale, out_zp)[1],
|
|
in_scale=in_scale,
|
|
in_zp=in_zp,
|
|
out_scale=out_scale,
|
|
out_zp=out_zp,
|
|
full_k=9,
|
|
k_slice=9,
|
|
weights=_pack_depthwise_weights(values, cin),
|
|
params=_pack_params(bias, multipliers, shifts),
|
|
)
|
|
|
|
|
|
def _lower_fully_connected(graph: GraphInfo, index: int) -> _Plan:
|
|
op = graph.operators[index]
|
|
options = op.options
|
|
_require(len(op.inputs) == 3, f"operator {index} (FULLY_CONNECTED) needs 3 inputs")
|
|
src, weight_t, bias_t = (graph.tensors[t] for t in op.inputs)
|
|
out_t = graph.tensors[op.outputs[0]]
|
|
h, w, cin = _hwc(src)
|
|
oh, ow, cout = _hwc(out_t)
|
|
_require(
|
|
(h, w, oh, ow) == (1, 1, 1, 1),
|
|
f"operator {index} (FULLY_CONNECTED) requires H=W=OH=OW=1",
|
|
)
|
|
_require(len(weight_t.shape) == 2, f"operator {index}: FC weights must be 2-D")
|
|
wcout, wcin = weight_t.shape
|
|
_require(
|
|
(wcout, wcin) == (cout, cin),
|
|
f"operator {index}: FC weight shape {weight_t.shape} mismatches {cin}->{cout}",
|
|
)
|
|
in_scale, in_zp = _activation_quant(src, "input")
|
|
out_scale, out_zp = _activation_quant(out_t, "output")
|
|
_require(weight_t.dtype == 9, f"operator {index}: FC weights must be INT8")
|
|
_require(
|
|
all(zero == 0 for zero in weight_t.zero_points),
|
|
f"operator {index}: FC weights must be symmetric (zero point 0)",
|
|
)
|
|
_require(weight_t.data is not None, f"operator {index}: FC weights have no data")
|
|
values = _signed(weight_t.data)
|
|
for value in values:
|
|
_require(value != -128, f"operator {index}: FC weights hold -128")
|
|
bias = _bias_values(bias_t, cout)
|
|
multipliers, shifts = _channel_quant(in_scale, weight_t.scales, out_scale, cout)
|
|
return _Plan(
|
|
op_index=index,
|
|
op_name=op.op_name,
|
|
kind="fully_connected",
|
|
input_tensor=op.inputs[0],
|
|
output_tensor=op.outputs[0],
|
|
h=1,
|
|
w=1,
|
|
cin=cin,
|
|
cout=cout,
|
|
oh=1,
|
|
ow=1,
|
|
kh=1,
|
|
kw=1,
|
|
sh=1,
|
|
sw=1,
|
|
pads=(0, 0, 0, 0),
|
|
act_min=_activation(options, out_scale, out_zp)[0],
|
|
act_max=_activation(options, out_scale, out_zp)[1],
|
|
in_scale=in_scale,
|
|
in_zp=in_zp,
|
|
out_scale=out_scale,
|
|
out_zp=out_zp,
|
|
full_k=cin,
|
|
k_slice=min(MAX_K_SLICE, cin),
|
|
weights=_pack_dense_weights(values, cout, cin),
|
|
params=_pack_params(bias, multipliers, shifts),
|
|
)
|
|
|
|
|
|
def _lower_global_average_pool(graph: GraphInfo, index: int) -> _Plan:
|
|
op = graph.operators[index]
|
|
options = op.options
|
|
_require(options.get("padding") == "VALID",
|
|
f"operator {index} (AVERAGE_POOL_2D): global pool must use VALID")
|
|
_require(len(op.inputs) == 1 and len(op.outputs) == 1,
|
|
f"operator {index} (AVERAGE_POOL_2D) requires one input and output")
|
|
src = graph.tensors[op.inputs[0]]
|
|
out_t = graph.tensors[op.outputs[0]]
|
|
h, w, cin = _hwc(src)
|
|
oh, ow, cout = _hwc(out_t)
|
|
_require((oh, ow, cout) == (1, 1, cin),
|
|
f"operator {index}: global pool must produce 1x1x{cin}")
|
|
_require(options["filter_height"] == h and options["filter_width"] == w
|
|
and options["stride_h"] == h and options["stride_w"] == w,
|
|
f"operator {index}: global pool filter/stride must cover {h}x{w}")
|
|
in_scale, in_zp = _activation_quant(src, "input")
|
|
out_scale, out_zp = _activation_quant(out_t, "output")
|
|
_require(in_scale == out_scale and in_zp == out_zp,
|
|
f"operator {index}: GLOBAL_AVERAGE_POOL requires equal quantization")
|
|
act_min, act_max = _activation(options, out_scale, out_zp)
|
|
return _Plan(
|
|
op_index=index, op_name=op.op_name, kind="global_average_pool",
|
|
input_tensor=op.inputs[0], output_tensor=op.outputs[0], h=h, w=w,
|
|
cin=cin, cout=cout, oh=1, ow=1, kh=0, kw=0, sh=0, sw=0,
|
|
act_min=act_min, act_max=act_max, in_scale=in_scale, in_zp=in_zp,
|
|
out_scale=out_scale, out_zp=out_zp, full_k=h * w,
|
|
k_slice=min(MAX_K_SLICE, h * w),
|
|
)
|
|
|
|
|
|
def _lower_add(graph: GraphInfo, index: int) -> _Plan:
|
|
op = graph.operators[index]
|
|
_require(len(op.inputs) == 2 and len(op.outputs) == 1,
|
|
f"operator {index} (ADD) requires two inputs and one output")
|
|
left, right = (graph.tensors[item] for item in op.inputs)
|
|
out_t = graph.tensors[op.outputs[0]]
|
|
h, w, cin = _hwc(left)
|
|
h1, w1, c1 = _hwc(right)
|
|
oh, ow, cout = _hwc(out_t)
|
|
_require((h1, w1, c1) == (h, w, cin) and (oh, ow, cout) == (h, w, cin),
|
|
f"operator {index} (ADD) does not support broadcasting")
|
|
scale0, z0 = _activation_quant(left, "ADD input0")
|
|
scale1, z1 = _activation_quant(right, "ADD input1")
|
|
out_scale, zout = _activation_quant(out_t, "ADD output")
|
|
act_min, act_max = _activation(op.options, out_scale, zout)
|
|
return _Plan(
|
|
op_index=index, op_name=op.op_name, kind="add",
|
|
input_tensor=op.inputs[0], input1_tensor=op.inputs[1], output_tensor=op.outputs[0],
|
|
h=h, w=w, cin=cin, cout=cout, oh=oh, ow=ow, kh=1, kw=1, sh=1, sw=1,
|
|
act_min=act_min, act_max=act_max, in_scale=scale0, in_zp=z0,
|
|
input1_scale=scale1, input1_zp=z1, out_scale=out_scale, out_zp=zout,
|
|
full_k=1, k_slice=1, params=_pack_add_params(scale0, scale1, out_scale),
|
|
)
|
|
|
|
|
|
def _lower_pool(graph: GraphInfo, index: int) -> _Plan:
|
|
op = graph.operators[index]
|
|
_require(len(op.inputs) == 1 and len(op.outputs) == 1,
|
|
f"operator {index} ({op.op_name}) requires one input and output")
|
|
src = graph.tensors[op.inputs[0]]
|
|
out_t = graph.tensors[op.outputs[0]]
|
|
h, w, cin = _hwc(src)
|
|
oh, ow, cout = _hwc(out_t)
|
|
in_scale, zin = _activation_quant(src, "pool input")
|
|
out_scale, zout = _activation_quant(out_t, "pool output")
|
|
_require(in_scale == out_scale and zin == zout and cout == cin,
|
|
f"operator {index} ({op.op_name}) requires equal quantization and channels")
|
|
options = op.options
|
|
kh, kw = options["filter_height"], options["filter_width"]
|
|
is_global = (op.op_name == "AVERAGE_POOL_2D" and options.get("padding") == "VALID"
|
|
and (oh, ow) == (1, 1) and (kh, kw) == (h, w)
|
|
and (options["stride_h"], options["stride_w"]) == (h, w))
|
|
if is_global:
|
|
return _lower_global_average_pool(graph, index)
|
|
_require(1 <= kh <= 16 and 1 <= kw <= 16,
|
|
f"operator {index} ({op.op_name}): kernel must be within 1..16")
|
|
_check_kernel_options(op.op_name, index, options)
|
|
act_min, act_max = _activation(options, out_scale, zout)
|
|
kind = "max_pool" if op.op_name == "MAX_POOL_2D" else "average_pool"
|
|
return _Plan(
|
|
op_index=index, op_name=op.op_name, kind=kind,
|
|
input_tensor=op.inputs[0], output_tensor=op.outputs[0],
|
|
h=h, w=w, cin=cin, cout=cout, oh=oh, ow=ow,
|
|
kh=kh, kw=kw, sh=options["stride_h"], sw=options["stride_w"],
|
|
pads=_pads(options, h, w, kh, kw, oh, ow), act_min=act_min, act_max=act_max,
|
|
in_scale=in_scale, in_zp=zin, out_scale=out_scale, out_zp=zout,
|
|
full_k=kh * kw, k_slice=min(MAX_K_SLICE, kh * kw),
|
|
)
|
|
|
|
|
|
def _lower_clamp(graph: GraphInfo, index: int) -> _Plan:
|
|
op = graph.operators[index]
|
|
_require(len(op.inputs) == 1 and len(op.outputs) == 1,
|
|
f"operator {index} ({op.op_name}) requires one input and output")
|
|
src, out_t = graph.tensors[op.inputs[0]], graph.tensors[op.outputs[0]]
|
|
h, w, cin = _hwc(src)
|
|
oh, ow, cout = _hwc(out_t)
|
|
in_scale, zin = _activation_quant(src, "CLAMP input")
|
|
out_scale, zout = _activation_quant(out_t, "CLAMP output")
|
|
_require((h, w, cin) == (oh, ow, cout) and in_scale == out_scale and zin == zout,
|
|
f"operator {index} ({op.op_name}) must preserve geometry and quantization")
|
|
fused = "RELU6" if op.op_name == "RELU6" else "RELU"
|
|
act_min, act_max = _activation({"fused_activation": fused}, out_scale, zout)
|
|
return _Plan(
|
|
op_index=index, op_name=op.op_name, kind="clamp",
|
|
input_tensor=op.inputs[0], output_tensor=op.outputs[0],
|
|
h=h, w=w, cin=cin, cout=cout, oh=oh, ow=ow, kh=1, kw=1, sh=1, sw=1,
|
|
act_min=act_min, act_max=act_max, in_scale=in_scale, in_zp=zin,
|
|
out_scale=out_scale, out_zp=zout, full_k=1, k_slice=1,
|
|
)
|
|
|
|
|
|
def _lower_operator(graph: GraphInfo, index: int) -> _Plan:
|
|
op = graph.operators[index]
|
|
if op.op_name == "CONV_2D":
|
|
return _lower_conv2d(graph, index)
|
|
if op.op_name == "DEPTHWISE_CONV_2D":
|
|
return _lower_depthwise(graph, index)
|
|
if op.op_name == "FULLY_CONNECTED":
|
|
return _lower_fully_connected(graph, index)
|
|
if op.op_name == "ADD":
|
|
return _lower_add(graph, index)
|
|
if op.op_name in ("AVERAGE_POOL_2D", "MAX_POOL_2D"):
|
|
return _lower_pool(graph, index)
|
|
if op.op_name in ("RELU", "RELU6"):
|
|
return _lower_clamp(graph, index)
|
|
if op.op_name == "RESHAPE":
|
|
src = graph.tensors[op.inputs[0]]
|
|
out_t = graph.tensors[op.outputs[0]]
|
|
in_count = 1
|
|
for dim in src.shape[1:]:
|
|
in_count *= dim
|
|
out_count = 1
|
|
for dim in out_t.shape[1:]:
|
|
out_count *= dim
|
|
_require(
|
|
in_count == out_count,
|
|
f"operator {index} (RESHAPE) must preserve the element count",
|
|
)
|
|
_require(
|
|
_activation_quant(src, "reshape input") == _activation_quant(out_t, "reshape output"),
|
|
f"operator {index} (RESHAPE) must preserve quantization",
|
|
)
|
|
return _Plan(
|
|
op_index=index,
|
|
op_name=op.op_name,
|
|
kind="reshape",
|
|
input_tensor=op.inputs[0],
|
|
output_tensor=op.outputs[0],
|
|
)
|
|
if op.op_name == "SOFTMAX":
|
|
_require(len(op.inputs) == 1 and len(op.outputs) == 1,
|
|
f"operator {index} (SOFTMAX): requires one input and output")
|
|
src = graph.tensors[op.inputs[0]]
|
|
out_t = graph.tensors[op.outputs[0]]
|
|
in_scale, in_zp = _activation_quant(src, "softmax input")
|
|
out_scale, out_zp = _activation_quant(out_t, "softmax output")
|
|
_require(src.shape == out_t.shape,
|
|
f"operator {index} (SOFTMAX): input/output shapes must match")
|
|
_require(all(1 <= dim <= 4096 for dim in _hwc(src)),
|
|
f"operator {index} (SOFTMAX): dimensions must be within 1..4096")
|
|
_require(-128 <= in_zp <= 127,
|
|
f"operator {index} (SOFTMAX): input zero point must fit INT8")
|
|
_require(out_scale == SOFTMAX_OUTPUT_SCALE and out_zp == SOFTMAX_OUTPUT_ZERO_POINT,
|
|
f"operator {index} (SOFTMAX): output quantization must be 1/256, -128")
|
|
try:
|
|
softmax = softmax_parameters(in_scale, op.options.get("beta", 1.0))
|
|
except ValueError as error:
|
|
raise CompilerError(f"operator {index} (SOFTMAX): {error}") from error
|
|
return _Plan(
|
|
op_index=index,
|
|
op_name=op.op_name,
|
|
kind="softmax",
|
|
input_tensor=op.inputs[0],
|
|
output_tensor=op.outputs[0],
|
|
in_scale=in_scale,
|
|
in_zp=in_zp,
|
|
out_scale=out_scale,
|
|
out_zp=out_zp,
|
|
softmax=softmax,
|
|
)
|
|
raise CompilerError(
|
|
f"operator {index} ({op.op_name}) has no supported NPU-P0 placement"
|
|
)
|
|
|
|
|
|
class _Arena:
|
|
"""Bump allocator with a first-fit free list; every allocation 64-byte aligned."""
|
|
|
|
def __init__(self) -> None:
|
|
self._free: list[list[int]] = []
|
|
self._high = 0
|
|
self.peak = 0
|
|
|
|
@staticmethod
|
|
def _aligned(size: int) -> int:
|
|
return (size + ARENA_ALIGNMENT - 1) & ~(ARENA_ALIGNMENT - 1)
|
|
|
|
def alloc(self, size: int) -> int:
|
|
size = self._aligned(size)
|
|
for block in self._free:
|
|
if block[1] >= size:
|
|
offset = block[0]
|
|
if block[1] == size:
|
|
self._free.remove(block)
|
|
else:
|
|
block[0] += size
|
|
block[1] -= size
|
|
return offset
|
|
offset = self._high
|
|
self._high += size
|
|
self.peak = max(self.peak, self._high)
|
|
return offset
|
|
|
|
def free(self, offset: int, size: int) -> None:
|
|
self._free.append([offset, self._aligned(size)])
|
|
self._free.sort(key=lambda block: block[0])
|
|
merged: list[list[int]] = []
|
|
for block in self._free:
|
|
if merged and merged[-1][0] + merged[-1][1] == block[0]:
|
|
merged[-1][1] += block[1]
|
|
else:
|
|
merged.append(block)
|
|
self._free = merged
|
|
|
|
|
|
def _append_aligned(region: bytearray, payload: bytes) -> int:
|
|
pad = (-len(region)) % ARENA_ALIGNMENT
|
|
region.extend(b"\x00" * pad)
|
|
offset = len(region)
|
|
region.extend(payload)
|
|
return offset
|
|
|
|
|
|
def _git_identity() -> dict[str, Any]:
|
|
try:
|
|
revision = subprocess.check_output(
|
|
["git", "-C", str(REPO_ROOT), "rev-parse", "HEAD"],
|
|
text=True,
|
|
stderr=subprocess.DEVNULL,
|
|
).strip()
|
|
dirty = bool(
|
|
subprocess.check_output(
|
|
["git", "-C", str(REPO_ROOT), "status", "--porcelain"],
|
|
text=True,
|
|
stderr=subprocess.DEVNULL,
|
|
).strip()
|
|
)
|
|
except (OSError, subprocess.CalledProcessError):
|
|
revision, dirty = "unknown", True
|
|
return {
|
|
"contract": CONTRACT_REVISION,
|
|
"git_revision": revision,
|
|
"git_dirty": dirty,
|
|
}
|
|
|
|
|
|
def _buffer_report(plan: _Plan, positions: int) -> dict[str, int]:
|
|
"""Raw-gather/packed half-buffer footprints with the 8192-byte bound checked."""
|
|
raw_positions = positions
|
|
if plan.kind == "depthwise3x3":
|
|
gather_slice = 9 * 8 # 9 kernel positions x 8 channel lanes per spatial lane
|
|
packed_a = positions * 72
|
|
packed_w = 72
|
|
elif plan.kind == "global_average_pool":
|
|
gather_slice = plan.k_slice * 8 # positions x 8 channel lanes
|
|
packed_a = plan.k_slice * 8
|
|
packed_w = 0
|
|
elif plan.kind in ("max_pool", "average_pool"):
|
|
# Local pool drains one output position before gathering the next.
|
|
gather_slice = max(1, plan.k_slice) * min(plan.cin, 8)
|
|
raw_positions = 1
|
|
packed_a = 0
|
|
packed_w = 0
|
|
elif plan.kind in ("add", "clamp"):
|
|
gather_slice = max(1, plan.k_slice) * min(plan.cin, 8)
|
|
packed_a = 0
|
|
packed_w = 0
|
|
else:
|
|
gather_slice = plan.k_slice
|
|
packed_a = plan.k_slice * 8
|
|
packed_w = plan.k_slice * 8
|
|
raw_gather = raw_positions * gather_slice
|
|
_require(
|
|
raw_gather <= MAX_BUFFER_HALF_BYTES,
|
|
f"operator {plan.op_index}: raw gather half {raw_gather} exceeds 8192",
|
|
)
|
|
_require(
|
|
packed_a <= MAX_BUFFER_HALF_BYTES and packed_w <= MAX_BUFFER_HALF_BYTES,
|
|
f"operator {plan.op_index}: packed A/W half exceeds 8192",
|
|
)
|
|
return {
|
|
"raw_gather_half_bytes": raw_gather,
|
|
"packed_a_bytes": packed_a,
|
|
"packed_w_bytes": packed_w,
|
|
"limit_bytes": MAX_BUFFER_HALF_BYTES,
|
|
}
|
|
|
|
|
|
def _dma_report(plan: _Plan) -> dict[str, int]:
|
|
weight_bytes = len(plan.weights) if plan.weights is not None else 0
|
|
param_bytes = len(plan.params) if plan.params is not None else 0
|
|
tile_h, tile_w = _tile(plan.oh, plan.ow)
|
|
slot_bytes = (plan.cout + 3) & ~3
|
|
pass_capacity = min(8, MAX_BUFFER_HALF_BYTES // slot_bytes)
|
|
passes = sum(
|
|
-(-(min(tile_h, plan.oh - tile_y) * min(tile_w, plan.ow - tile_x)) //
|
|
pass_capacity)
|
|
for tile_y in range(0, plan.oh, tile_h)
|
|
for tile_x in range(0, plan.ow, tile_w)
|
|
)
|
|
weight_fetches = passes if weight_bytes > (2 * MAX_BUFFER_HALF_BYTES) else int(
|
|
weight_bytes != 0
|
|
)
|
|
weight_reads = weight_bytes * weight_fetches
|
|
param_fetches = passes if param_bytes > MAX_BUFFER_HALF_BYTES else int(param_bytes != 0)
|
|
param_reads = param_bytes * param_fetches
|
|
if plan.kind in ("conv2d", "depthwise3x3"):
|
|
valid_kernel_positions = 0
|
|
for oy in range(plan.oh):
|
|
for ox in range(plan.ow):
|
|
for ky in range(plan.kh):
|
|
iy = oy * plan.sh + ky - plan.pads[0]
|
|
if not 0 <= iy < plan.h:
|
|
continue
|
|
for kx in range(plan.kw):
|
|
ix = ox * plan.sw + kx - plan.pads[2]
|
|
if 0 <= ix < plan.w:
|
|
valid_kernel_positions += 1
|
|
gather = valid_kernel_positions * plan.cin
|
|
macs = (plan.oh * plan.ow * plan.full_k *
|
|
(plan.cin if plan.kind == "depthwise3x3" else plan.cout))
|
|
elif plan.kind == "global_average_pool":
|
|
gather = plan.h * plan.w * plan.cin
|
|
macs = 0
|
|
elif plan.kind == "add":
|
|
gather = 2 * plan.h * plan.w * plan.cin
|
|
macs = 0
|
|
elif plan.kind in ("max_pool", "average_pool"):
|
|
valid_kernel_positions = 0
|
|
for oy in range(plan.oh):
|
|
for ox in range(plan.ow):
|
|
for ky in range(plan.kh):
|
|
iy = oy * plan.sh + ky - plan.pads[0]
|
|
if not 0 <= iy < plan.h:
|
|
continue
|
|
for kx in range(plan.kw):
|
|
ix = ox * plan.sw + kx - plan.pads[2]
|
|
if 0 <= ix < plan.w:
|
|
valid_kernel_positions += 1
|
|
gather = valid_kernel_positions * plan.cin
|
|
macs = 0
|
|
elif plan.kind == "clamp":
|
|
gather = plan.h * plan.w * plan.cin
|
|
macs = 0
|
|
else:
|
|
# Every output position gathers its full receptive field; redundant
|
|
# reads of overlapping windows are included on purpose.
|
|
gather = plan.oh * plan.ow * plan.full_k
|
|
macs = plan.oh * plan.ow * plan.cout * plan.full_k
|
|
input_fetches = (
|
|
(plan.cout + 7) // 8
|
|
if plan.kind in ("conv2d", "fully_connected") and plan.full_k > MAX_K_SLICE
|
|
else 1
|
|
)
|
|
gather *= input_fetches
|
|
write = plan.oh * plan.ow * plan.cout
|
|
pack_cycles = -(-gather // 8)
|
|
estimated = (-(-macs // 8) + pack_cycles +
|
|
-(-(weight_reads + param_reads) // 8) + -(-write // 8))
|
|
return {
|
|
"gather_read_bytes": gather,
|
|
"input_tensor_fetches": input_fetches,
|
|
"weight_read_bytes": weight_reads,
|
|
"weight_tensor_fetches": weight_fetches,
|
|
"param_read_bytes": param_reads,
|
|
"param_tensor_fetches": param_fetches,
|
|
"read_bytes": gather + weight_reads + param_reads,
|
|
"write_bytes": write,
|
|
"useful_macs": macs,
|
|
"pack_cycles": pack_cycles,
|
|
"estimated_cycles": estimated,
|
|
}
|
|
|
|
|
|
def _operator_report(plan: _Plan, descriptor_index: int | None) -> dict[str, Any]:
|
|
entry: dict[str, Any] = {
|
|
"index": plan.op_index,
|
|
"operator": plan.op_name,
|
|
"descriptor_index": descriptor_index,
|
|
}
|
|
if plan.kind == "reshape":
|
|
entry["placement"] = "none"
|
|
entry["note"] = "storage-preserving reshape; emits no runtime work"
|
|
return entry
|
|
if plan.kind == "softmax":
|
|
entry["placement"] = "cpu-softmax"
|
|
entry["pinned"] = {
|
|
**(plan.softmax or {}),
|
|
"output_scale": SOFTMAX_OUTPUT_SCALE,
|
|
"output_zero_point": SOFTMAX_OUTPUT_ZERO_POINT,
|
|
}
|
|
entry["quantization"] = {
|
|
"input_scale": plan.in_scale,
|
|
"input_zero_point": plan.in_zp,
|
|
"output_scale": plan.out_scale,
|
|
"output_zero_point": plan.out_zp,
|
|
}
|
|
return entry
|
|
opcode = _OPCODE_FOR_KIND[plan.kind]
|
|
tile_h, tile_w = _tile(plan.oh, plan.ow)
|
|
sizes = _k_sizes(plan.full_k, plan.k_slice)
|
|
dma = _dma_report(plan)
|
|
entry.update(
|
|
{
|
|
"placement": "npu",
|
|
"opcode": opcode,
|
|
"opcode_name": OPCODE_NAMES[opcode],
|
|
"geometry": {
|
|
"h": plan.h,
|
|
"w": plan.w,
|
|
"cin": plan.cin,
|
|
"cout": plan.cout,
|
|
"oh": plan.oh,
|
|
"ow": plan.ow,
|
|
"kh": plan.kh,
|
|
"kw": plan.kw,
|
|
"stride_h": plan.sh,
|
|
"stride_w": plan.sw,
|
|
"pads": list(plan.pads),
|
|
},
|
|
"quantization": {
|
|
"input_scale": plan.in_scale,
|
|
"input_zero_point": plan.in_zp,
|
|
"input1_scale": plan.input1_scale if plan.input1_tensor is not None else None,
|
|
"input1_zero_point": plan.input1_zp if plan.input1_tensor is not None else None,
|
|
"output_scale": plan.out_scale,
|
|
"output_zero_point": plan.out_zp,
|
|
"act_min": plan.act_min,
|
|
"act_max": plan.act_max,
|
|
},
|
|
"full_k": plan.full_k,
|
|
"k_slice": plan.k_slice,
|
|
"k_slices": {"count": len(sizes), "sizes": sizes},
|
|
"single_slice": len(sizes) == 1,
|
|
"tile": {
|
|
"tile_h": tile_h,
|
|
"tile_w": tile_w,
|
|
"positions": tile_h * tile_w,
|
|
"tiles": -(-plan.oh // tile_h) * -(-plan.ow // tile_w),
|
|
},
|
|
"lane_tails": {
|
|
"cout_mod8": plan.cout % 8,
|
|
"oh_mod_tile": plan.oh % tile_h,
|
|
"ow_mod_tile": plan.ow % tile_w,
|
|
},
|
|
"tail_utilization": {
|
|
"output_channels": plan.cout / (8 * ((plan.cout + 7) // 8)),
|
|
"tile_positions": min(8, plan.oh * plan.ow) / 8.0,
|
|
},
|
|
"reloads": {
|
|
"input_k_slice": max(0, len(sizes) - 1) *
|
|
(-(-plan.oh // tile_h) * -(-plan.ow // tile_w)),
|
|
"input_tensors": dma["input_tensor_fetches"],
|
|
"weight_groups": (((plan.cout + 7) // 8) *
|
|
dma["weight_tensor_fetches"]),
|
|
"weight_tensors": dma["weight_tensor_fetches"],
|
|
"param_tensors": dma["param_tensor_fetches"],
|
|
},
|
|
"buffers": _buffer_report(plan, tile_h * tile_w),
|
|
"weights": (
|
|
{"bytes": len(plan.weights)} if plan.weights is not None else None
|
|
),
|
|
"params": ({"bytes": len(plan.params)} if plan.params is not None else None),
|
|
"dma": dma,
|
|
"useful_macs": dma["useful_macs"],
|
|
"pack_cycles": dma["pack_cycles"],
|
|
"estimated_cycles": dma["estimated_cycles"],
|
|
}
|
|
)
|
|
return entry
|
|
|
|
|
|
def _emit_descriptor(
|
|
plan: _Plan,
|
|
input_offset: int,
|
|
input1_offset: int,
|
|
output_offset: int,
|
|
weight_offset: int,
|
|
param_offset: int,
|
|
) -> Descriptor:
|
|
opcode = _OPCODE_FOR_KIND[plan.kind]
|
|
tile_h, tile_w = _tile(plan.oh, plan.ow)
|
|
has_params = plan.params is not None
|
|
pad_top, pad_bottom, pad_left, pad_right = plan.pads
|
|
return Descriptor(
|
|
version_opcode=(ABI_VERSION << 16) | opcode,
|
|
input0_base=input_offset,
|
|
input1_base=input1_offset,
|
|
output_base=output_offset,
|
|
param_base=param_offset if has_params else 0,
|
|
input_hw=plan.h | (plan.w << 16),
|
|
channels=plan.cin | (plan.cout << 16),
|
|
output_hw=plan.oh | (plan.ow << 16),
|
|
input0_row_bytes=plan.w * plan.cin,
|
|
input1_row_bytes=plan.w * plan.cin if plan.input1_tensor is not None else 0,
|
|
output_row_bytes=plan.ow * plan.cout,
|
|
kernel_stride=plan.kh | (plan.kw << 8) | (plan.sh << 16) | (plan.sw << 24),
|
|
padding=pad_top | (pad_bottom << 8) | (pad_left << 16) | (pad_right << 24),
|
|
tile_hw=tile_h | (tile_w << 8),
|
|
k_slice=plan.k_slice,
|
|
input0_bytes=used_span(plan.h, plan.w, plan.cin, plan.w * plan.cin),
|
|
output_bytes=used_span(plan.oh, plan.ow, plan.cout, plan.ow * plan.cout),
|
|
input1_bytes=(used_span(plan.h, plan.w, plan.cin, plan.w * plan.cin)
|
|
if plan.input1_tensor is not None else 0),
|
|
param_bytes=len(plan.params) if has_params else 0,
|
|
input0_zero=plan.in_zp,
|
|
input1_zero=plan.input1_zp if plan.input1_tensor is not None else 0,
|
|
output_zero=plan.out_zp,
|
|
act_min=plan.act_min,
|
|
act_max=plan.act_max,
|
|
weight_base=weight_offset if plan.weights is not None else 0,
|
|
weight_bytes=len(plan.weights) if plan.weights is not None else 0,
|
|
)
|
|
|
|
|
|
def _self_check(job: CompiledJob) -> None:
|
|
"""Finalize templates against documented bases and run the ABI validator."""
|
|
region = (_SELF_CHECK_BASES["descriptors"], len(job.descriptors) * DESCRIPTOR_BYTES)
|
|
finalized = [Descriptor(**vars(desc)) for desc in job.descriptors]
|
|
word_fields = {2: "input0_base", 3: "input1_base", 4: "output_base", 5: "param_base", 24: "weight_base"}
|
|
for relocation in job.relocations:
|
|
desc = finalized[relocation["descriptor_index"]]
|
|
field = word_fields[relocation["word_index"]]
|
|
if getattr(desc, field) != relocation["offset"]:
|
|
raise CompilerError("internal: relocation offset mismatches the template word")
|
|
setattr(desc, field, _SELF_CHECK_BASES[relocation["region"]] + relocation["offset"])
|
|
for index, desc in enumerate(finalized):
|
|
try:
|
|
validate_descriptor(desc, descriptor_region=region)
|
|
except DescriptorError as error:
|
|
raise CompilerError(
|
|
f"internal: emitted descriptor {index} fails ABI validation: {error}"
|
|
) from error
|
|
|
|
|
|
def compile_model(
|
|
graph: GraphInfo,
|
|
*,
|
|
workload: str,
|
|
reference_input: bytes | None = None,
|
|
source_model_sha256: str | None = None,
|
|
) -> CompiledJob:
|
|
"""Lower one batch-one INT8 graph into a descriptor ABI 1.0 CompiledJob.
|
|
|
|
reference_input, when given, is the raw model input byte image; the P0
|
|
report then carries per-layer SHA-256 digests of a direct npu_reference
|
|
evaluation of the whole graph (including the CPU softmax).
|
|
"""
|
|
_require(len(graph.operators) > 0, "graph holds no operators")
|
|
_require(len(graph.inputs) == 1, "graph must have exactly one input")
|
|
_require(len(graph.outputs) == 1, "graph must have exactly one output")
|
|
plans = [_lower_operator(graph, index) for index in range(len(graph.operators))]
|
|
for index, plan in enumerate(plans):
|
|
_require(
|
|
plan.kind != "softmax" or index == len(plans) - 1,
|
|
f"operator {index} (SOFTMAX): only a terminal CPU softmax is supported",
|
|
)
|
|
last_use: dict[int, int] = {}
|
|
for index, op in enumerate(graph.operators):
|
|
for tensor_index in op.inputs:
|
|
last_use[tensor_index] = index
|
|
|
|
arena = _Arena()
|
|
alloc: dict[int, tuple[int, int, int]] = {}
|
|
descriptors: list[Descriptor] = []
|
|
relocations: list[dict] = []
|
|
weights_region = bytearray()
|
|
params_region = bytearray()
|
|
report_ops: list[dict[str, Any]] = []
|
|
producer_descriptor: dict[int, int] = {}
|
|
|
|
in_index = graph.inputs[0]
|
|
in_h, in_w, in_c = _hwc(graph.tensors[in_index])
|
|
in_scale, in_zp = _activation_quant(graph.tensors[in_index], "graph input")
|
|
in_bytes = in_h * in_w * in_c
|
|
input_offset = arena.alloc(in_bytes)
|
|
pinned = len(graph.operators) + 1 # the input image stays live for the whole job
|
|
alloc[in_index] = (input_offset, in_bytes, pinned)
|
|
|
|
def free_consumed(tensor_index: int, at: int) -> None:
|
|
entry = alloc.get(tensor_index)
|
|
if entry is None:
|
|
return
|
|
aliases = [key for key, value in alloc.items() if value[0] == entry[0]]
|
|
if max(alloc[key][2] for key in aliases) <= at:
|
|
arena.free(entry[0], entry[1])
|
|
for key in aliases:
|
|
del alloc[key]
|
|
|
|
for index, plan in enumerate(plans):
|
|
if plan.kind == "reshape":
|
|
src_offset, src_size, src_last = alloc[plan.input_tensor]
|
|
out_last = last_use.get(plan.output_tensor, len(plans))
|
|
retirement = max(src_last, out_last)
|
|
alloc[plan.input_tensor] = (src_offset, src_size, retirement)
|
|
alloc[plan.output_tensor] = (src_offset, src_size, retirement)
|
|
if plan.input_tensor in producer_descriptor:
|
|
producer_descriptor[plan.output_tensor] = producer_descriptor[plan.input_tensor]
|
|
report_ops.append(_operator_report(plan, None))
|
|
continue
|
|
if plan.kind == "softmax":
|
|
free_consumed(plan.input_tensor, index)
|
|
report_ops.append(_operator_report(plan, None))
|
|
continue
|
|
in_off, _, _ = alloc[plan.input_tensor]
|
|
input1_off = 0
|
|
if plan.input1_tensor is not None:
|
|
input1_off, _, _ = alloc[plan.input1_tensor]
|
|
out_bytes = plan.oh * plan.ow * plan.cout
|
|
out_off = arena.alloc(out_bytes)
|
|
alloc[plan.output_tensor] = (
|
|
out_off,
|
|
out_bytes,
|
|
last_use.get(plan.output_tensor, len(plans)),
|
|
)
|
|
weight_offset = 0
|
|
param_offset = 0
|
|
if plan.weights is not None:
|
|
weight_offset = _append_aligned(weights_region, plan.weights)
|
|
if plan.params is not None:
|
|
param_offset = _append_aligned(params_region, plan.params)
|
|
descriptor = _emit_descriptor(
|
|
plan, in_off, input1_off, out_off, weight_offset, param_offset
|
|
)
|
|
descriptor_index = len(descriptors)
|
|
descriptors.append(descriptor)
|
|
producer_descriptor[plan.output_tensor] = descriptor_index
|
|
relocations.append(
|
|
{
|
|
"descriptor_index": descriptor_index,
|
|
"word_index": 2,
|
|
"region": "arena",
|
|
"offset": in_off,
|
|
}
|
|
)
|
|
if plan.input1_tensor is not None:
|
|
relocations.append(
|
|
{"descriptor_index": descriptor_index, "word_index": 3,
|
|
"region": "arena", "offset": input1_off}
|
|
)
|
|
relocations.append(
|
|
{
|
|
"descriptor_index": descriptor_index,
|
|
"word_index": 4,
|
|
"region": "arena",
|
|
"offset": out_off,
|
|
}
|
|
)
|
|
if plan.params is not None:
|
|
relocations.append(
|
|
{
|
|
"descriptor_index": descriptor_index,
|
|
"word_index": 5,
|
|
"region": "params",
|
|
"offset": param_offset,
|
|
}
|
|
)
|
|
if plan.weights is not None:
|
|
relocations.append(
|
|
{
|
|
"descriptor_index": descriptor_index,
|
|
"word_index": 24,
|
|
"region": "weights",
|
|
"offset": weight_offset,
|
|
}
|
|
)
|
|
entry = _operator_report(plan, descriptor_index)
|
|
entry["arena"] = {
|
|
"input_offset": in_off,
|
|
"input1_offset": input1_off if plan.input1_tensor is not None else None,
|
|
"output_offset": out_off,
|
|
"output_bytes": out_bytes,
|
|
"output_live_until_operator": last_use.get(plan.output_tensor, len(plans)),
|
|
}
|
|
if plan.weights is not None:
|
|
entry["weights"]["offset"] = weight_offset
|
|
if plan.params is not None:
|
|
entry["params"]["offset"] = param_offset
|
|
report_ops.append(entry)
|
|
free_consumed(plan.input_tensor, index)
|
|
if plan.input1_tensor is not None and plan.input1_tensor != plan.input_tensor:
|
|
free_consumed(plan.input1_tensor, index)
|
|
|
|
steps: list[dict[str, Any]] = []
|
|
if descriptors:
|
|
steps.append({"kind": "npu", "first_descriptor": 0, "count": len(descriptors)})
|
|
output_tensor = graph.outputs[0]
|
|
if plans[-1].kind == "softmax":
|
|
_require(
|
|
plans[-1].output_tensor == output_tensor,
|
|
"terminal SOFTMAX output must be the graph output",
|
|
)
|
|
source_descriptor = producer_descriptor.get(plans[-1].input_tensor)
|
|
_require(
|
|
source_descriptor is not None,
|
|
"terminal SOFTMAX input must be produced by an NPU descriptor",
|
|
)
|
|
steps.append(
|
|
{
|
|
"kind": "cpu",
|
|
"op": "softmax",
|
|
"source_descriptor": source_descriptor,
|
|
"note": "pinned integer softmax (numerical profile 1)",
|
|
**(plans[-1].softmax or {}),
|
|
}
|
|
)
|
|
else:
|
|
source_descriptor = producer_descriptor.get(output_tensor)
|
|
_require(
|
|
source_descriptor is not None,
|
|
"graph output must be produced by an NPU descriptor",
|
|
)
|
|
|
|
output_h, output_w, output_c = _hwc(graph.tensors[output_tensor])
|
|
source_entry = next(
|
|
entry for entry in report_ops if entry.get("descriptor_index") == source_descriptor
|
|
)
|
|
output_record = {
|
|
"tensor_index": output_tensor,
|
|
"shape": [output_h, output_w, output_c],
|
|
"bytes": output_h * output_w * output_c,
|
|
"placement": "cpu-softmax" if plans[-1].kind == "softmax" else "npu",
|
|
"source_descriptor": source_descriptor,
|
|
"source_offset": source_entry["arena"]["output_offset"],
|
|
}
|
|
|
|
if reference_input is not None:
|
|
reference_layers = evaluate_reference(graph, reference_input)
|
|
produced = [entry for entry, plan in zip(report_ops, plans) if plan.kind != "reshape"]
|
|
_require(
|
|
len(produced) == len(reference_layers),
|
|
"internal: reference evaluation layer count mismatch",
|
|
)
|
|
for entry, tensor in zip(produced, reference_layers):
|
|
entry["reference_sha256"] = _digest(tensor)
|
|
entry["reference_shape"] = list(tensor.shape)
|
|
|
|
descriptor_read_bytes = len(descriptors) * DESCRIPTOR_BYTES
|
|
totals = {
|
|
"descriptors": len(descriptors),
|
|
"descriptor_read_bytes": descriptor_read_bytes,
|
|
"weights_bytes": len(weights_region),
|
|
"params_bytes": len(params_region),
|
|
"useful_macs": sum(entry.get("useful_macs", 0) for entry in report_ops),
|
|
"dma_read_bytes": descriptor_read_bytes + sum(
|
|
entry.get("dma", {}).get("read_bytes", 0) for entry in report_ops
|
|
),
|
|
"dma_write_bytes": sum(
|
|
entry.get("dma", {}).get("write_bytes", 0) for entry in report_ops
|
|
),
|
|
"estimated_cycles": -(-descriptor_read_bytes // 8) + sum(
|
|
entry.get("estimated_cycles", 0) for entry in report_ops
|
|
),
|
|
}
|
|
report: dict[str, Any] = {
|
|
"workload": workload,
|
|
"source_model_sha256": source_model_sha256,
|
|
"numeric_profile": NUMERIC_PROFILE,
|
|
"compiler": _git_identity(),
|
|
"input": {
|
|
"tensor_index": in_index,
|
|
"shape": [in_h, in_w, in_c],
|
|
"scale": in_scale,
|
|
"zero_point": in_zp,
|
|
"bytes": in_bytes,
|
|
"offset": input_offset,
|
|
},
|
|
"arena_bytes": arena.peak,
|
|
"arena_note": (
|
|
"single contiguous arena; input image pinned for the whole job; "
|
|
"layer outputs use a 64-byte-aligned first-fit free list freed "
|
|
"after their last consumer; input/output allocations are disjoint"
|
|
),
|
|
"cycle_model": _CYCLE_MODEL,
|
|
"operators": report_ops,
|
|
"output": output_record,
|
|
"totals": totals,
|
|
}
|
|
job = CompiledJob(
|
|
descriptors=descriptors,
|
|
weights=bytes(weights_region),
|
|
params=bytes(params_region),
|
|
arena_bytes=arena.peak,
|
|
input_offset=input_offset,
|
|
relocations=relocations,
|
|
steps=steps,
|
|
report=report,
|
|
)
|
|
_self_check(job)
|
|
return job
|
|
|
|
|
|
def _nest(values: list[int], shape: tuple[int, ...]) -> Any:
|
|
if len(shape) == 1:
|
|
return tuple(values[: shape[0]])
|
|
step = 1
|
|
for dim in shape[1:]:
|
|
step *= dim
|
|
return tuple(
|
|
_nest(values[index * step : (index + 1) * step], shape[1:])
|
|
for index in range(shape[0])
|
|
)
|
|
|
|
|
|
def evaluate_reference(graph: GraphInfo, input_bytes: bytes) -> list[Tensor]:
|
|
"""Run npu_reference layer functions over the whole graph (incl. softmax).
|
|
|
|
RESHAPE is storage-preserving and produces no layer entry; every other
|
|
operator appends exactly one tensor, in graph order.
|
|
"""
|
|
in_index = graph.inputs[0]
|
|
src_t = graph.tensors[in_index]
|
|
h, w, cin = _hwc(src_t)
|
|
_require(
|
|
len(input_bytes) == h * w * cin,
|
|
f"reference input is {len(input_bytes)} bytes, expected {h * w * cin}",
|
|
)
|
|
in_scale, in_zp = _activation_quant(src_t, "graph input")
|
|
tensors = {in_index: Tensor((h, w, cin), tuple(_signed(bytes(input_bytes))), in_scale, in_zp)}
|
|
layers: list[Tensor] = []
|
|
for index, op in enumerate(graph.operators):
|
|
options = op.options
|
|
out_t = graph.tensors[op.outputs[0]]
|
|
current = tensors[op.inputs[0]]
|
|
if op.op_name in ("CONV_2D", "DEPTHWISE_CONV_2D", "FULLY_CONNECTED"):
|
|
weight_t = graph.tensors[op.inputs[1]]
|
|
bias_t = graph.tensors[op.inputs[2]]
|
|
out_scale, out_zp = _activation_quant(out_t, "output")
|
|
act_min, act_max = _activation(options, out_scale, out_zp)
|
|
cout = out_t.shape[-1]
|
|
bias = _bias_values(bias_t, cout)
|
|
multipliers, shifts = _channel_quant(
|
|
current.scale, weight_t.scales, out_scale, cout
|
|
)
|
|
values = _signed(weight_t.data or b"")
|
|
if op.op_name == "CONV_2D":
|
|
wcout, kh, kw, wcin = weight_t.shape
|
|
pads = _pads(
|
|
options, current.height, current.width, kh, kw, out_t.shape[1], out_t.shape[2]
|
|
)
|
|
current = conv2d(
|
|
current,
|
|
_nest(values, (wcout, kh, kw, wcin)),
|
|
bias,
|
|
multipliers,
|
|
shifts,
|
|
stride_h=options["stride_h"],
|
|
stride_w=options["stride_w"],
|
|
pad_top=pads[0],
|
|
pad_bottom=pads[1],
|
|
pad_left=pads[2],
|
|
pad_right=pads[3],
|
|
output_scale=out_scale,
|
|
output_zero_point=out_zp,
|
|
act_min=act_min,
|
|
act_max=act_max,
|
|
)
|
|
elif op.op_name == "DEPTHWISE_CONV_2D":
|
|
_, kh, kw, _ = weight_t.shape
|
|
pads = _pads(
|
|
options, current.height, current.width, kh, kw, out_t.shape[1], out_t.shape[2]
|
|
)
|
|
current = depthwise_conv2d(
|
|
current,
|
|
_nest(values, (kh, kw, current.channels)),
|
|
bias,
|
|
multipliers,
|
|
shifts,
|
|
stride_h=options["stride_h"],
|
|
stride_w=options["stride_w"],
|
|
pad_top=pads[0],
|
|
pad_bottom=pads[1],
|
|
pad_left=pads[2],
|
|
pad_right=pads[3],
|
|
output_scale=out_scale,
|
|
output_zero_point=out_zp,
|
|
act_min=act_min,
|
|
act_max=act_max,
|
|
)
|
|
elif op.op_name == "FULLY_CONNECTED":
|
|
current = fully_connected(
|
|
current,
|
|
_nest(values, weight_t.shape),
|
|
bias,
|
|
multipliers,
|
|
shifts,
|
|
output_scale=out_scale,
|
|
output_zero_point=out_zp,
|
|
act_min=act_min,
|
|
act_max=act_max,
|
|
)
|
|
elif op.op_name == "ADD":
|
|
other = tensors[op.inputs[1]]
|
|
out_scale, out_zp = _activation_quant(out_t, "ADD output")
|
|
common = max(current.scale, other.scale)
|
|
m0, s0 = quantize_multiplier(current.scale / (2.0 * common))
|
|
m1, s1 = quantize_multiplier(other.scale / (2.0 * common))
|
|
mout, sout = quantize_multiplier((2.0 * common) / ((1 << 20) * out_scale))
|
|
act_min, act_max = _activation(options, out_scale, out_zp)
|
|
current = add(
|
|
current, other, input0_multiplier=m0, input0_shift=s0,
|
|
input1_multiplier=m1, input1_shift=s1, output_multiplier=mout,
|
|
output_shift=sout, output_scale=out_scale, output_zero_point=out_zp,
|
|
act_min=act_min, act_max=act_max,
|
|
)
|
|
elif op.op_name in ("AVERAGE_POOL_2D", "MAX_POOL_2D"):
|
|
out_scale, out_zp = _activation_quant(out_t, "output")
|
|
act_min, act_max = _activation(options, out_scale, out_zp)
|
|
kh, kw = options["filter_height"], options["filter_width"]
|
|
is_global = (op.op_name == "AVERAGE_POOL_2D" and options["padding"] == "VALID"
|
|
and tuple(_hwc(out_t)[:2]) == (1, 1) and (kh, kw) == (current.height, current.width)
|
|
and (options["stride_h"], options["stride_w"])
|
|
== (current.height, current.width))
|
|
if is_global:
|
|
current = global_average_pool(current, act_min=act_min, act_max=act_max)
|
|
else:
|
|
oh, ow, _ = _hwc(out_t)
|
|
pads = _pads(options, current.height, current.width, kh, kw, oh, ow)
|
|
pool = max_pool if op.op_name == "MAX_POOL_2D" else average_pool
|
|
current = pool(
|
|
current, kernel_h=kh, kernel_w=kw,
|
|
stride_h=options["stride_h"], stride_w=options["stride_w"],
|
|
pad_top=pads[0], pad_bottom=pads[1], pad_left=pads[2],
|
|
pad_right=pads[3], act_min=act_min, act_max=act_max,
|
|
)
|
|
elif op.op_name in ("RELU", "RELU6"):
|
|
out_scale, out_zp = _activation_quant(out_t, "CLAMP output")
|
|
fused = "RELU6" if op.op_name == "RELU6" else "RELU"
|
|
act_min, act_max = _activation({"fused_activation": fused}, out_scale, out_zp)
|
|
current = clamp(current, act_min, act_max)
|
|
elif op.op_name == "RESHAPE":
|
|
oh, ow, oc = _hwc(out_t)
|
|
current = Tensor((oh, ow, oc), current.data, current.scale, current.zero_point)
|
|
tensors[op.outputs[0]] = current
|
|
continue
|
|
elif op.op_name == "SOFTMAX":
|
|
current = softmax_int8(
|
|
current, **softmax_parameters(current.scale, options.get("beta", 1.0))
|
|
)
|
|
else:
|
|
raise CompilerError(
|
|
f"operator {index} ({op.op_name}) has no supported NPU-P0 placement"
|
|
)
|
|
tensors[op.outputs[0]] = current
|
|
layers.append(current)
|
|
return layers
|
|
|
|
|
|
def vww_input_bytes(raw: bytes) -> bytes:
|
|
"""Map U8 VWW corpus bytes to raw INT8 two's-complement model input bytes."""
|
|
return bytes(value ^ 0x80 for value in raw)
|
|
|
|
|
|
def compile_kws() -> CompiledJob:
|
|
"""Compile the locked KWS graph; the report hashes use the first corpus input."""
|
|
model = load_kws_model()
|
|
return compile_model(
|
|
model.main_graph(),
|
|
workload="kws",
|
|
reference_input=KWS_CORPUS_FIRST.read_bytes(),
|
|
source_model_sha256=KWS_TFLITE_SHA256,
|
|
)
|
|
|
|
|
|
def compile_vww() -> CompiledJob:
|
|
"""Compile the locked VWW graph; the report hashes use the first corpus input."""
|
|
model = load_vww_model()
|
|
return compile_model(
|
|
model.main_graph(),
|
|
workload="vww",
|
|
reference_input=vww_input_bytes(VWW_CORPUS_FIRST.read_bytes()),
|
|
source_model_sha256=VWW_TFLITE_SHA256,
|
|
)
|
|
|
|
|
|
def serialize_descriptors(job: CompiledJob) -> bytes:
|
|
"""Encode the descriptor templates as the N x 128-byte descriptors.bin image."""
|
|
return b"".join(descriptor.to_bytes() for descriptor in job.descriptors)
|
|
|
|
|
|
def write_artifacts(job: CompiledJob, out_dir: str | Path) -> dict[str, Any]:
|
|
"""Write npu.json, descriptors.bin, weights.bin and params.bin; return the manifest."""
|
|
out = Path(out_dir)
|
|
out.mkdir(parents=True, exist_ok=True)
|
|
descriptors_bin = serialize_descriptors(job)
|
|
files = {
|
|
"descriptors.bin": descriptors_bin,
|
|
"weights.bin": job.weights,
|
|
"params.bin": job.params,
|
|
}
|
|
opcode_mask = 0
|
|
for descriptor in job.descriptors:
|
|
opcode_mask |= 1 << descriptor.opcode
|
|
layers_meta = [
|
|
{
|
|
"descriptor_index": entry["descriptor_index"],
|
|
"output_scale": entry["quantization"]["output_scale"],
|
|
"output_zero_point": entry["quantization"]["output_zero_point"],
|
|
}
|
|
for entry in job.report["operators"]
|
|
if entry.get("descriptor_index") is not None
|
|
]
|
|
final_entry = job.report["operators"][-1]
|
|
manifest: dict[str, Any] = {
|
|
"schema": 1,
|
|
"workload": job.report["workload"],
|
|
"source_model_sha256": job.report["source_model_sha256"],
|
|
"compiler": job.report["compiler"],
|
|
"numeric_profile": NUMERIC_PROFILE,
|
|
"required_capability_mask": REQUIRED_CAPABILITY_MASK,
|
|
"required_capability_bits": list(REQUIRED_CAPABILITY_BITS),
|
|
"required_opcode_mask": opcode_mask,
|
|
"files": {
|
|
name: {"bytes": len(payload), "sha256": hashlib.sha256(payload).hexdigest()}
|
|
for name, payload in files.items()
|
|
},
|
|
"arena_bytes": job.arena_bytes,
|
|
"input_offset": job.input_offset,
|
|
"steps": job.steps,
|
|
"relocations": job.relocations,
|
|
"tensors": {
|
|
"input": job.report["input"],
|
|
"layers": layers_meta,
|
|
"output": final_entry.get("quantization"),
|
|
},
|
|
"report": job.report,
|
|
}
|
|
for name, payload in files.items():
|
|
(out / name).write_bytes(payload)
|
|
(out / "npu.json").write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n")
|
|
return manifest
|
|
max_pool,
|