Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 243 additions & 0 deletions .github/workflows/asic_gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# Copyright © 2019-2026
#
# Licensed under the Apache License, Version 2.0 (the "License").
#
# asic_gate — nightly ASIC synthesis-regression gate on hosted runners.
# Synthesizes the DUT catalog (ci/testcases/asic_gate.yaml) through
# Yosys + OpenSTA on ASAP7 and asserts Fmax/cell area against the golden results
# in ci/baselines/synthesis/yosys/, so an RTL change that costs timing closure or
# area cannot sit unnoticed on master.
#
# Why its own workflow rather than a cell in ci.yml:
#
# - A DUT takes 1-2 hours, so the builds must fan out to ONE STANDALONE JOB
# EACH. A ci.yml cell is one job running a pytest slice; eleven sequential
# synthesis runs in one cell would be a day.
# - Nightly builds are expensive even when free, so the run is SKIPPED unless
# master has actually moved since the last gate run. Unlike the self-hosted
# fpga_gate, a hosted runner keeps no state between runs, so the "already
# gated this commit" marker lives in the actions cache, keyed by SHA.
#
# It always gates master, whatever branch the schedule happens to fire on. No
# licence and no dedicated machine: yosys/sv2v/OpenSTA ship in the prebuilt
# toolchain and ASAP7 is a 59 MB content-addressed fetch.
#
# See docs/designs/continuous_integration.md §3.5.

name: ASIC Gate

on:
schedule:
- cron: '0 4 * * *' # nightly 04:00 UTC (an hour behind fpga_gate)
workflow_dispatch:
inputs:
builds:
description: "build ids/groups, space separated (blank = all)"
default: ""
force:
description: "run even if master has not moved since the last gate"
type: boolean
default: false

# One sweep at a time, so two nights' runs cannot both claim the SHA marker.
# Never cancel a run in flight -- hours of synthesis are already spent.
concurrency:
group: asic-gate
cancel-in-progress: false

env:
CCACHE_DISABLE: 1

jobs:
# ---------------------------------------------------------------------------
# plan — decide whether to run at all, and emit one matrix entry per build.
# No build env; just PyYAML.
# ---------------------------------------------------------------------------
plan:
runs-on: ubuntu-22.04
outputs:
run: ${{ steps.q.outputs.run }}
builds: ${{ steps.q.outputs.builds }}
sha: ${{ steps.q.outputs.sha }}
key: ${{ steps.q.outputs.key }}
steps:
- uses: actions/checkout@v4
with:
ref: master # hard-pinned: the gate always tracks master
fetch-depth: 0
- run: pip install --quiet pyyaml

# The marker key is master's head AS CHECKED OUT, not github.sha: on a
# schedule those are normally the same, but the thing being gated is what
# the checkout produced. Computed here rather than with hashFiles() in the
# step below, which cannot see a runtime value.
- name: Compute gate key
id: key
run: |
set -euo pipefail
SHA=$(git rev-parse HEAD)
# The spec is in the key too, so editing the build list re-gates a
# commit that was already gated under the old list.
SPEC=$(sha256sum ci/testcases/asic_gate.yaml | cut -c1-16)
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "key=asic-gate-$SHA-$SPEC" >> "$GITHUB_OUTPUT"

# Probe (lookup-only, no download) for this commit's marker. A HIT means a
# previous run already reached a verdict on this SHA, so master has not
# moved and there is nothing new to gate. The `report` job writes the
# marker at the end.
- name: Read gate marker
id: marker
uses: actions/cache/restore@v4
with:
path: .asic_gate_sha
key: ${{ steps.key.outputs.key }}
lookup-only: true

