test: support 1000 KAT Vectors

This commit is contained in:
2026-07-11 19:48:17 +08:00
parent ba160cca34
commit 961f25f50e
5 changed files with 1661 additions and 1202 deletions

366
run_tb.sh
View File

@@ -3,14 +3,14 @@
#
# Usage: ./run_tb.sh <module> [K] [CASE]
# ./run_tb.sh ntt # run the module's full xsim_run.tcl
# ./run_tb.sh top # ML-KEM KeyGen: all K, all cases
# ./run_tb.sh top 2 # only K=2 (ML-KEM-512), all its cases
# ./run_tb.sh top # ML-KEM KeyGen: all K, all KAT vectors
# ./run_tb.sh top 2 # only K=2 (ML-KEM-512), all KAT vectors
# ./run_tb.sh top 2 3 # only K=2, only CASE=3
# ./run_tb.sh top 4 0 # only K=4 (ML-KEM-1024), CASE=0
# ./run_tb.sh enc # ML-KEM Encaps: all K, all cases (0..2)
# ./run_tb.sh enc 2 # Encaps K=2, all its cases
# ./run_tb.sh enc # ML-KEM Encaps: all K, all KAT vectors
# ./run_tb.sh enc 2 # Encaps K=2, all KAT vectors
# ./run_tb.sh enc 4 1 # Encaps K=4, only CASE=1
# ./run_tb.sh dec # ML-KEM Decaps: all K, all cases (0..2)
# ./run_tb.sh dec # ML-KEM Decaps: all K, all KAT vectors
# ./run_tb.sh dec 2 0 # Decaps K=2, only CASE=0
# ./run_tb.sh hello [K] # hello_world end-to-end (single instance; K=2/3/4)
# ./run_tb.sh hello two [K] # hello_world end-to-end (two instances; K=2/3/4)
@@ -20,8 +20,10 @@
# all compile the xvlog lines from sync_rtl/top/TB/xsim_run.tcl. They differ only
# in the testbench: top -> tb_mlkem_kg_katK_xsim, enc -> tb_mlkem_enc_katK_xsim,
# dec -> tb_mlkem_dec_katK_xsim.
# For these, K (2/3/4) and CASE select a single KAT run so you can iterate
# quickly. Other modules ignore the extra args and run their xsim_run.tcl verbatim.
# For these, K (2/3/4) and CASE select a subset of the KAT run so you can
# iterate quickly. Without CASE the script generates/enumerates every vector in
# test_data/kat_MLKEM_{512,768,1024}.rsp. Limit xsim parallelism with
# RUN_TB_JOBS=N (default: min(nproc, 4)).
#
# Prerequisites:
# Vivado 2019.2 at /opt/Xilinx/Vivado/2019.2/
@@ -30,6 +32,8 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VIVADO_SETTINGS="/opt/Xilinx/Vivado/2019.2/settings64.sh"
KAT_VECTOR_DIR="$SCRIPT_DIR/sync_rtl/top/TB/vectors"
KAT_RUN_VECTOR_DIR=""
if [ "$1" = "--list" ]; then
echo "Available modules:"
@@ -117,114 +121,240 @@ execute_tcl() {
# the shared xsim.dir BEFORE any parallel run starts).
#
# run_xsim_jobs <module-tag> <pass-grep-regex> <pass-match-string> <spec>...
# each <spec> = "snapshot:K:CASE". Launches all specs in parallel, waits,
# then prints an ordered summary and returns nonzero if any job failed.
# each <spec> = "snapshot:K:CASE". Launches specs with bounded parallelism,
# waits, then prints an ordered summary and returns nonzero if any job failed.
PAR_BASE="${TMPDIR:-/tmp}/run_tb_par_$$"
KAT_TMP_BASE="${TMPDIR:-/tmp}/run_tb_vectors_$$"
trap 'rm -rf "$PAR_BASE" "$KAT_TMP_BASE"' EXIT
print_timing_summary() {
local logs=("$@")
local found=0 log
for log in "${logs[@]}"; do
if [ -f "$log" ] && grep -q '^TIME ' "$log"; then
found=1
fi
done
[ "$found" -eq 0 ] && return 0
echo ""
echo "Timing summary (clk=20MHz, period=50ns)"
printf " %-7s %-3s %-5s %10s %12s %12s %s\n" "OP" "K" "CASE" "cycles" "time_us" "time_ms" "log"
for log in "${logs[@]}"; do
[ -f "$log" ] || continue
awk -v logfile="$log" '
/^TIME / {
k = ""; c = ""; op = ""; cycles = ""; runtime = "";
for (i = 1; i <= NF; i++) {
split($i, a, "=");
if (a[1] == "K") k = a[2];
else if (a[1] == "CASE") c = a[2];
else if (a[1] == "OP") op = a[2];
else if (a[1] == "cycles") cycles = a[2];
else if (a[1] == "runtime") runtime = a[2];
}
if (runtime != "") {
printf " %-7s %-3s %-5s %10d %12.3f %12.3f %s\n",
op, k, c, cycles, runtime / 1000.0, runtime / 1000000.0, logfile;
}
awk '
/^TIME / {
cycles = ""; runtime = "";
for (i = 1; i <= NF; i++) {
split($i, a, "=");
if (a[1] == "cycles") cycles = a[2];
else if (a[1] == "runtime") runtime = a[2];
}
' "$log"
done
if (cycles != "") {
n++;
sum_cycles += cycles;
if (runtime != "") sum_runtime += runtime;
}
}
END {
if (n == 0) {
print "Timing average: no TIME records found";
} else {
printf "Timing average (clk=20MHz, period=50ns): runs=%d avg_cycles=%.1f avg_time_us=%.3f avg_time_ms=%.3f\n",
n, sum_cycles / n, (sum_runtime / n) / 1000.0, (sum_runtime / n) / 1000000.0;
}
}
' "${logs[@]}"
}
local found_state=0
for log in "${logs[@]}"; do
if [ -f "$log" ] && grep -q '^ STATE ' "$log"; then
found_state=1
fi
done
[ "$found_state" -eq 0 ] && return 0
validate_kat_selection() {
local ksel="$1" csel="$2"
case "$ksel" in
""|2|3|4) ;;
*) echo "ERROR: K must be 2, 3, or 4 (got '$ksel')"; return 1 ;;
esac
if [ -n "$csel" ] && ! [[ "$csel" =~ ^[0-9]+$ ]]; then
echo "ERROR: CASE must be a non-negative integer (got '$csel')"
return 1
fi
}
echo ""
echo "State timing breakdown (clk=20MHz, period=50ns)"
printf " %-7s %-3s %-5s %-12s %10s %12s\n" "OP" "K" "CASE" "STATE" "cycles" "time_us"
for log in "${logs[@]}"; do
[ -f "$log" ] || continue
awk '
/^TIME_BREAKDOWN / {
k = ""; c = ""; op = "";
for (i = 1; i <= NF; i++) {
split($i, a, "=");
if (a[1] == "K") k = a[2];
else if (a[1] == "CASE") c = a[2];
else if (a[1] == "OP") op = a[2];
}
next;
}
/^ STATE / {
state = $2;
cycles = "";
time_ns = "";
for (i = 1; i <= NF; i++) {
split($i, a, "=");
if (a[1] == "cycles") cycles = a[2];
else if (a[1] == "time_ns") time_ns = a[2];
}
if (time_ns != "") {
printf " %-7s %-3s %-5s %-12s %10d %12.3f\n",
op, k, c, state, cycles, time_ns / 1000.0;
}
}
' "$log"
done
generate_kat_vectors() {
local op="$1" ksel="${2:-}" csel="${3:-}"
validate_kat_selection "$ksel" "$csel" || return 1
KAT_RUN_VECTOR_DIR="$KAT_TMP_BASE/$op"
rm -rf "$KAT_RUN_VECTOR_DIR"
mkdir -p "$KAT_RUN_VECTOR_DIR"
[ -d "$KAT_VECTOR_DIR/encgold" ] && ln -s "$KAT_VECTOR_DIR/encgold" "$KAT_RUN_VECTOR_DIR/encgold"
[ -d "$KAT_VECTOR_DIR/decgold" ] && ln -s "$KAT_VECTOR_DIR/decgold" "$KAT_RUN_VECTOR_DIR/decgold"
python3 - "$SCRIPT_DIR" "$op" "${ksel:-all}" "${csel:-all}" "$KAT_RUN_VECTOR_DIR" <<'PY'
import pathlib
import sys
root = pathlib.Path(sys.argv[1])
op = sys.argv[2]
ksel = sys.argv[3]
csel = sys.argv[4]
out_dir = pathlib.Path(sys.argv[5])
ks = [2, 3, 4] if ksel == "all" else [int(ksel)]
case_filter = None if csel == "all" else int(csel)
rsp_by_k = {
2: root / "test_data" / "kat_MLKEM_512.rsp",
3: root / "test_data" / "kat_MLKEM_768.rsp",
4: root / "test_data" / "kat_MLKEM_1024.rsp",
}
def parse_rsp(path):
records = []
cur = {}
with path.open("r", encoding="ascii") as f:
for raw in f:
line = raw.strip()
if not line or line.startswith("#") or " = " not in line:
continue
key, val = line.split(" = ", 1)
if key == "count":
if cur:
records.append(cur)
cur = {"count": int(val)}
else:
cur[key] = val.lower()
if cur:
records.append(cur)
return records
def bytes_of(hexstr, field, count, k):
if len(hexstr) % 2:
raise SystemExit(f"odd hex length for {field}, K={k}, count={count}")
return [hexstr[i:i + 2] for i in range(0, len(hexstr), 2)]
def write_bytes(path, hexstr, field, count, k):
data = bytes_of(hexstr, field, count, k)
path.write_text("".join(b + "\n" for b in data), encoding="ascii")
def write_word256_reversed(path, hexstr, field, count, k):
data = bytes_of(hexstr, field, count, k)
if len(data) != 32:
raise SystemExit(f"{field} must be 32 bytes, got {len(data)}, K={k}, count={count}")
path.write_text("".join(reversed(data)) + "\n", encoding="ascii")
def require(rec, keys, k):
missing = [key for key in keys if key not in rec]
if missing:
raise SystemExit(f"missing {','.join(missing)} in K={k}, count={rec.get('count')}")
summary = []
for k in ks:
records = parse_rsp(rsp_by_k[k])
wrote = 0
for rec in records:
count = rec["count"]
if case_filter is not None and count != case_filter:
continue
tag = f"k{k}"
if op == "top":
require(rec, ["d", "z", "pk", "sk"], k)
base = out_dir / f"kat_{tag}_c{count}"
write_word256_reversed(base.with_name(base.name + "_d.hex"), rec["d"], "d", count, k)
write_word256_reversed(base.with_name(base.name + "_z.hex"), rec["z"], "z", count, k)
write_bytes(base.with_name(base.name + "_ek.hex"), rec["pk"], "pk", count, k)
write_bytes(base.with_name(base.name + "_dk.hex"), rec["sk"], "sk", count, k)
elif op == "enc":
require(rec, ["pk", "msg", "ct", "ss"], k)
base = out_dir / f"enc_{tag}_c{count}"
write_bytes(base.with_name(base.name + "_ek.hex"), rec["pk"], "pk", count, k)
write_bytes(base.with_name(base.name + "_m.hex"), rec["msg"], "msg", count, k)
write_bytes(base.with_name(base.name + "_ct.hex"), rec["ct"], "ct", count, k)
write_bytes(base.with_name(base.name + "_ss.hex"), rec["ss"], "ss", count, k)
elif op == "dec":
require(rec, ["sk", "ct", "ss", "ct_n", "ss_n"], k)
base = out_dir / f"dec_{tag}_c{count}"
write_bytes(base.with_name(base.name + "_dk.hex"), rec["sk"], "sk", count, k)
write_bytes(base.with_name(base.name + "_ct.hex"), rec["ct"], "ct", count, k)
write_bytes(base.with_name(base.name + "_ss.hex"), rec["ss"], "ss", count, k)
write_bytes(base.with_name(base.name + "_ctn.hex"), rec["ct_n"], "ct_n", count, k)
write_bytes(base.with_name(base.name + "_ssn.hex"), rec["ss_n"], "ss_n", count, k)
else:
raise SystemExit(f"unknown KAT op: {op}")
wrote += 1
if case_filter is not None and wrote == 0:
raise SystemExit(f"CASE={case_filter} not found in {rsp_by_k[k]}")
summary.append(f"K={k}:{wrote}")
print(f" generated {op} KAT vector files ({', '.join(summary)})")
PY
}
kat_cases_for() {
local op="$1" k="$2" prefix suffix
local dir="${KAT_RUN_VECTOR_DIR:-$KAT_VECTOR_DIR}"
case "$op" in
top) prefix="kat_k${k}_c"; suffix="_d\\.hex" ;;
enc) prefix="enc_k${k}_c"; suffix="_ek\\.hex" ;;
dec) prefix="dec_k${k}_c"; suffix="_dk\\.hex" ;;
*) echo "ERROR: unknown KAT op '$op'" >&2; return 1 ;;
esac
find "$dir" -maxdepth 1 -type f -printf '%f\n' \
| sed -n "s/^${prefix}\([0-9][0-9]*\)${suffix}$/\1/p" \
| sort -n \
| tr '\n' ' '
}
run_xsim_jobs() {
local mtag="$1" pgrep="$2" pmatch="$3"; shift 3
local specs=("$@")
local repo; repo="$(pwd)"
local max_jobs="${RUN_TB_JOBS:-}"
if [ -z "$max_jobs" ]; then
if command -v nproc >/dev/null 2>&1; then
max_jobs="$(nproc)"
[ "$max_jobs" -gt 4 ] && max_jobs=4
else
max_jobs=4
fi
fi
case "$max_jobs" in
''|*[!0-9]*|0) echo "ERROR: RUN_TB_JOBS must be a positive integer (got '$max_jobs')"; return 1 ;;
esac
rm -rf "$PAR_BASE"; mkdir -p "$PAR_BASE"
echo " launching ${#specs[@]} xsim job(s), parallelism=$max_jobs"
# launch all jobs in parallel
local s snap k c wd
local s snap k c wd active=0 launched=0 total_specs=${#specs[@]}
for s in "${specs[@]}"; do
IFS=: read -r snap k c <<< "$s"
launched=$((launched + 1))
if [ -t 1 ]; then
printf "\r running [%d/%d] K=%s CASE=%s" "$launched" "$total_specs" "$k" "$c"
else
echo " running [$launched/$total_specs] K=$k CASE=$c"
fi
wd="$PAR_BASE/${mtag}_k${k}_c${c}"
mkdir -p "$wd"
cp -r "$repo/xsim.dir" "$wd/xsim.dir"
# The TB loads vectors via repo-relative $readmemh paths
# (sync_rtl/top/TB/vectors/...). xsim resolves these against the run
# cwd, so symlink the repo's sync_rtl tree into each private workdir.
ln -sf "$repo/sync_rtl" "$wd/sync_rtl"
# The TB loads vectors via repo-relative $readmemh paths. xsim resolves
# these against the run cwd, so mount the generated vector directory
# under each private workdir.
if [ -n "$KAT_RUN_VECTOR_DIR" ]; then
mkdir -p "$wd/sync_rtl/top/TB"
ln -sf "$KAT_RUN_VECTOR_DIR" "$wd/sync_rtl/top/TB/vectors"
else
ln -sf "$repo/sync_rtl" "$wd/sync_rtl"
fi
(
cd "$wd"
echo " xsim $snap -R -testplusarg CASE=$c"
xsim "$snap" -R -testplusarg "CASE=$c" --nolog > "$wd/run.out" 2>&1
{
echo "xsim $snap -R -testplusarg CASE=$c"
xsim "$snap" -R -testplusarg "CASE=$c" --nolog
} > "$wd/run.out" 2>&1
) &
active=$((active + 1))
if [ "$active" -ge "$max_jobs" ]; then
wait -n || true
active=$((active - 1))
fi
done
wait
wait || true
[ -t 1 ] && echo ""
# ordered summary + per-job log copy to /tmp/run_tb_<mtag>_k*_c*.log
local fail=0 pf nf log timing_logs=()
# ordered result aggregation + per-job log copy to /tmp/run_tb_<mtag>_k*_c*.log
local fail=0 pf nf log ok
local total=0 pass_count=0 fail_count=0 noresult_count=0 fnf_count=0
local timing_logs=() failures=()
for s in "${specs[@]}"; do
IFS=: read -r snap k c <<< "$s"
wd="$PAR_BASE/${mtag}_k${k}_c${c}"
@@ -233,14 +363,38 @@ run_xsim_jobs() {
timing_logs+=("$log")
pf=$(grep -oE "$pgrep" "$log" | tail -1)
nf=$(grep -c 'cannot be opened' "$log")
echo " K=$k CASE=$c -> ${pf:-NORESULT} (file-not-found=$nf, log: $log)"
total=$((total + 1))
[ -n "$pf" ] || noresult_count=$((noresult_count + 1))
[ "$nf" -eq 0 ] || fnf_count=$((fnf_count + 1))
# pmatch == "PASS*" means "any PASS (Dn) stage tag"; else exact match.
if [ "$pmatch" = "PASS*" ]; then
{ [[ "$pf" == PASS* ]] && [ "$nf" -eq 0 ]; } || fail=1
[[ "$pf" == PASS* ]] && [ "$nf" -eq 0 ]; ok=$?
else
{ [ "$pf" = "$pmatch" ] && [ "$nf" -eq 0 ]; } || fail=1
[ "$pf" = "$pmatch" ] && [ "$nf" -eq 0 ]; ok=$?
fi
if [ "$ok" -eq 0 ]; then
pass_count=$((pass_count + 1))
else
fail=1
fail_count=$((fail_count + 1))
if [ "${#failures[@]}" -lt 20 ]; then
failures+=("K=$k CASE=$c -> ${pf:-NORESULT} (file-not-found=$nf, log: $log)")
fi
fi
done
echo ""
echo "Result summary: total=$total success=$pass_count failure=$fail_count noresult=$noresult_count file-not-found=$fnf_count"
if [ "$fail_count" -gt 0 ]; then
echo "Failed cases (first ${#failures[@]} of $fail_count):"
local item
for item in "${failures[@]}"; do
echo " $item"
done
fi
print_timing_summary "${timing_logs[@]}"
rm -rf "$PAR_BASE"
return $fail
@@ -248,11 +402,12 @@ run_xsim_jobs() {
# Fast single-K / single-case path for the 'top' (ML-KEM KeyGen) module.
# Compiles the same RTL as the tcl, but elaborates ONLY the requested K and
# runs the CASE(s) in parallel. Avoids the 3-snapshot + 11-run serial sweep.
# runs the generated/enumerated CASE(s) through the bounded parallel launcher.
run_top_selected() {
local tcl_file="$1" ksel="$2" csel="$3"
set +e # we do our own error handling / result parsing here
generate_kat_vectors top "$ksel" "$csel" || return 1
rm -rf xsim.dir .Xil
# Compile every xvlog line from the tcl, verbatim.
@@ -267,10 +422,13 @@ run_top_selected() {
echo " xelab tb_mlkem_kg_katK_xsim -generic_top KP=$k -s mlkem_kg_k$k --timescale 1ns/1ps"
xelab tb_mlkem_kg_katK_xsim -generic_top KP=$k -s mlkem_kg_k$k --timescale 1ns/1ps \
|| { echo "ELAB FAILED for K=$k"; return 1; }
# Cases per K: K=2 has 0..4, K=3/4 have 0..2.
local cases
if [ "$k" = "2" ]; then cases="0 1 2 3 4"; else cases="0 1 2"; fi
if [ -n "$csel" ]; then cases="$csel"; fi
if [ -n "$csel" ]; then
cases="$csel"
else
cases="$(kat_cases_for top "$k")"
fi
[ -n "$cases" ] || { echo "ERROR: no KeyGen KAT cases found for K=$k"; return 1; }
local c
for c in $cases; do specs+=("mlkem_kg_k$k:$k:$c"); done
done
@@ -285,6 +443,7 @@ run_enc_selected() {
local tcl_file="$1" ksel="$2" csel="$3"
set +e
generate_kat_vectors enc "$ksel" "$csel" || return 1
rm -rf xsim.dir .Xil
# Compile every xvlog line from the tcl EXCEPT the KeyGen TB (the encaps TB
@@ -306,8 +465,13 @@ run_enc_selected() {
echo " xelab tb_mlkem_enc_katK_xsim -generic_top KP=$k -s mlkem_enc_k$k --timescale 1ns/1ps"
xelab tb_mlkem_enc_katK_xsim -generic_top KP=$k -s mlkem_enc_k$k --timescale 1ns/1ps \
|| { echo "ELAB FAILED for K=$k"; return 1; }
local cases="0 1 2"
if [ -n "$csel" ]; then cases="$csel"; fi
local cases
if [ -n "$csel" ]; then
cases="$csel"
else
cases="$(kat_cases_for enc "$k")"
fi
[ -n "$cases" ] || { echo "ERROR: no Encaps KAT cases found for K=$k"; return 1; }
local c
for c in $cases; do specs+=("mlkem_enc_k$k:$k:$c"); done
done
@@ -321,6 +485,7 @@ run_dec_selected() {
local tcl_file="$1" ksel="$2" csel="$3"
set +e
generate_kat_vectors dec "$ksel" "$csel" || return 1
rm -rf xsim.dir .Xil
while read -r cmd; do
@@ -338,8 +503,13 @@ run_dec_selected() {
echo " xelab tb_mlkem_dec_katK_xsim -generic_top KP=$k -s mlkem_dec_k$k --timescale 1ns/1ps"
xelab tb_mlkem_dec_katK_xsim -generic_top KP=$k -s mlkem_dec_k$k --timescale 1ns/1ps \
|| { echo "ELAB FAILED for K=$k"; return 1; }
local cases="0 1 2"
if [ -n "$csel" ]; then cases="$csel"; fi
local cases
if [ -n "$csel" ]; then
cases="$csel"
else
cases="$(kat_cases_for dec "$k")"
fi
[ -n "$cases" ] || { echo "ERROR: no Decaps KAT cases found for K=$k"; return 1; }
local c
for c in $cases; do specs+=("mlkem_dec_k$k:$k:$c"); done
done