The Co-design Loop: Mapping and Hardware Together¶
An agent proposes a machine. A frozen mapper enumerates the whole loop-nest
mapspace against that machine, asks it which nests it can encode, and the best
one it can is what goes under the RTL. Cycles come from Vitis C/RTL cosim and
resources from csynth of the same build, reported as a pair.
This is the loop ACT and the TinyTPU-isa Mapper argued for, built. It reuses ACT’s
interface – a mapping as a tuple of Loop(rank, factor, level, spatial) –
and none of its code, for the reasons that page measures. ACT is Kai Shao’s
work (https://github.com/kkkaishao/allo, branch act); see ATTRIBUTION.md
at tag chia-codesign-final.
The design-level loop it is built on – the tool surface, the path allowlist,
the fresh evaluation tree from git show, byte-identity on frozen files,
import-time policing, bwrap, nonce-vouched gates, the spend caps – is
CHIA Agentic Co-Design. Only what the co-design loop adds is described here.
Why the loop has to be a co-design loop¶
Because the mapping search alone finds almost nothing, and that is measured.
At 16x16x16 the mapper enumerates 1,226 nests and the shipped hardware can
encode 3. On the shipped hardware the nest the design already ships is the
best of those 3 at 16x16x16, so the only thing a pure mapping search buys is
3 cycles at 4x4x4 – the trip-count-1 loop/endloop pair the
enumerator omits and the hand-written program keeps (see The control, below).
The thing that collapsed the mapspace is a hardware/ISA parameter, not a compiler bug:
nests |
cause |
what it is |
|---|---|---|
1,150 |
|
|
54 |
|
a limitation of |
17 |
|
|
2 |
|
|
LOOP_DEPTH=4 refuses nothing at all here. So the agent’s lever is the
machine, and the number of encodable nests is the first thing a change moves.
What is editable, and what is frozen¶
files |
how it is enforced |
|
|---|---|---|
editable |
|
the agent edits a private copy under |
frozen |
|
|
The agent may read the mapper (read_reference("mapspace.py")). It should
– the rule is what “better nest” means. It just cannot change it. The frozen
half is what makes “the best mapping for this hardware” a claim rather than a
sample.
The inner loop is exhaustive, not agentic¶
For each hardware candidate, mapspace.search enumerates the whole
mapspace and asks the candidate’s encoder about every nest. It does not search
cleverly and does not stop early.
That is the design decision the loop turns on. A non-exhaustive inner search would turn “which hardware runs the better program” into “whose search got luckier”. Keeping the mapspace enumerable is therefore a requirement, not a convenience – at 16x16x16 it is 1,226 nests and about one second of pure python.
The selection rule, stated once and frozen: among the nests the candidate can
encode at a shape, take the minimum of (dynamic instruction issues, static
instruction words, nest string). Both counts are static properties of the
program, not cycle estimates; the nest string only breaks ties so the choice is
deterministic. A nest must be encodable at both relu settings, and is
ranked by the relu=False program, which is the one the scored testbench
runs.
A nest counts as encodable only if the encoder produced a program and
assemble took it – which is where IMEM_SIZE is enforced. A program the
instruction memory cannot hold is refused, never truncated.
Feedback is measured, not modelled¶
Cycles: Vitis HLS 2023.2
csynth+xsimC/RTL cosim, through main’s frozencosim.py, at 4x4x4 and 16x16x16 in the search and at all fiveSHAPESinaccept.py. Each testbench bit-exact against numpy, one row per shape,mismatches = 0in the summary and in that shape’s own log, a PASS, and the cycle count cross-checked against the log’s simulated time.Resources: FF, LUT, BRAM18K, DSP, URAM and the estimated clock from the
csynth.xmlof the same build. The clock must meet 3.33 ns, or cycles at a clock the design cannot meet are not comparable.
codesign_cosim.py is how the mapper’s program reaches the RTL: cosim.py
stays byte-identical to main, and this frozen driver binds cosim.gemm_program
to gemm_from_nest(mapspace.select(...)) before calling cosim.main().
Same testbench generator, same operands, same golden reference, same TCL.
No modelled cycle count is ever reported. The only proxy in the loop is the
static ranking above, it lives inside mapspace.py, and it never leaves it.
The failure mode being avoided is named in ACT and the TinyTPU-isa Mapper: ACT’s
Priced.cost is (makespan, emits) and models no instruction memory, while
assemble asserts len(words) <= IMEM_SIZE, so a loop scored on it would
reward a program the machine cannot hold. The five-point regression
74.5 + 21.70 x dynamic invites the same mistake more cheaply.
The objective is a pair, and is never collapsed¶
classify() in loop.py, frozen:
win – a Pareto improvement: no shape’s cycle count higher, at least one lower, and no resource term grown.
trade – the total cycles fall, but something is paid for it: a resource term grew, or another shape regressed.
regression – the total does not fall.
A candidate that trades 2.3x the block RAM for 9% of the cycles is a trade. That
exact trade has already happened on this design once: run 1’s dma_ld
widening took BRAM18K from 42 to 98 for -59 cycles at the largest shape. The
loop still tracks its best by total cycles so that the search can move, but the
pair is what gets reported.
Correctness is absolute¶
A candidate must pass, in order, before any cycle is reported:
bench_isa.py– ALL EXACT, and it requiresisa_dsl.gemm_programto equalmicroarch_isa.gemm_program_handwrittenword for word.stress_isa.py– 492/492 runs exact: full-range/corner/boundary int8 at all 64 shapes,Cprefilled and compared in full, vector and 200 random programs againstisa_ref.py, and the program validator’s controls.param_check.py– the design rebuilt at MAXDIM 8 and 12 and exact there.the mapspace gate (new): the seam – the canonical nest must re-emit the candidate’s own
gemm_programword for word – then the exhaustive enumeration, then every encodable nest (up to 48 per shape, best first, always including the one that gets cosimmed) proved againstisa_ref.runat full-range int8 withCprefilled and the whole ofCcompared.the cosim – bit-exact at every shape reported.
Point 4 is what makes “we widened the encoder” checkable. Deleting a legality check to make 1,150 nests encodable is the cheap way to a big number, and the gate rejects it immediately: the nests compute the wrong thing.
Each check runs under gate_runner.py, which reads a per-run nonce on stdin
before the candidate is imported, takes the check’s return value rather than
its printed output, freezes every loaded numpy*/allo* module against
rebinding, and only then prints the line the evaluator requires.
The control¶
control.RECORDED is keyed on the git blobs of the editable design files
— fourteen paths since the unit library landed, not the two it was when this
was written — so prose-only edits invalidate it. (accept.py has no
baseline table at all and no cycle literal: it measures its control per run.)
The co-design loop therefore measures its control in the same run:
loop.py iteration 0, and test_codesign.py case k1.
Measured 2026-09-22, unmodified design, same environment and tool state:
shape |
co-design |
published |
why |
|---|---|---|---|
4x4x4 |
169 |
172 |
N/T = K/T = 1, so the enumerator offers a nest with no emitted loops
while the hand-written program keeps a trip-count-1 |
16x16x16 |
686 |
686 |
the mapper’s pick is the canonical nest, so the program under the RTL
is |
The published column is main @ 476a70d8: 172 / 262 / 418 / 484 / 686. It
reproduces exactly wherever the program is the same one, which is the check that
matters – the 4x4x4 difference is the mapper finding a better program, not the
tools moving.
Resources at that build, csynth: BRAM18K 42, DSP 14, FF 17,481, LUT 26,583,
URAM 0, estimated clock 2.431 ns against the 3.33 ns target.
Running it¶
$0, no model, before anything is spent:
conda activate allo
export LLVM_BUILD_DIR=/home/sk3463/llvm-allo-6b09f739/build OMP_NUM_THREADS=8
examples/tinytpu/reproduce_codesign.sh mapspace # ~2 s
examples/tinytpu/reproduce_codesign.sh control # ~3 min
examples/tinytpu/chia_agent/test_codesign.py # ~7 min
A search:
conda activate chia_env
set -a; source chia.env; set +a # CHIA_TOTAL_CAP_USD per allocation.json
ray start --head --resources='{"opencode_creds": 2}' --include-dashboard=false
cd examples/tinytpu/chia_agent
python preflight.py --budget-usd 15 # the gate alone, $0
python swarm.py --codesign --workers 2 --iterations 3 --budget-usd 15
python accept.py --codesign --diff <run>/<worker>/best.diff \
--out <run>/accept-<worker>
ray stop
The Vitis project goes wherever TPU_PRJ says and is wiped before every
evaluation, so a stale report can never be read as a result. Disk on this host
is tight; reproduce_codesign.sh deletes each project when it is done.
The LLVM-free test suite¶
chia_agent/test_codesign.py, $0, no model. test_harness.py still
covers the design-level loop; this covers only what the co-design loop adds.
case |
what it does |
expected |
|---|---|---|
k1 |
a no-op (re-save the two files) |
reproduces the control exactly: the cycles, and the whole pinned refusal histogram and chosen nest at both shapes |
k2 |
a real hardware widening: |
the gate passes and the encodable count rises, 3 -> 7 at 16x16x16,
with |
k3 |
the encoder widened by deleting |
REJECTED at |
k4 |
the seam broken – |
REJECTED at |
k5 |
the PE partial sum narrowed from int32 to int16 |
REJECTED at |
k6 |
|
REJECTED, the refusal naming |
k7 |
a sabotaged |
the sabotage is ignored (the evaluator composes those from git) and the verdict is the control’s; each diff is refused by path |