- name: Plan builds
id: q
env:
IN_BUILDS: ${{ github.event.inputs.builds }}
FORCE: ${{ github.event.inputs.force }}
HIT: ${{ steps.marker.outputs.cache-hit }}
SHA: ${{ steps.key.outputs.sha }}
KEY: ${{ steps.key.outputs.key }}
run: |
set -euo pipefail
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
echo "key=$KEY" >> "$GITHUB_OUTPUT"
if [ "$HIT" = "true" ] && [ "${FORCE:-}" != "true" ]; then
echo "master unchanged since the last gate run ($SHA) — skipping"
echo "run=false" >> "$GITHUB_OUTPUT"
echo "builds=[]" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "gating $SHA"
# Dispatch inputs only narrow the catalog, and reach the shell through
# an env var: interpolating one into the script would let a dispatch
# value execute arbitrary commands on the runner.
ARGS=()
for b in ${IN_BUILDS:-}; do ARGS+=(-b "$b"); done
BUILDS=$(python3 ci/asic_gate.py --matrix "${ARGS[@]}")
echo "builds=$BUILDS" >> "$GITHUB_OUTPUT"
[ "$BUILDS" = "[]" ] && echo "run=false" >> "$GITHUB_OUTPUT" || echo "run=true" >> "$GITHUB_OUTPUT"
python3 ci/testcase.py lint

# ---------------------------------------------------------------------------
# synth — one standalone job per DUT. Each configures its own build tree and
# gates exactly one build, so a 2-hour DUT costs 2 hours of wall time, not 2
# hours of everyone else's.
# ---------------------------------------------------------------------------
synth:
needs: plan
if: needs.plan.outputs.run == 'true'
runs-on: ubuntu-22.04
timeout-minutes: 300
strategy:
# Never cancel siblings: each DUT is an independent measurement, and one
# regression must not hide the ten other numbers this run would have
# produced.
fail-fast: false
max-parallel: 6
matrix:
build: ${{ fromJson(needs.plan.outputs.builds) }}
steps:
# The SHA the plan job resolved, not `master`: master moving mid-run must
# not split one gate across two source trees.
- uses: actions/checkout@v4
with:
ref: ${{ needs.plan.outputs.sha }}
submodules: recursive

# Restores the prebuilt toolchain cache, which already carries yosys, sv2v
# and OpenSTA (ci/toolchain_install.sh installs all three by default).
- name: Setup Vortex
uses: ./.github/actions/setup-vortex

# ASAP7 is content-addressed by its manifest, so that file's hash IS the
# cache key. Every job in the matrix shares it; the first to finish saves
# it and the rest log a harmless "cache already exists". The gate installs
# it on a miss (hw/syn/yosys/Makefile's `asap7` target) -- about a minute.
- name: Cache ASAP7 PDK
uses: actions/cache@v4
with:
path: build32_asic_gate/hw/syn/libs/asap7
key: asap7-rvt-${{ hashFiles('hw/syn/libs/asap7/manifest.txt') }}

# The command is the one the catalog declares (ci/testcases/asic_gate.yaml's
# single `run:` case), narrowed to this job's build, so what CI runs and
# what the catalog says cannot drift apart.
- name: Gate ${{ matrix.build.id }}
id: run
env:
BUILD_ID: ${{ matrix.build.id }}
run: |
set -uo pipefail
CMD=$(python3 -c "import yaml; print(yaml.safe_load(open('ci/testcases/asic_gate.yaml'))['tests'][0]['run'])")
rc=0
# --timeout under the job's timeout-minutes, so a hung build is
# reported BY the gate (with the phase it died in, and a report to
# upload) instead of vanishing into a GitHub job kill.
$CMD -b "$BUILD_ID" --timeout 16200 \
--report "asic_gate_$BUILD_ID.json" || rc=$?
echo "rc=$rc" >> "$GITHUB_OUTPUT"
exit 0

- uses: actions/upload-artifact@v4
if: always()
with:
name: asic-gate-${{ matrix.build.id }}
path: |
asic_gate_${{ matrix.build.id }}.json
build32_asic_gate/hw/syn/yosys/asic_gate_*/build.log
build32_asic_gate/hw/syn/yosys/asic_gate_*/synth_summary.csv
build32_asic_gate/hw/syn/yosys/asic_gate_*/reports/
if-no-files-found: warn

