Files
retroSoC/scripts/run_apu_p7_concurrent_rtl.py
T

870 lines
34 KiB
Python

#!/usr/bin/env python3
"""Run the APU-P7 concurrent decode + continuous KWS zero-xrun RTL qualification.
Builds tests/rtl/apu_p7_concurrent_tb.sv once (sv2v + Icarus), then runs the
frozen concurrency matrix: continuous RX KWS concurrent with looped WAV/FLAC
decode streamed to I2S TX under the active P5 ready-memory qualification
(64-word prefill, then ready memory without injected stalls). Each scenario
requires zero codec underrun, zero I2S RX/KWS overrun, every full uninterrupted
scheduled KWS window completed before its successor, and matching
byte/frame/cycle/stall scoreboards. Writes the concurrent.json report and exits
nonzero on any violation or missing input. Nothing is downloaded.
Prerequisites (no network access is used by this script):
- Icarus Verilog (iverilog/vvp) and sv2v on PATH.
- The deterministic P5 codec bundle `apu-p5.apumc`, built once with
`make CONFIG=configs/ci/ihp130.mk apu-p5-bundle` (writes
build/<variant>/apu/p5/apu-p5.apumc). Pass it with --apumc or let the newest
build/*/apu/p5/apu-p5.apumc be auto-discovered.
- The locked P7 TFLite model under .cache/retrosoc/sources/apu-mlperf-tiny
(installed by the shared P7 reference setup); the APUM image is derived
locally with scripts/apu_kws_convert.py. --apum accepts a prebuilt image.
The frozen release gate is 60 simulated seconds per scenario at 48 MHz PCLK
(--seconds 60). The default is a short development run; --smoke selects the
minimal single-scenario matrix for pytest.
"""
from __future__ import annotations
import argparse
import hashlib
import json
import platform
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from apu_codecs import crc8, crc16, decode_flac, decode_wav, process_pcm # noqa: E402
from apu_kws import APUM_PAYLOAD_CRC, APUM_SHA256 # noqa: E402
from generate_apu_kws_rtl_constants import build_apuc # noqa: E402
PCLK_HZ = 48_000_000
FULL_GATE_SECONDS = 60.0
FULL_GATE_CYCLES = int(FULL_GATE_SECONDS * PCLK_HZ)
FLAC_CONSTANT = -2
CONTRACT_REVISION = "apu-kws-convert/1.0.0"
DEFAULT_TFLITE = (
ROOT
/ ".cache/retrosoc/sources/apu-mlperf-tiny/benchmark/training/keyword_spotting"
/ "trained_models/kws_ref_model.tflite"
)
BUNDLE_PREREQUISITE = "make CONFIG=configs/ci/ihp130.mk apu-p5-bundle"
PROFILE = "configs/ci/ihp130.mk"
RESULT_PATTERN = re.compile(
r"CONCURRENT_RESULT "
r"rate=(?P<rate>\d+) "
r"precision=(?P<precision>\d+) "
r"seconds_ms=(?P<seconds_ms>\d+) "
r"duration_cycles=(?P<duration_cycles>\d+) "
r"jobs=(?P<jobs>\d+) "
r"wav_jobs=(?P<wav_jobs>\d+) "
r"flac_jobs=(?P<flac_jobs>\d+) "
r"tx_words=(?P<tx_words>\d+) "
r"consumed=(?P<consumed>\d+) "
r"underrun=(?P<underrun>\d+) "
r"rx_overrun=(?P<rx_overrun>\d+) "
r"kws_overrun=(?P<kws_overrun>\d+) "
r"kws_frames=(?P<kws_frames>\d+) "
r"kws_windows=(?P<kws_windows>\d+) "
r"rx_words=(?P<rx_words>\d+) "
r"codec_input_bytes=(?P<codec_input_bytes>\d+) "
r"codec_output_bytes=(?P<codec_output_bytes>\d+) "
r"codec_frames=(?P<codec_frames>\d+) "
r"codec_cycles=(?P<codec_cycles>\d+) "
r"dma_read_stalls=(?P<dma_read_stalls>\d+) "
r"dma_write_stalls=(?P<dma_write_stalls>\d+) "
r"stream_stalls=(?P<stream_stalls>\d+) "
r"active_cycles=(?P<active_cycles>\d+) "
r"elapsed_cycles=(?P<elapsed_cycles>\d+) "
r"scoreboard=(?P<scoreboard>\w+)"
)
def _sha256_bytes(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def _sha256_file(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for block in iter(lambda: source.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def _write_json(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(json.dumps(value, indent=2, sort_keys=True) + "\n", encoding="utf-8")
temporary.replace(path)
def _hex_words(path: Path, payload: bytes, words: int) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
lines = []
for index in range(words):
chunk = payload[index * 4 : index * 4 + 4]
lines.append(f"{int.from_bytes(chunk.ljust(4, bytes(1)), 'little'):08x}\n")
path.write_text("".join(lines), encoding="utf-8")
def _run_command(command: list[str], *, timeout: int | None = None) -> dict[str, Any]:
started = time.monotonic()
try:
process = subprocess.run(
command, cwd=ROOT, check=False, capture_output=True, text=True, timeout=timeout
)
return {
"command": command,
"duration_seconds": round(time.monotonic() - started, 3),
"exit_code": process.returncode,
"stdout": process.stdout,
"stderr": process.stderr,
"timed_out": False,
}
except subprocess.TimeoutExpired as error:
stdout = error.stdout.decode("utf-8", errors="replace") if error.stdout else ""
stderr = error.stderr.decode("utf-8", errors="replace") if error.stderr else ""
return {
"command": command,
"duration_seconds": round(time.monotonic() - started, 3),
"exit_code": 124,
"stdout": stdout,
"stderr": stderr,
"timed_out": True,
}
def _tool_version(name: str, flag: str = "--version") -> dict[str, Any]:
path = shutil.which(name)
if path is None:
return {"path": None, "version": None}
probe = _run_command([path, flag], timeout=30)
first_line = (probe["stdout"] or probe["stderr"]).splitlines()
return {
"path": path,
"version": first_line[0].strip() if first_line else "unknown",
"sha256": _sha256_file(Path(path)),
}
def _git_revision() -> tuple[str, bool]:
revision = _run_command(["git", "rev-parse", "HEAD"], timeout=30)
dirty = _run_command(["git", "status", "--porcelain"], timeout=30)
return (
revision["stdout"].strip() if revision["exit_code"] == 0 else "unknown",
bool(dirty["stdout"].strip()) if dirty["exit_code"] == 0 else True,
)
def _production_sources() -> list[Path]:
common = ROOT / "rtl/managed/clusterip/common/rtl"
multimedia = ROOT / "rtl/ip/multimedia"
names = (
"apu_microcode_pkg.sv",
"apu_dma.sv",
"apu_ring_scheduler.sv",
"apu_stream_router.sv",
"apu_control_store.sv",
"apu_proof_memo.sv",
"apu_microcode_loader.sv",
"apu_local_sram.sv",
"apu_kws_coeff_store.sv",
"apu_kws_coeff_loader.sv",
"apu_kws_engine.sv",
"apu_kws_sram_client.sv",
"apu_kws_model_loader.sv",
"apu_bitstream_engine.sv",
"apu_entropy_engine.sv",
"apu_reconstruction_engine.sv",
"apu_transform_engine.sv",
"apu_resampler.sv",
"apu_kernel_engine.sv",
"apu_primitive_dispatcher.sv",
"apu_codec_sequencer.sv",
"apu_codec_transport.sv",
"apu_codec_controller.sv",
"apu_reg.sv",
"apb4_apu.sv",
)
return [
common / "interface/apb4_if.sv",
common / "interface/axi4_if.sv",
common / "interface/axi4_stream_if.sv",
common / "utils/register.sv",
common / "utils/fifo.sv",
ROOT / "rtl/tech/tc_sram.sv",
ROOT / "rtl/ip/peripheral/dma_axi4_master.sv",
*(multimedia / name for name in names),
ROOT / "tests/rtl/apu_p7_concurrent_tb.sv",
]
def _verification_sources() -> dict[str, str]:
inputs = [
*_production_sources(),
ROOT / "rtl/ip/multimedia/apu_define.svh",
ROOT / "scripts/apu_codecs.py",
ROOT / "scripts/apu_kws.py",
ROOT / "scripts/apu_kws_convert.py",
ROOT / "rtl/mini/script/convt_sv2v.py",
Path(__file__).resolve(),
]
return {
path.relative_to(ROOT).as_posix(): _sha256_file(path)
for path in sorted(inputs)
if path.is_file()
}
def _resolve_apumc(apumc: Path | None) -> Path:
if apumc is not None:
resolved = apumc.resolve()
if not resolved.is_file():
raise SystemExit(f"APUMC bundle not found: {resolved}\nprerequisite: {BUNDLE_PREREQUISITE}")
return resolved
candidates = sorted(
ROOT.glob("build/*/apu/p5/apu-p5.apumc"), key=lambda path: path.stat().st_mtime
)
if not candidates:
raise SystemExit(
"no APUMC bundle found below build/*/apu/p5/\n"
f"prerequisite: {BUNDLE_PREREQUISITE}"
)
return candidates[-1].resolve()
def _resolve_apum(apum: Path | None) -> tuple[bytes, str]:
if apum is not None:
resolved = apum.resolve()
if not resolved.is_file():
raise SystemExit(f"APUM model image not found: {resolved}")
payload = resolved.read_bytes()
return payload, str(resolved)
if not DEFAULT_TFLITE.is_file():
raise SystemExit(
f"locked P7 TFLite model not installed: {DEFAULT_TFLITE}\n"
"prerequisite: make CONFIG=configs/ci/ihp130.mk setup-apu-kws-reference"
)
from apu_kws_convert import import_tflite
payload = import_tflite(DEFAULT_TFLITE)
return payload, f"generated:{DEFAULT_TFLITE}"
def _wav_payload(rate: int, bits: int, frames: int, seed: int) -> bytes:
block_align = 2 * (bits // 8)
state = seed | 1
data = bytearray()
for frame in range(frames):
for channel in range(2):
state = (1103515245 * state + 12345 + channel + frame) & 0x7FFFFFFF
value = ((state >> 8) & ((1 << bits) - 1)) - (1 << (bits - 1))
data.extend(value.to_bytes(bits // 8, "little", signed=True))
header = bytearray()
header.extend(b"RIFF")
header.extend((36 + len(data)).to_bytes(4, "little"))
header.extend(b"WAVE")
header.extend(b"fmt ")
header.extend((16).to_bytes(4, "little"))
header.extend((1).to_bytes(2, "little"))
header.extend((2).to_bytes(2, "little"))
header.extend(rate.to_bytes(4, "little"))
header.extend((rate * block_align).to_bytes(4, "little"))
header.extend(block_align.to_bytes(2, "little"))
header.extend(bits.to_bytes(2, "little"))
header.extend(b"data")
header.extend(len(data).to_bytes(4, "little"))
return bytes(header + data)
def _flac_payload(rate: int, bits: int, block: int) -> bytes:
streaminfo = bytearray(34)
streaminfo[0:2] = block.to_bytes(2, "big")
streaminfo[2:4] = block.to_bytes(2, "big")
streaminfo[10:18] = ((rate << 44) | ((bits - 1) << 36) | block).to_bytes(8, "big")
size_code = 4 if bits == 16 else 6
frame_header = ((0x3FFE << 18) | (7 << 12) | (size_code << 1)).to_bytes(4, "big")
frame_header += b"\x00" + (block - 1).to_bytes(2, "big")
frame_header += bytes([crc8(frame_header)])
frame = frame_header + b"\x00" + FLAC_CONSTANT.to_bytes(bits // 8, "big", signed=True)
frame += crc16(frame).to_bytes(2, "big")
return b"fLaC" + bytes([0x80, 0, 0, 34]) + bytes(streaminfo) + frame
def _expected_pcm(
source: bytes, *, is_wav: bool, rate: int, bits: int, resample: bool
) -> tuple[list[int], int]:
decoded = decode_wav(source, strict=True) if is_wav else decode_flac(source, strict=True)
processed = process_pcm(
decoded, output_rate=rate, output_channels=2, output_bits=bits, resample=resample,
i2s=True,
)
payload = processed.payload
if len(payload) % 4 != 0:
raise RuntimeError("BAM PCM payload is not word aligned")
words = [
int.from_bytes(payload[offset : offset + 4], "little")
for offset in range(0, len(payload), 4)
]
return words, processed.frames
def _write_pattern_hex(path: Path, words: list[int]) -> None:
padded = list(words) + [0] * (16384 - len(words))
path.write_text("".join(f"{word:08x}\n" for word in padded), encoding="utf-8")
def _compile(build_dir: Path) -> dict[str, Any]:
common = ROOT / "rtl/managed/clusterip/common/rtl"
multimedia = ROOT / "rtl/ip/multimedia"
filelist = build_dir / "apu_p7_concurrent.fl"
filelist.write_text(
"\n".join(
[
"+define+SV_ASSRT_DISABLE",
f"+incdir+{common}",
f"+incdir+{common / 'interface'}",
f"+incdir+{multimedia}",
*(str(source) for source in _production_sources()),
"",
]
),
encoding="utf-8",
)
converted = build_dir / "apu_p7_concurrent.v"
conversion = _run_command(
[
sys.executable,
str(ROOT / "rtl/mini/script/convt_sv2v.py"),
"-f",
str(filelist),
"--output",
str(converted),
],
timeout=900,
)
if conversion["exit_code"] != 0:
raise RuntimeError(f"sv2v conversion failed: {conversion['stderr'][-2000:]}")
binary = build_dir / "apu_p7_concurrent_sim"
icarus = _run_command(
[
"iverilog",
"-g2012",
"-s",
"apu_p7_concurrent_tb",
"-o",
str(binary),
str(converted),
],
timeout=900,
)
if icarus["exit_code"] != 0:
raise RuntimeError(f"Icarus compile failed: {icarus['stderr'][-2000:]}")
result = {
"status": "passed",
"filelist": str(filelist),
"converted": str(converted),
"converted_sha256": _sha256_file(converted),
"binary": str(binary),
"sv2v": conversion,
"icarus": icarus,
}
_write_json(build_dir / "compile.json", result)
return result
def _kws_schedule(rx_words: int, precision: int, rate: int) -> tuple[int, int, int]:
words_per_frame = 2 if precision == 24 else 1
decimation = 6 if rate == 96000 else 3
samples_16k = (rx_words // words_per_frame) // decimation
frames = (samples_16k - 480) // 320 + 1 if samples_16k >= 480 else 0
windows = (samples_16k - 16000) // 1600 + 1 if samples_16k >= 16000 else 0
return samples_16k, frames, windows
def _check_scenario(
scenario: dict[str, Any], parsed: dict[str, Any], workloads: dict[str, Any]
) -> list[str]:
errors: list[str] = []
rate = scenario["rate_hz"]
precision = scenario["precision_bits"]
if parsed["scoreboard"] != "match":
errors.append("testbench scoreboard did not match")
for field in ("underrun", "rx_overrun", "kws_overrun"):
if parsed[field] != 0:
errors.append(f"{field}={parsed[field]} violates the zero-xrun gate")
expected_rx_words = scenario["duration_cycles"] // scenario["frame_period"]
if parsed["rx_words"] != expected_rx_words:
errors.append(f"rx_words={parsed['rx_words']} != expected {expected_rx_words}")
samples_16k, kws_frames, kws_windows = _kws_schedule(parsed["rx_words"], precision, rate)
scenario["kws_16k_samples"] = samples_16k
scenario["expected_kws_frames"] = kws_frames
scenario["expected_kws_windows"] = kws_windows
if parsed["kws_frames"] != kws_frames:
errors.append(f"kws_frames={parsed['kws_frames']} != expected {kws_frames}")
if parsed["kws_windows"] != kws_windows:
errors.append(f"kws_windows={parsed['kws_windows']} != expected {kws_windows}")
if parsed["jobs"] != parsed["wav_jobs"] + parsed["flac_jobs"]:
errors.append("job accounting mismatch")
if parsed["wav_jobs"] < 1 or parsed["flac_jobs"] < 1:
errors.append("WAV and FLAC workloads must both run in every scenario")
expected_tx = (
parsed["wav_jobs"] * workloads["wav_out_words"]
+ parsed["flac_jobs"] * workloads["flac_out_words"]
)
if parsed["tx_words"] != expected_tx or parsed["consumed"] != expected_tx:
errors.append(f"tx_words/consumed != expected {expected_tx}")
expected_input = (
parsed["wav_jobs"] * workloads["wav_bytes"]
+ parsed["flac_jobs"] * workloads["flac_bytes"]
)
if parsed["codec_input_bytes"] != expected_input:
errors.append(f"codec_input_bytes != expected {expected_input}")
if parsed["codec_output_bytes"] != expected_tx * 4:
errors.append(f"codec_output_bytes != expected {expected_tx * 4}")
expected_frames = (
parsed["wav_jobs"] * workloads["wav_frames"]
+ parsed["flac_jobs"] * workloads["flac_frames"]
)
if parsed["codec_frames"] != expected_frames:
errors.append(f"codec_frames != expected {expected_frames}")
return errors
def _run_scenario(
scenario: dict[str, Any],
compile_result: dict[str, Any],
artifacts: dict[str, str],
workloads: dict[str, Any],
timeout: int,
) -> dict[str, Any]:
scenario_dir = Path(artifacts["scenario_dir"])
log_path = scenario_dir / "run.log"
command = [
"vvp",
compile_result["binary"],
f"+APUMC_HEX={artifacts['apumc_hex']}",
f"+APUM_HEX={artifacts['apum_hex']}",
f"+APUC_HEX={artifacts['apuc_hex']}",
f"+WAV_HEX={scenario_dir / 'wav.hex'}",
f"+FLAC_HEX={scenario_dir / 'flac.hex'}",
f"+WAV_PCM_HEX={scenario_dir / 'wav_pcm.hex'}",
f"+FLAC_PCM_HEX={scenario_dir / 'flac_pcm.hex'}",
f"+WAV_BYTES={workloads['wav_bytes']}",
f"+WAV_FRAMES={workloads['wav_frames']}",
f"+WAV_OUT_WORDS={workloads['wav_out_words']}",
f"+WAV_SOURCE_RATE={workloads['wav_source_rate']}",
f"+FLAC_BYTES={workloads['flac_bytes']}",
f"+FLAC_FRAMES={workloads['flac_frames']}",
f"+FLAC_OUT_WORDS={workloads['flac_out_words']}",
f"+RATE={scenario['rate_hz']}",
f"+PRECISION={scenario['precision_bits']}",
f"+DURATION_CYCLES={scenario['duration_cycles']}",
f"+SECONDS_MS={scenario['seconds_ms']}",
f"+MAX_CYCLES={scenario['max_cycles']}",
f"+SEED={scenario['seed']}",
"+WORKLOAD_MASK=3",
]
execution = _run_command(command, timeout=timeout)
log_path.write_text(execution["stdout"] + execution["stderr"], encoding="utf-8")
result: dict[str, Any] = {
"scenario": scenario["id"],
"command": command,
"log": str(log_path),
"exit_code": execution["exit_code"],
"timed_out": execution["timed_out"],
"wall_seconds": execution["duration_seconds"],
"timeout_seconds": timeout,
"status": "failed",
"errors": [],
}
match = RESULT_PATTERN.search(execution["stdout"])
if execution["exit_code"] != 0 or match is None:
tail = (execution["stdout"] + execution["stderr"]).strip().splitlines()[-8:]
result["errors"].append(
"simulator failed" if execution["exit_code"] != 0 else "no CONCURRENT_RESULT line"
)
result["log_tail"] = tail
return result
parsed: dict[str, Any] = {
name: int(value) for name, value in match.groupdict().items() if name != "scoreboard"
}
parsed["scoreboard"] = match.group("scoreboard")
result["result"] = parsed
result["simulated_cycles_per_wall_second"] = (
round(parsed["elapsed_cycles"] / execution["duration_seconds"], 3)
if execution["duration_seconds"] > 0
else None
)
errors = _check_scenario(scenario, parsed, workloads)
result["errors"] = errors
result["status"] = "passed" if not errors else "failed"
return result
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--build-dir", type=Path, required=True)
parser.add_argument("--apumc", type=Path, default=None)
parser.add_argument("--apum", type=Path, default=None)
parser.add_argument(
"--seconds",
type=float,
default=0.5,
help="simulated seconds per scenario; the frozen release gate is 60",
)
parser.add_argument("--smoke", action="store_true", help="minimal single-scenario matrix")
parser.add_argument(
"--rates",
type=int,
nargs="+",
default=None,
choices=(48000, 96000),
help="I2S rates to run (default both, or 48000 in --smoke)",
)
parser.add_argument(
"--precisions",
type=int,
nargs="+",
default=None,
choices=(16, 24),
help="output precisions to run (default both, or 16 in --smoke)",
)
parser.add_argument(
"--smoke-seconds",
type=float,
default=0.03,
help="simulated seconds per scenario in --smoke mode (kept short for pytest)",
)
parser.add_argument(
"--job-frames",
type=int,
default=None,
help="stereo frames per WAV/FLAC job (default 512 in --smoke, else 4096)",
)
parser.add_argument("--timeout-seconds", type=int, default=14400)
parser.add_argument("--seed", type=int, default=0)
parser.add_argument("--output", type=Path, default=None)
args = parser.parse_args()
build_dir = args.build_dir.resolve()
build_dir.mkdir(parents=True, exist_ok=True)
output = args.output.resolve() if args.output else build_dir / "concurrent.json"
for tool in ("iverilog", "vvp", "sv2v"):
if shutil.which(tool) is None:
raise SystemExit(f"required tool not on PATH: {tool}")
if args.seconds <= 0 or args.smoke_seconds <= 0:
raise SystemExit("--seconds/--smoke-seconds must be positive")
seconds = args.smoke_seconds if args.smoke else args.seconds
job_frames = args.job_frames
if job_frames is None:
job_frames = 512 if args.smoke else 4096
if job_frames < 64 or job_frames > 4096:
raise SystemExit("--job-frames must be within 64..4096")
apumc_path = _resolve_apumc(args.apumc)
apumc_payload = apumc_path.read_bytes()
apum_payload, apum_source = _resolve_apum(args.apum)
if len(apum_payload) != 32768:
raise SystemExit(f"APUM image must be 32768 bytes, got {len(apum_payload)}")
apum_sha256 = _sha256_bytes(apum_payload)
apum_matches_freeze = apum_sha256 == APUM_SHA256
if args.smoke and (args.rates is not None or args.precisions is not None):
raise SystemExit("--smoke selects its own minimal matrix; do not combine with --rates/--precisions")
rates = tuple(args.rates) if args.rates is not None else ((48000,) if args.smoke else (48000, 96000))
precisions = (
tuple(args.precisions) if args.precisions is not None else ((16,) if args.smoke else (16, 24))
)
scenarios: list[dict[str, Any]] = []
for rate in rates:
for precision in precisions:
precision_id = "s16" if precision == 16 else "s24_32"
duration_cycles = int(round(seconds * PCLK_HZ))
frame_period = PCLK_HZ // rate // (2 if precision == 24 else 1)
if duration_cycles % frame_period != 0:
raise SystemExit(
f"--seconds {seconds} does not divide the {rate}-Hz/{precision}-bit "
"I2S frame cadence; choose a duration with millisecond resolution"
)
# WAV workload: the P5-qualified playback shapes. A 44.1 kHz
# stereo S16 source resampled to 48 kHz for the 48 kHz I2S
# scenarios, and an 8 kHz stereo S16 source resampled to 96 kHz
# for the 96 kHz scenarios (the PLAYBACK_44100_48000 /
# PLAYBACK_8000_96000 qualification shapes). Source frame counts
# are multiples of 147 for the exact 160/147 output ratio.
wav_source_rate = 44100 if rate == 48000 else 8000
if wav_source_rate == 44100:
wav_source_frames = max(147, 147 * (job_frames // 147))
else:
wav_source_frames = job_frames
wav = _wav_payload(wav_source_rate, 16, wav_source_frames, args.seed)
flac = _flac_payload(rate, precision, job_frames)
wav_pcm, wav_frames = _expected_pcm(
wav, is_wav=True, rate=rate, bits=precision, resample=True
)
flac_pcm, flac_frames = _expected_pcm(
flac, is_wav=False, rate=rate, bits=precision, resample=False
)
if flac_frames != job_frames:
raise SystemExit("FLAC BAM frame count mismatch")
if len(wav_pcm) != wav_frames * (2 if precision == 24 else 1):
raise SystemExit("WAV BAM PCM word count mismatch")
if len(flac_pcm) != flac_frames * (2 if precision == 24 else 1):
raise SystemExit("FLAC BAM PCM word count mismatch")
pair_cycles = (wav_frames + flac_frames) * (PCLK_HZ // rate)
if duration_cycles < pair_cycles:
raise SystemExit(
f"--seconds {seconds} at {rate}-Hz/{precision}-bit cannot fit one "
f"WAV+FLAC job pair ({pair_cycles} playback cycles); increase "
"--seconds or reduce --job-frames"
)
scenarios.append(
{
"id": f"{rate}-{precision_id}",
"rate_hz": rate,
"precision_bits": precision,
"seconds": seconds,
"seconds_ms": int(round(seconds * 1000)),
"duration_cycles": duration_cycles,
"max_cycles": duration_cycles + 400_000_000,
"frame_period": frame_period,
"seed": args.seed,
"wav": wav,
"flac": flac,
"wav_pcm": wav_pcm,
"flac_pcm": flac_pcm,
"workloads": {
"wav_bytes": len(wav),
"flac_bytes": len(flac),
"wav_sha256": _sha256_bytes(wav),
"flac_sha256": _sha256_bytes(flac),
"wav_source_rate": wav_source_rate,
"wav_source_frames": wav_source_frames,
"wav_frames": wav_frames,
"flac_frames": flac_frames,
"wav_out_words": len(wav_pcm),
"flac_out_words": len(flac_pcm),
},
}
)
apumc_hex = build_dir / "apu-p5.hex"
_hex_words(apumc_hex, apumc_payload, 16384)
apum_hex = build_dir / "kws-apum.hex"
_hex_words(apum_hex, apum_payload, 8192)
apuc_hex = build_dir / "kws-apuc.hex"
apuc_payload, _layout = build_apuc(apum_payload)
_hex_words(apuc_hex, apuc_payload, 15376)
artifacts = {
"apumc_hex": str(apumc_hex),
"apum_hex": str(apum_hex),
"apuc_hex": str(apuc_hex),
"scenario_dir": "",
}
started = time.monotonic()
compile_result = _compile(build_dir)
scenario_results: list[dict[str, Any]] = []
failures: list[str] = []
for scenario in scenarios:
scenario_dir = build_dir / "scenarios" / scenario["id"]
scenario_dir.mkdir(parents=True, exist_ok=True)
_hex_words(scenario_dir / "wav.hex", scenario["wav"], 8192)
_hex_words(scenario_dir / "flac.hex", scenario["flac"], 1024)
_write_pattern_hex(scenario_dir / "wav_pcm.hex", scenario["wav_pcm"])
_write_pattern_hex(scenario_dir / "flac_pcm.hex", scenario["flac_pcm"])
workloads = scenario["workloads"]
artifacts["scenario_dir"] = str(scenario_dir)
scenario_results.append(
_run_scenario(scenario, compile_result, artifacts, workloads, args.timeout_seconds)
)
status = scenario_results[-1]["status"]
print(f"APU-P7 concurrent: {scenario['id']} {status}", flush=True)
_write_json(scenario_dir / "result.json", scenario_results[-1])
if status != "passed":
failures.extend(f"{scenario['id']}: {error}" for error in scenario_results[-1]["errors"])
git_revision, git_dirty = _git_revision()
cycles_per_second = [
item["simulated_cycles_per_wall_second"]
for item in scenario_results
if item.get("simulated_cycles_per_wall_second")
]
slowest = min(cycles_per_second) if cycles_per_second else None
projected_per_config = (
round(FULL_GATE_CYCLES / slowest, 1) if slowest else None
)
full_matrix_configs = 4
projection = {
"full_gate_seconds_per_config": FULL_GATE_SECONDS,
"full_gate_cycles_per_config": FULL_GATE_CYCLES,
"full_matrix_configs": full_matrix_configs,
"measured_min_cycles_per_wall_second": slowest,
"projected_wall_seconds_per_config": projected_per_config,
"projected_wall_seconds_full_matrix": (
round(projected_per_config * full_matrix_configs, 1) if projected_per_config else None
),
"projected_full_matrix_exceeds_12h": (
bool(projected_per_config * full_matrix_configs > 12 * 3600)
if projected_per_config
else None
),
}
aggregate = {
"scenarios": len(scenario_results),
"jobs": sum(item["result"]["jobs"] for item in scenario_results if "result" in item),
"codec_underruns": sum(
item["result"]["underrun"] for item in scenario_results if "result" in item
),
"rx_overruns": sum(
item["result"]["rx_overrun"] for item in scenario_results if "result" in item
),
"kws_overruns": sum(
item["result"]["kws_overrun"] for item in scenario_results if "result" in item
),
"codec_input_bytes": sum(
item["result"]["codec_input_bytes"] for item in scenario_results if "result" in item
),
"codec_output_bytes": sum(
item["result"]["codec_output_bytes"] for item in scenario_results if "result" in item
),
"codec_frames": sum(
item["result"]["codec_frames"] for item in scenario_results if "result" in item
),
"codec_cycles": sum(
item["result"]["codec_cycles"] for item in scenario_results if "result" in item
),
}
report = {
"schema_version": 1,
"phase": "APU-P7",
"report": "concurrent",
"gate": "concurrent WAV/FLAC decode with continuous RX KWS zero-xrun",
"status": "passed" if not failures else "failed",
"success": not failures,
"release_qualifying": bool(
not failures
and not args.smoke
and seconds >= FULL_GATE_SECONDS
and set(rates) == {48000, 96000}
and set(precisions) == {16, 24}
),
"mode": "smoke" if args.smoke else "full-matrix",
"seconds_per_scenario": seconds,
"job_frames_per_workload": job_frames,
"required_full_gate_seconds": FULL_GATE_SECONDS,
"pclk_hz": PCLK_HZ,
"git_revision": git_revision,
"git_dirty": git_dirty,
"command": [sys.executable, str(Path(__file__).resolve()), *sys.argv[1:]],
"seed": args.seed,
"profile": PROFILE,
"bundle_prerequisite": BUNDLE_PREREQUISITE,
"inputs": {
"apumc": {
"path": str(apumc_path),
"bytes": len(apumc_payload),
"sha256": _sha256_bytes(apumc_payload),
},
"apum": {
"path": apum_source,
"bytes": len(apum_payload),
"sha256": apum_sha256,
"payload_crc32": f"0x{APUM_PAYLOAD_CRC:08x}",
"matches_locked_freeze": apum_matches_freeze,
},
"tflite": {
"path": str(DEFAULT_TFLITE),
"sha256": _sha256_file(DEFAULT_TFLITE) if DEFAULT_TFLITE.is_file() else None,
},
},
"converter": {
"path": "scripts/apu_kws_convert.py",
"sha256": _sha256_file(ROOT / "scripts/apu_kws_convert.py"),
"contract_revision": CONTRACT_REVISION,
},
"bam": {
"path": "scripts/apu_codecs.py",
"sha256": _sha256_file(ROOT / "scripts/apu_codecs.py"),
},
"rtl_revision": git_revision,
"verification_sources": _verification_sources(),
"simulator": {
"name": "icarus",
"iverilog": _tool_version("iverilog", "-V"),
"vvp": _tool_version("vvp", "-V"),
},
"tools": {
"sv2v": _tool_version("sv2v", "--version"),
"python": platform.python_version(),
},
"compile": compile_result,
"harness": {
"axi_model": "ready memory, no injected stalls",
"playback_prefill_words": 64,
"playback_fifo_words": 1024,
"router_fifo_words": 64,
"rx_model": "one I2S frame word per cadence tick, never re-presented",
"kws_config": "0x0180 (threshold 128, debounce 1)",
},
"matrix": [
{
"scenario": scenario["id"],
"rate_hz": scenario["rate_hz"],
"precision_bits": scenario["precision_bits"],
"seconds": scenario["seconds"],
"workloads": {
"wav": {
"shape": "resample" if scenario["workloads"]["wav_source_rate"]
!= scenario["rate_hz"] else "passthrough",
"source_rate_hz": scenario["workloads"]["wav_source_rate"],
"source_precision_bits": 16,
"source_channels": 2,
"source_frames_per_job": scenario["workloads"]["wav_source_frames"],
"output_frames_per_job": scenario["workloads"]["wav_frames"],
"sha256": scenario["workloads"]["wav_sha256"],
},
"flac": {
"shape": "mono-constant",
"rate_hz": scenario["rate_hz"],
"precision_bits": scenario["precision_bits"],
"block_frames": scenario["workloads"]["flac_frames"],
"output_frames_per_job": scenario["workloads"]["flac_frames"],
"sha256": scenario["workloads"]["flac_sha256"],
},
},
}
for scenario in scenarios
],
"scenarios": scenario_results,
"aggregate_counts": aggregate,
"full_gate_projection": projection,
"artifacts": {
"build_dir": str(build_dir),
"apumc_hex": str(apumc_hex),
"apum_hex": str(apum_hex),
"report": str(output),
},
"errors": failures,
"wall_seconds": round(time.monotonic() - started, 3),
}
_write_json(output, report)
print(f"APU-P7 concurrent report: {output} ({'passed' if not failures else 'failed'})")
return 0 if not failures else 1
if __name__ == "__main__":
raise SystemExit(main())