- name: Verdict
run: |
case "${{ steps.run.outputs.rc }}" in
0) echo "asic_gate ${{ matrix.build.id }} passed" ;;
1) echo "::error::asic_gate ${{ matrix.build.id }} FAILED — Fmax/area moved beyond threshold vs baseline"; exit 1 ;;
*) echo "::error::asic_gate ${{ matrix.build.id }} build error (see logs)"; exit 1 ;;
esac

# ---------------------------------------------------------------------------
# report — collect every build's verdict into one summary, and record the SHA
# so tomorrow's run skips an unchanged master.
# ---------------------------------------------------------------------------
report:
needs: [plan, synth]
if: always() && needs.plan.outputs.run == 'true'
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ needs.plan.outputs.sha }}
- uses: actions/download-artifact@v4
with:
pattern: asic-gate-*
path: reports
merge-multiple: true

- name: Summarize
id: sum
run: |
set -uo pipefail
python3 ci/synth_report.py reports >> "$GITHUB_STEP_SUMMARY"
echo "rc=$?" >> "$GITHUB_OUTPUT"
exit 0

# Record the SHA once every build has reached a VERDICT (pass or
# regression), so a red master is not re-synthesized every night — the
# failed run is the record. A build error (rc=2) does NOT record, so the
# next nightly retries it.
- name: Record gated SHA
if: steps.sum.outputs.rc != '2'
run: echo "${{ needs.plan.outputs.sha }}" > .asic_gate_sha
- name: Save gate marker
if: steps.sum.outputs.rc != '2'
uses: actions/cache/save@v4
with:
path: .asic_gate_sha
key: ${{ needs.plan.outputs.key }}
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
/build*
/.*
# ...but not the CI definitions. `/.*` ignores every dotfile at the root, and
# .github is one: the workflows already tracked are unaffected (ignore rules do
# not apply to tracked files), so a NEW workflow or action would be silently
# dropped from a commit with nothing to notice.
!/.github/
# Archived/superseded docs — kept on disk, never tracked
docs/archives/

# ci/ holds importable modules now (synth_gate is imported by its entry points)
__pycache__/

# ASAP7 collateral fetched by hw/syn/libs/asap7/install.sh (never vendored)
hw/syn/libs/asap7/downloads/
hw/syn/libs/asap7/lib/
hw/syn/libs/asap7/verilog/
hw/syn/libs/asap7/metadata/
hw/syn/libs/asap7/.installed_*
hw/syn/libs/asap7/install.sh
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This is the canonical entry point for **both human contributors and AI coding ag
### Simulation & test
- [docs/simulation.md](docs/simulation.md) — driver modes (simx, rtlsim, opae, xrt) and blackbox usage
- [docs/testing.md](docs/testing.md) — test and regression flow
- [docs/debugging.md](docs/debugging.md) — debug traces (`--debug`), VCD, scope, trace_csv
- [docs/debugging.md](docs/debugging.md) — debug traces (`--debug`), VCD (`--vcd`), scope, trace_csv
- [docs/debug_mode.md](docs/debug_mode.md) — debug-mode hardware support
- [docs/perfetto_analysis.md](docs/perfetto_analysis.md) — Perfetto trace and analysis
- [docs/synthesis_analysis.md](docs/synthesis_analysis.md) — synthesis/PPA analysis
Expand Down Expand Up @@ -87,6 +87,7 @@ See [docs/testing.md](docs/testing.md) and [docs/debugging.md](docs/debugging.md
- **RTL coverage path is `xrt`, not `rtlsim`.** When discussing or planning RTL verification, `xrt` is the canonical path — `rtlsim` bypasses the AFU surface. `rtlsim` remains useful for fast iteration on processor RTL; `xrt` is what proves the full integration.
- **`ci/regression.sh` is the canonical source of tested configurations.** Use it to discover supported parameter combinations before inventing ad hoc ones.
- **Perf-regression baselines (`ci/baselines/perf/*.json`) are golden data — never hand-edit them, and never "fix" a red perf gate by bumping the number.** They are regenerated only by `pytest ci -m perf_gate --update-baselines` (a human-run, reviewed step), and CI must never pass that flag. A `perf_gate` failure means real cycles moved: root-cause it, or — if the change is intended — regenerate the baseline so the diff shows the perf delta for review. Same rule as image goldens and `known_issue:`.
- **Synthesis baselines (`ci/baselines/synthesis/{xilinx,yosys}/*.json`) are golden data under the same rule** — never hand-edited, regenerated only by a human running `ci/fpga_gate.py --update-baseline` / `ci/asic_gate.py --update-baseline`, never by CI. A red `fpga_gate`/`asic_gate` means real Fmax or area moved. Note the two gates measure the *same* `hw/unittest/*/VX_*_top.sv` wrappers through different flows, so a divergence between them is a finding, not noise — keep `hw/syn/{xilinx,yosys}/dut/catalog.mk` in step when you add or rename a DUT. See [docs/designs/continuous_integration.md §3.5](docs/designs/continuous_integration.md).
- **SimX is the RTL's timing model — keep them in lockstep.** Any change that moves RTL cycles (pipeline structure, arbitration, queue depths, cache/memory behavior) must land together with the matching SimX timing-model update, and vice-versa. The `model_parity` CI gate enforces this: a `check: model_parity` case (`ci/testcases/core.yaml` + per-extension parity categories) runs the same app/args/configs on simx and rtlsim and asserts exact retired-instruction match plus cycle agreement within the case tolerance (default 5%). Never widen a tolerance to absorb a divergence — model the behavior. When adding a hardware feature, add or extend a parity case that exercises it.
- **When RTL debugging stalls, switch to the SimX-as-oracle pattern.** For numerical bugs, deep pipeline races, or any failure mode where rtlsim is "close but wrong": (1) build/extend the SimX C++ model so it mirrors the *new* RTL architecture and gets to PASS; (2) add matching trace dumps to both SimX and RTL (cycle, FU events, SRAM addresses+data, hazards) — same CSV format on both sides; (3) diff trace files — the first divergence is the bug. Don't keep guessing from output values; localize via trace diff. See [docs/debugging.md](docs/debugging.md#simx-as-oracle-for-rtl-debug).

Expand All @@ -108,7 +109,7 @@ make -C tests/opencl run-rtlsim

### Architecture overrides

`blackbox.sh` exposes the common knobs directly: `--clusters=`, `--cores=`, `--warps=`, `--threads=`, `--l2cache`, `--l3cache`, `--debug=`, `--perf=`. For anything not exposed as a flag, use `CONFIGS="-D..."` (all parameters take the `VX_CFG_*` prefix — e.g. `-DVX_CFG_NUM_THREADS=8`, `-DVX_CFG_EXT_TCU_ENABLE`). Baseline parameters live in `VX_config.toml` and `VX_types.toml` at the repo root — edit those only when an override is needed for *all* builds, and re-`configure` afterward.
`blackbox.sh` exposes the common knobs directly: `--clusters=`, `--cores=`, `--warps=`, `--threads=`, `--l2cache`, `--l3cache`, `--debug=`, `--vcd`, `--perf=`. For anything not exposed as a flag, use `CONFIGS="-D..."` (all parameters take the `VX_CFG_*` prefix — e.g. `-DVX_CFG_NUM_THREADS=8`, `-DVX_CFG_EXT_TCU_ENABLE`). Baseline parameters live in `VX_config.toml` and `VX_types.toml` at the repo root — edit those only when an override is needed for *all* builds, and re-`configure` afterward.

```bash
./ci/blackbox.sh --driver=simx --app=sgemm --clusters=1 --cores=2 --warps=4 --threads=4 --l2cache
Expand Down
Loading
Loading