HW Design-Space Agent

/writeup

HW Design Space Agent: final technical report (M1–M4)

The repository's own closing report, rendered unedited from docs/WRITEUP.md at 80415c6 of HW_Design_Space_Agent. Each section's numbers are bound to named facts (open the list under its heading), which the repository computes from its committed data and checks number by number.

This is the project's closing report. It is built from the data committed in this repository. Every number below is a named fact that scripts/writeup_facts.py computes from the committed data or reads from one named cell of a generated report (docs/writeup_facts.json); each section names, in a comment under its heading, the fact behind each of its numbers in reading order, and tests/test_writeup.py checks every printed number against its fact at the printed precision. Where a section reports a loss, the loss comes first.

The question

The 2 named facts behind this section's numbers, in reading order
  • grid_designs = 635,040
  • budget_evals = 400

Can a language model make hardware design-space exploration better, if it is never allowed to produce a number? The running problem is a CORDIC sin/cos unit with four architecture families (iterative, unrolled_k, pipelined, pipelined_m) and numeric knobs (data width, iterations, angle-path width, guard bits, rounding, and the unroll or pipelining factor): 635,040 designs, small enough to enumerate, so every method can be scored against the exact answer. A spec gives constraints (throughput, accuracy, from M3 system timing), one to three objectives, a selection rule and a budget of 400 evaluations.

The rule held from the first commit to the last: the LLM is never the optimiser and never produces a PPA, accuracy or system-level number. It plays the architect: it reads the spec, picks families and ranges from a fixed registry, reads code-written summaries, and takes one of a few typed decisions (refine, widen, add a family, map the front, declare infeasible, stop). Optuna's NSGA-II searches inside its boxes; a bit-accurate golden model gives exact accuracy; cost models give estimates; synthesis gives measurements; simulation gives system metrics. Every number carries its provenance: exact, estimate, measured (tool, version) or simulated (model, version). The structured-output schemas have no field where a number about a design could go.

Two scores recur. HV fraction: the hypervolume of the feasible designs a run found over the true hypervolume of the exhaustive grid. Selection regret: how much worse the design the run finally selects is than the true optimum on the spec's selection metric. Baselines are NSGA-II and random search over the whole space with the same budget and the same evaluation function.

The ladder

The 12 named facts behind this section's numbers, in reading order
  • golden_exhaustive_angles = 65,536
  • l3_rtl_configurations = 47
  • l3_rtl_runs = 94
  • l3_rtl_mismatches = 0
  • l3_gate_runs = 22
  • l3_gate_mismatches = 0
  • formal_pass = 7
  • formal_jobs = 7
  • l4_points = 39
  • vivado_designs = 14
  • asic_calibration_points = 57
  • asic_validation_points = 9
  1. L0 spec intake
    YAML or natural language → validated Spec, human confirms
  2. L1 analytical exploration
    golden model (exact) + cost model (estimate)
    NSGA-II in LLM-chosen boxes
  3. L2 system simulation (M3)
    cycle model checked against RTL; SimPy systems
  4. L3 RTL (M2)
    generated SystemVerilog, two simulators, formal
  5. L4 synthesis (M2, M4)
    Yosys + nextpnr-xilinx; Vivado (local); Yosys + OpenSTA on sky130
  6. L5 back-annotation
    measured vs estimate, refit, flag winner changes, re-explore
  • ↺ L2 → L1: M4: L2 → L1 feedback, corrected system bound
  • ↺ L5 → L1: M3: re-explore under the refit
Drawn from the write-up's Mermaid source.

Each rung is code; the LLM sits only in the L1 loop. The ladder was built bottom-up in evidence: the golden model is bit-exact against the reference RTL on all 65,536 input angles of the reference configuration; the generated RTL matched the golden model in 47 configurations across two simulators (94 runs, 0 mismatches) and at gate level (22 runs, 0 mismatches), with 7 of 7 formal proofs; L4 has 39 open-source FPGA points, 14 Vivado designs and, in M4, 57 ASIC calibration points plus 9 held-out ones.

Milestone 1: the architect is a good selector and a bad mapper

The 13 named facts behind this section's numbers, in reading order
  • m1.hv_rel.dds_250msps.sonnet = 0.32
  • m1.hv_rel.dds_250msps.deepseek = 0.87
  • m1.hv_rel.dds_250msps.qwen = 0.46
  • m1.hv_rel.low_area_control.sonnet = 0.23
  • m1.hv_rel.low_area_control.deepseek = 0.22
  • m1.hv_rel.low_area_control.qwen = 0.18
  • m1.hv_gain_rule_pct = 1
  • m1.regret.low_area_control.qwen = 12.2
  • m1.regret.low_area_control.nsga2 = 8
  • infeasible.rate_msps = 400
  • m1.infeasible_evals_min = 100
  • m1.infeasible_evals_max = 200
  • budget_evals = 400

Loss first. The M1 agent mapped the Pareto front far worse than plain NSGA-II on two of three feasible specs: its HV fraction relative to NSGA-II was 0.32, 0.87 and 0.46 on dds_250msps and 0.23, 0.22 and 0.18 on low_area_control for Sonnet 5.5, DeepSeek V4.1 Flash and Qwen3.8-27B (reasoning off). It dived at the corner the selection rule picks and the 1% hypervolume-gain rule then stopped it with a narrow front.

What worked. It selected better. Mean selection regret was below NSGA-II's on every model and spec except one (Qwen on low_area_control, +12.2% against NSGA-II's +8.0%), and every model declared the infeasible 400 MSPS spec infeasible itself in every run, using 100–200 evaluations where the baselines spend all 400 and simply find nothing. The cost model behind all of this was weak by construction: five constants solved from two Vivado anchors.

Milestone 2: climbing the ladder, and fixing the mapper in code

The 28 named facts behind this section's numbers, in reading order
  • m2.coverage_reserve_pct = 40
  • m2.hv_rel.dds_250msps.sonnet = 1.02
  • m2.hv_rel.dds_250msps.deepseek = 1.08
  • m2.hv_rel.dds_250msps.qwen = 1.09
  • m2.hv_rel.low_area_control.sonnet = 0.97
  • m2.hv_rel.low_area_control.deepseek = 1.02
  • m2.hv_rel.low_area_control.qwen = 0.99
  • m2.hv_rel.high_precision.sonnet = 1.04
  • m2.hv_rel.high_precision.deepseek = 1.05
  • m2.hv_rel.high_precision.qwen = 0.96
  • m2.regret.dds.deepseek.m1 = 2.2
  • m2.regret.dds.deepseek.m2 = 7.6
  • m2.regret.low_area.deepseek.m1 = 1.6
  • m2.regret.low_area.deepseek.m2 = 5.1
  • m2.regret.high_precision.qwen = 17.5
  • m2.regret.high_precision.qwen_std = 27.2
  • yosys_fpga_version = 0.68
  • vivado_version = 2025.2
  • vivado_designs = 14
  • m2.viv_pipelined_fmax_low_min = 15
  • m2.viv_pipelined_fmax_low_max = 17
  • m2.viv_iterative_fmax_high_min = 14
  • m2.viv_iterative_fmax_high_max = 16
  • m2.viv_unrolled_luts_low_min = 37
  • m2.viv_unrolled_luts_low_max = 40
  • m2.viv_unrolled_k_lo = 3
  • m2.viv_unrolled_k_hi = 4
  • m2.refit_points = 51

The levers. M2 added deterministic whole-curve levers: a 40% reserve the LLM's own rounds may not spend once something is feasible, a front-mapping round that code runs with whatever is left, and a map_front decision the LLM may choose. They were tuned offline by replaying the recorded M1 decisions, without spending anything. Live, the front-mapping gap closed: agent HV relative to NSGA-II went to 1.02, 1.08 and 1.09 on dds_250msps, 0.97, 1.02 and 0.99 on low_area_control and 1.04, 1.05 and 0.96 on high_precision.

Losses. The selection lead mostly survived but not everywhere: DeepSeek's regret rose on dds_250msps (+2.2% to +7.6%) and low_area_control (+1.6% to +5.1%), and Qwen on high_precision reached +17.5% ± 27.2 on the strength of one seed whose first box was too narrow to reach the accuracy target; the front-mapping round then never revisited that family.

The measurements. The open-source flow (Yosys 0.68 + nextpnr-xilinx on the anchors' part) and, later, Vivado 2025.2 on 14 generated designs showed what the two-anchor model gets wrong. Its known biases against Vivado are pipelined Fmax low by 15–17%, iterative Fmax high by 14–16%, and unrolled_k LUTs low by 37–40% at k = 3–4. A refit on 51 points was no better on unseen Vivado points than the untouched M1 model, so the default never changed, and no spec's ground-truth winner moved under the refit.

Milestone 3: the design in a system, and an LLM outer loop that gained nothing

The 17 named facts behind this section's numbers, in reading order
  • m3.l2_traces_identical = 48
  • m3.l2_traces = 48
  • m3.l2_clock_edges = 28,424
  • m3.multiaxis.l1_bound_feasible = 53,667
  • m3.multiaxis.sim_feasible = 51,244
  • m3.l2_reselect_structured = 0
  • m3.structured_runs = 30
  • m3.l2_reselect_campaign = 4
  • m3.campaigns = 65
  • m3.campaigns_single_run_dse_400 = 52
  • m3.campaigns = 65
  • budget_evals = 400
  • m3.campaign_cost_ratio.qwen = 1.44
  • m3.campaign_cost_ratio.deepseek = 1.62
  • m3.campaign_cost_ratio.sonnet = 2.25
  • m3.qwen_campaign_failed = 4
  • m3.qwen_campaigns = 30

L2. A cycle-accurate model of each family's interface contract matched the generated RTL on 48/48 bursty traces (28,424 clock edges). SimPy models put the unit in a DDS, a control loop and a bursty stream. On multiaxis_control only the simulation finds the winner: 53,667 designs pass the L1 bound, 51,244 the simulation, and the bound's winner misses the batch deadline. On bursty_offload a peak-rate floor would have done as well, and the DDS spec reduced to the throughput-only view. In the live runs L2 changed the L1 selection in 0 of 30 structured runs and in 4 of 65 campaign runs, all on multiaxis_control.

The campaign agent (loss). An outer agent on LangChain Deep Agents could call the structured graph, single NSGA-II studies, the L2–L5 rungs and a memory. In 52 of 65 campaigns its plan came down to one 400-evaluation call to the structured graph, and it cost 1.44 (Qwen), 1.62 (DeepSeek) and 2.25 (Sonnet) times as much per spec. Qwen failed 4 of 30 campaigns by looping on the ladder tools. Its free-text notes to the inner architect narrowed the first box in the worst run. Memory on vs off showed no benefit at three seeds. The campaign layer stayed experimental and off by default.

Milestone 4: a second target, a fleet, and feedback from L2 to L1

An ASIC target on sky130

The 28 named facts behind this section's numbers, in reading order
  • asic.yosys_version = 0.33
  • asic.opensta_version = 3.1.0
  • asic.corner_temp_c = 25
  • asic.corner_v = 1.8
  • asic.clock_ns = 10
  • asic_calibration_points = 57
  • asic.loo_rms.area = 8.7
  • asic.loo_rms.ffs = 4.6
  • asic.loo_rms.fmax = 10.6
  • asic.loo_rms.power = 5.8
  • asic.loo_area.unrolled_k = 12.8
  • asic.loo_fmax.pipelined = 12.9
  • asic.loo_fmax.unrolled_k = 12.5
  • asic.loo_one_adder_area = 14.8
  • asic.t_bit_ns = 0.29
  • asic.power.rate_msps = 50
  • asic.power.winner_m = 3
  • asic.power.twin_m = 7
  • asic.multiaxis.deadline_us = 0.5
  • asic.multiaxis.winner_m = 2
  • asic.multiaxis.twin_m = 5
  • asic.low_area.pipelined_ratio = 4.17
  • asic.low_area.pipelined_ratio_fpga = 4.99
  • asic_area_ratio_pipelined_over_iterative_w16 = 4.77
  • anchor_w = 16
  • anchor_n = 14
  • asic.multiaxis.l1_bound_feasible = 8,533
  • asic.multiaxis.sim_feasible = 7,898

The flow. The same generated RTL goes through Yosys 0.33 and OpenSTA 3.1.0 against the sky130_fd_sc_hd library at its typical corner (25 °C, 1.80 V), with a 10 ns clock: cell area, flip-flops, the register-to-register path with the library's wire-load model, and vectorless power. There is no placement: these are post-synthesis numbers, the ASIC counterpart of the Vivado anchors, not of nextpnr's routed timing. OpenROAD was not installed: it has no apt package for this Ubuntu, its conda build installs only through a commercial channel, there is no Docker daemon for the container, and it would need a floorplan and power grid per design, so place and route stayed out of scope.

The model. The FPGA model's structural counts, re-weighted for standard cells and fitted to 57 synthesised designs. Leave-one-out RMS error: area 8.7%, flip-flops 4.6%, Fmax 10.6%, power against OpenSTA 5.8%. Per family, the worst area error is unrolled_k's (12.8%) and the worst Fmax error pipelined's (12.9%, with unrolled_k at 12.5%). One adder constant for every family fitted badly (14.8% on area); the FSM families needed their own. The fitted adder delay is linear in width, about 0.29 ns per bit: Yosys's default adder mapping ripples, which makes chained rotations expensive on this target.

Does the family ranking differ from the FPGA? No, on thin evidence. Three ASIC specs, each with an FPGA twin (the same constraints with area in LUTs + FFs, on the FPGA ground truth), rank the families in the same order on both targets. Only asic_low_area ranks all four families; on the other two only the pipelined families meet the rate, so each is a single pairwise comparison (pipelined_m ahead of pipelined). What moves is the best configuration inside a family. At 50 MSPS the ASIC winner is pipelined_m with m=3 where the FPGA twin's is m=7; under the 0.5 µs batch deadline of asic_multiaxis_control it is m=2 where the FPGA's is m=5. The slow ripple adders force shallower pipelining. The gaps shrink too: pipelined costs 4.17× the iterative winner's area on asic_low_area and 4.99× its LUT+FF on the FPGA twin. Measured directly, pipelined is 4.77× iterative in cell area at the anchors' W=16, N=14.

L2 carried over unchanged (it reads only the contract and Fmax) and on asic_multiaxis_control it changes the winner: 8,533 designs pass the bound, 7,898 the simulation.

The held-out check: where the ASIC model is wrong

The 13 named facts behind this section's numbers, in reading order
  • asic.ho.area_err_bound_pct = 5
  • asic.ho.fmax_err_min = -14.9
  • asic.ho.fmax_err_max = 39.6
  • asic.power.twin_m = 7
  • asic.ho.twin_power_fmax = 26.7
  • asic.power.rate_msps = 50
  • asic.ho.multiaxis_winner_fmax = 72.2
  • asic.ho.multiaxis_winner_fmax_est = 82.5
  • asic.ho.multiaxis_winner_p99_us = 0.567
  • asic.multiaxis.deadline_us = 0.5
  • asic.ho.pipelined_w = 17
  • asic.ho.pipelined_n = 14
  • asic.ho.pipelined_fmax = 119.1

Loss first. Nine ground-truth designs that the fit never saw (each spec's per-family optima, the FPGA twins' optima, the L1-bound pick) were synthesised. Area is within 5% on all nine; Fmax is not, from −14.9% to +39.6%, the worst an unrolled_k design. Two consequences. The FPGA twin's m=7 winner for asic_power_50msps runs at 26.7 MHz on sky130 and cannot make 50 MSPS. And the model's winner for asic_multiaxis_control measures 72.2 MHz against an estimate of 82.5, and at that clock its simulated p99 batch latency is 0.567 µs against the 0.5 µs deadline; of the nine, only plain pipelined W=17, N=14 (119.1 MHz) meets that spec with measured numbers. The ASIC back-annotation node flags exactly this case, but it does not re-explore: there is no ASIC L5 loop yet, and the nine held-out designs were kept out of the fit so that they remain a test.

ASIC specs: the structured graph against the baselines

The 29 named facts behind this section's numbers, in reading order
  • asic.meets_per_model_min = 15
  • asic.runs_per_model = 15
  • asic.regret.asic_low_area.nsga2 = 6.1
  • asic.regret.asic_low_area.sonnet = 0.8
  • asic.regret.asic_low_area.deepseek = 4.7
  • asic.regret.asic_low_area.qwen = 10.6
  • asic.regret.asic_power_50msps.nsga2 = 54
  • asic.regret.asic_power_50msps.sonnet = 3.5
  • asic.regret.asic_power_50msps.deepseek = 15.6
  • asic.regret.asic_power_50msps.qwen = 10.1
  • asic.regret.asic_multiaxis_control.nsga2 = 23.4
  • asic.regret.asic_multiaxis_control.sonnet = 17.9
  • asic.regret.asic_multiaxis_control.deepseek = 25.1
  • asic.regret.asic_multiaxis_control.qwen = 21.6
  • asic.regret.asic_power_50msps.nsga2 = 54
  • asic.regret_std.asic_power_50msps.nsga2 = 36.1
  • asic.hv.asic_power_50msps.qwen = 0.754
  • asic.hv.asic_power_50msps.deepseek = 0.794
  • asic.hv.asic_power_50msps.nsga2 = 0.812
  • asic.hv.asic_low_area.sonnet = 0.925
  • asic.hv.asic_low_area.deepseek = 0.923
  • asic.hv.asic_low_area.nsga2 = 0.922
  • asic.hv.asic_low_area.qwen = 0.859
  • asic.hv.asic_multiaxis_control.sonnet = 0.82
  • asic.hv.asic_multiaxis_control.deepseek = 0.864
  • asic.hv.asic_multiaxis_control.nsga2 = 0.837
  • asic.multiaxis.regret_min = 17.9
  • asic.multiaxis.regret_max = 25.1
  • asic.l2_changed.sonnet = 2

Five seeds of the structured graph per model on the three ASIC specs, against NSGA-II and random search with the same budget (all scored on the ASIC ground truth, the system spec on simulated feasibility). Every selected design met its spec (15/15 per model).

specNSGA-II regretSonnet 5.5DeepSeek V4.1 FlashQwen3.8-27B
asic_low_area+6.1%+0.8%+4.7%+10.6%
asic_power_50msps+54.0%+3.5%+15.6%+10.1%
asic_multiaxis_control+23.4%+17.9%+25.1%+21.6%

The M2 pattern carries to the new target where the selection rule has a corner to aim at: on the power-driven spec every model's regret is a fraction of NSGA-II's (+54.0% ± 36.1): the architects narrow onto the pipelining depth that the selection rule rewards, where NSGA-II's union search keeps sampling every family. HV is lower than NSGA-II's on that spec (0.754–0.794 against 0.812), level with it on asic_low_area for Sonnet and DeepSeek (0.925 and 0.923 against 0.922, Qwen 0.859) and between 0.820 and 0.864 against 0.837 on asic_multiaxis_control. Loss: on asic_multiaxis_control nobody gets close: NSGA-II and the three models all land 17.9–25.1% above the true winner, whose trick (two more iterations for a narrower angle path and a faster clock) sits in a corner the bound hides. L2 changed the L1 pick in 2 Sonnet runs.

The fleet

The 123 named facts behind this section's numbers, in reading order
  • ab.qwen.fleet.min_member_budget = 10
  • ab.qwen.fleet.evals_min = 396
  • budget_evals = 400
  • ab.qwen.fleet_v2.min_member_budget = 20
  • ab.seed_first = 0
  • ab.seed_last = 4
  • budget_evals = 400
  • ab.qwen.single.runs = 25
  • ab.qwen.single.hv = 0.828589
  • ab.qwen.single.hv_std = 0.0706629
  • ab.qwen.single.regret = 14.8318
  • ab.qwen.single.regret_std = 11.4004
  • ab.qwen.single.cost = 0.00901422
  • ab.qwen.single.wall = 57.116
  • ab.qwen.single.wall_std = 48.4053
  • ab.qwen.fleet.runs = 25
  • ab.qwen.fleet.hv = 0.79585
  • ab.qwen.fleet.hv_std = 0.0987525
  • ab.qwen.fleet.regret = 11.6522
  • ab.qwen.fleet.regret_std = 19.9354
  • ab.qwen.fleet.cost = 0.0127007
  • ab.qwen.fleet.wall = 171.352
  • ab.qwen.fleet.wall_std = 188.042
  • ab.qwen.fleet_v2.runs = 25
  • ab.qwen.fleet_v2.hv = 0.788488
  • ab.qwen.fleet_v2.hv_std = 0.117091
  • ab.qwen.fleet_v2.regret = 6.25812
  • ab.qwen.fleet_v2.regret_std = 6.00649
  • ab.qwen.fleet_v2.cost = 0.01309
  • ab.qwen.fleet_v2.wall = 89.472
  • ab.qwen.fleet_v2.wall_std = 38.2203
  • ab.deepseek.single.runs = 25
  • ab.deepseek.single.hv = 0.869565
  • ab.deepseek.single.hv_std = 0.0613298
  • ab.deepseek.single.regret = 12.1112
  • ab.deepseek.single.regret_std = 11.9447
  • ab.deepseek.single.cost = 0.0167103
  • ab.deepseek.single.wall = 153.024
  • ab.deepseek.single.wall_std = 147.615
  • ab.deepseek.fleet.runs = 25
  • ab.deepseek.fleet.hv = 0.833912
  • ab.deepseek.fleet.hv_std = 0.0804863
  • ab.deepseek.fleet.regret = 15.7811
  • ab.deepseek.fleet.regret_std = 21.4606
  • ab.deepseek.fleet.cost = 0.0257254
  • ab.deepseek.fleet.wall = 228.864
  • ab.deepseek.fleet.wall_std = 259.96
  • ab.sonnet.single.runs = 5
  • ab.sonnet.single.hv = 0.820351
  • ab.sonnet.single.hv_std = 0.0687739
  • ab.sonnet.single.regret = 17.9028
  • ab.sonnet.single.regret_std = 3.20335
  • ab.sonnet.single.cost = 0.0930636
  • ab.sonnet.single.wall = 41.14
  • ab.sonnet.single.wall_std = 1.51605
  • ab.sonnet.fleet.runs = 5
  • ab.sonnet.fleet.hv = 0.7941
  • ab.sonnet.fleet.hv_std = 0.0506224
  • ab.sonnet.fleet.regret = 19.851
  • ab.sonnet.fleet.regret_std = 3.56227
  • ab.sonnet.fleet.cost = 0.148791
  • ab.sonnet.fleet.wall = 49.22
  • ab.sonnet.fleet.wall_std = 1.22049
  • ab.paired.pooled.fleet.hv.fleet_higher = 18
  • ab.paired.pooled.fleet.hv.pairs = 55
  • ab.paired.pooled.fleet.hv.sign_p = 0.0144538
  • ab.paired.deepseek.fleet.hv.fleet_higher = 6
  • ab.paired.deepseek.fleet.hv.pairs = 25
  • ab.paired.deepseek.fleet.hv.sign_p = 0.0146333
  • ab.paired.deepseek.fleet.hv.t = -3.02555
  • ab.paired.deepseek.fleet.hv.t_p = 0.00583981
  • ab.paired.qwen.fleet.hv.fleet_higher = 10
  • ab.paired.qwen.fleet.hv.pairs = 25
  • ab.paired.qwen.fleet.hv.sign_p = 0.424356
  • ab.paired.sonnet.fleet.hv.fleet_higher = 2
  • ab.paired.sonnet.fleet.hv.pairs = 5
  • ab.paired.qwen.fleet_v2.hv.fleet_higher = 8
  • ab.paired.qwen.fleet_v2.hv.pairs = 25
  • ab.paired.qwen.fleet_v2.hv.sign_p = 0.107752
  • ab.paired.qwen.fix_effect.hv.fleet_higher = 12
  • ab.paired.qwen.fix_effect.hv.fleet_lower = 13
  • ab.paired.qwen.fix_effect.hv.sign_p = 1
  • ab.paired.qwen.fleet.regret.fleet_lower = 18
  • ab.paired.qwen.fleet.regret.untied = 24
  • ab.paired.qwen.fleet.regret.sign_p = 0.0226558
  • ab.paired.qwen.fleet_v2.regret.fleet_lower = 22
  • ab.paired.qwen.fleet_v2.regret.pairs = 25
  • ab.paired.qwen.fleet_v2.regret.sign_p = 0.000156522
  • ab.paired.qwen.fleet_v2.regret.mean_diff = -8.57365
  • ab.paired.qwen.fix_effect.regret.mean_diff = -5.39405
  • ab.paired.qwen.fix_effect.regret.t_p = 0.210556
  • ab.qwen.fleet.cost_ratio = 1.40897
  • ab.deepseek.fleet.cost_ratio = 1.53949
  • ab.sonnet.fleet.cost_ratio = 1.59881
  • ab.qwen.fleet.failed_calls_qd = 17
  • ab.qwen.single.failed_calls_qd = 3
  • ab.qwen.fleet_v2.failed_calls = 0
  • ab.qwen.fleet.failed_calls = 7
  • ab.qd.fleet.fsm_evals = 179.9
  • budget_evals = 400
  • ab.qwen.single_fsm_evals_vs_fleet = 127.56
  • ab.deepseek.single_fsm_evals_vs_fleet = 165.96
  • ab.sonnet.fleet.fsm_evals = 100
  • ab.sonnet.single_fsm_evals_vs_fleet = 17.6
  • ab.qwen.fleet.fsm_declared_infeasible = 16
  • ab.qwen.fleet.runs = 25
  • ab.qwen.fleet.phase2_all_pipelined = 14
  • ab.deepseek.fleet.phase2_all_pipelined = 15
  • ab.sonnet.fleet.phase2_all_pipelined = 5
  • ab.qwen.fleet_v2.phase2_all_pipelined = 15
  • ab.qwen.multiaxis.hv_single = 0.840353
  • ab.qwen.multiaxis.hv_fleet = 0.73401
  • ab.qwen.multiaxis.hv_fleet_v2 = 0.744508
  • ab.qwen.single.designs_per_hour = 25211.8
  • ab.deepseek.single.designs_per_hour = 9410.29
  • ab.sonnet.single.designs_per_hour = 35002.4
  • ab.qwen.fleet.designs_per_hour = 8400.39
  • ab.deepseek.fleet.designs_per_hour = 6291.95
  • ab.sonnet.fleet.designs_per_hour = 29256.4
  • ab.qwen.fleet_v2.designs_per_hour = 16094.4
  • proj.qwen.designs_per_day = 605084
  • proj.qwen.specs_per_day = 1512.71
  • proj.qwen.cost_per_day = 13.6359

M3's lesson was that an LLM in the outer loop adds cost and loops without adding search. M4 tried the opposite: several structured graphs in parallel with code deciding everything between them. Two members, one per contract class (FSM families and pipelined families), each an unmodified structured graph restricted to its families. Half the budget is split equally; code computes per-family facts from the evaluations the members write to a shared LangGraph Store and gives the other half to the members in proportion to their share of the merged Pareto front; code merges, selects and runs L2 once. No LLM-written text reaches the store or another member. No LLM coordinator was built: the coordinator's decisions are arithmetic over numbers code already has, and M3 showed what an LLM in that seat costs.

A fix after review. In the first A/B the Store was only written: the coordinator read it, but the phase-2 members started cold, and a member budget of 10 could not be fully spent (one run used 396 of 400 evaluations). The fleet now seeds every phase-2 study with the fleet's stored front designs of its family, read back from the Store, and a member gets at least 20 evaluations. The first fleet's results are kept as before_fix; the fixed fleet was re-run for Qwen only (the spend cap), on the same specs and seeds.

The A/B (same five specs, three ASIC and two FPGA, seeds 0–4, 400 L1 evaluations for every arm, every run started alone so its wall-clock is its own; Sonnet's fleet arm on asic_multiaxis_control only, for the spend cap):

modelarmrunsHV fractionregretcost / runwall / run
Qwen3.8-27Bsingle250.829 ± 0.071+14.8% ± 11.4$0.009057 ± 48 s
fleet, before_fix250.796 ± 0.099+11.7% ± 19.9$0.0127171 ± 188 s
fleet, after fix250.788 ± 0.117+6.3% ± 6.0$0.013189 ± 38 s
DeepSeek V4.1 Flashsingle250.870 ± 0.061+12.1% ± 11.9$0.0167153 ± 148 s
fleet, before_fix250.834 ± 0.080+15.8% ± 21.5$0.0257229 ± 260 s
Sonnet 5.5single50.820 ± 0.069+17.9% ± 3.2$0.093141 ± 2 s
fleet, before_fix50.794 ± 0.051+19.9% ± 3.6$0.148849 ± 1 s

The arms are paired (same spec and seed), so the tests are paired: the exact sign test and the paired t-test, both two-sided.

HV: the single graph maps more of the front; significant for DeepSeek and pooled, not for Qwen or Sonnet. Pooled over the three models, the first fleet had the higher HV in 18 of 55 pairs (sign test p = 0.014). DeepSeek alone: 6 of 25 (p = 0.015; paired t = −3.03, p = 0.006). Qwen alone: 10 of 25 (p = 0.42), not significant; Sonnet, 2 of 5 pairs, cannot tell. The fixed fleet did not change this: 8 of 25 against the single graph (p = 0.11), and against the first fleet 12 higher, 13 lower (p = 1.0). The cold restart did not explain the HV loss.

Regret: a gain for the fleet with Qwen. Qwen's first fleet selected the better design in 18 of 24 untied pairs (p = 0.023); the fixed fleet in 22 of 25 (p = 0.0002), with a mean paired difference of −8.6 points. Against the first fleet the fix changed regret by −5.4 points on average, not significantly (p = 0.21). DeepSeek and Sonnet show no regret difference.

Cost and time. The fleet costs 1.41 (Qwen), 1.54 (DeepSeek) and 1.60 (Sonnet) times as much per spec. Wall-clock is noisy (provider latency dominates; the arms ran at different times); the first fleet was slower for every model, the fixed one less so. No run in any arm failed to produce a spec-meeting design; the first fleet had more failed (retried) LLM calls, 17 against 3 over the Qwen and DeepSeek arms, and the fixed Qwen fleet had 0 (against 7 before the fix).

A confound. The fixed fleet ran hours after the single arm it is paired with, so provider drift (see below: large for Qwen) may affect the quality comparison as well as the wall-clock. Its regret is lower than the single graph's on every one of the five specs, which a uniform drift would not explain on its own, but the comparison is not same-session.

What the records show, and what they do not. The fleet spends more of its budget on the FSM families than a single graph does: on average 180 of 400 evaluations for the Qwen and DeepSeek fleets against 128 (Qwen) and 166 (DeepSeek) for their single graphs, and 100 against 18 for Sonnet on asic_multiaxis_control, where no FSM design can meet the deadline. Its FSM member often proves that the hard way (Qwen's declared its families infeasible 16 times in 25 runs), and phase 2 then went entirely to the pipelined member in 14 (Qwen), 15 (DeepSeek) and 5 (Sonnet) runs, and 15 for the fixed Qwen fleet. The HV loss is largest on the system specs (multiaxis_control, Qwen: 0.840 single, 0.734 before the fix, 0.745 after). One reading is that a fixed split by contract class allocates worse than the architect's own early choice of families plus one front-mapping round over all of them, and that smaller studies per member (fewer NSGA-II generations each) cost front coverage; the regret gain would fit a pipelined member that searches only the families a selection rule usually picks from. These are readings of the records, not tested causes.

Throughput, measured (designs evaluated per hour of wall-clock, one run at a time): single 25,212 (Qwen), 9,410 (DeepSeek), 35,002 (Sonnet, one spec); first fleet 8,400, 6,292 and 29,256; fixed Qwen fleet 16,094. Projected, not run: one such machine running Qwen single-graph runs back to back would evaluate 605,084 designs and close 1513 specs a day for $13.64, if provider latency, rate limits and the spec mix stayed as measured.

L2 → L1 feedback

The 26 named facts behind this section's numbers, in reading order
  • l2f.rounds = 3
  • l2f.evals_per_round = 60
  • l2f.recorded_fired = 0
  • l2f.recorded_searches = 92
  • l2f.live_m4_fired = 0
  • l2f.live_m4_runs = 60
  • l2f.burst = 32
  • l2f.rate_msps = 130
  • l2f.throughput_floor = 130
  • l2f.p99_us = 0.4
  • l2f.l1_bound_feasible = 37,882
  • l2f.sim_feasible = 447
  • l2f.constructed_searches = 15
  • budget_evals = 400
  • l2f.searches_per_method = 5
  • l2f.constructed_fired = 3
  • l2f.recovered_corrected = 3
  • l2f.constructed_fired = 3
  • l2f.regret_mean = 17.2596
  • l2f.regret_std = 10.9791
  • l2f.recovered_uncorrected = 1
  • l2f.constructed_fired = 3
  • l2f_v1.throughput_floor = 1
  • l2f.rate_msps = 130
  • l2f_v1.constructed_fired = 6
  • l2f_v1.constructed_searches = 15

When L2 rejects every L1-feasible design of a run, the run spent its budget where the bound was wrong. The feedback measures the bound's error on every simulated design, fits a monotone correction in the device's utilisation per contract class (still optimistic at every design L2 has seen), and re-explores with a separate budget of 3 × 60 evaluations under the corrected bound.

It never fires on the committed specs: 0 of 92 recorded searches (M3 structured runs rebuilt from their archived evaluations, M3 campaign pools, NSGA-II and random on every system spec) and 0 of 60 live M4 runs found an L1-feasible set that L2 rejected whole. A spec was constructed where it does: bursts of 32 requests at 130 MS/s, a sustained throughput floor of 130 MS/s and p99 latency ≤ 0.4 µs. 37,882 designs pass the bound and 447 pass the simulation, all plain pipelined; the cheaper pipelined_m designs the area objective prefers pass the bound and queue in simulation. Of 15 searches of 400 evaluations (5 each of NSGA-II, random and the rule-based architect) it fired in 3, all random search: NSGA-II and the architect found one of the few passing designs on their own. The correction recovered a passing design in 3 of 3 (regret +17.3% ± 11.0 against the true winner); the ablation, the same re-exploration budget screened with the uncorrected bound, recovered in 1 of 3. That is evidence for the correction over the extra evaluations, on three cases.

(The first version of this spec had a throughput floor of 1 MS/s; against a 130 MS/s stream that was a strawman, and with it the feedback fired in 6 of 15 searches.)

The A/Bs, together

The 21 named facts behind this section's numbers, in reading order
  • budget_evals = 400
  • m1.regret_cells_better = 8
  • m1.regret_cells = 9
  • m2.hv_rel_min = 0.96
  • m2.hv_rel_max = 1.09
  • m3.campaign_cost_ratio.qwen = 1.44
  • m3.campaign_cost_ratio.sonnet = 2.25
  • m3.qwen_campaign_failed = 4
  • m3.qwen_campaigns = 30
  • m3.memory_seeds = 3
  • ab.paired.pooled.fleet.hv.sign_p = 0.0144538
  • ab.paired.deepseek.fleet.hv.sign_p = 0.0146333
  • ab.paired.qwen.fleet_v2.regret.sign_p = 0.000156522
  • ab.qwen.fleet.cost_ratio = 1.40897
  • ab.sonnet.fleet.cost_ratio = 1.59881
  • l2f.recovered_corrected = 3
  • l2f.constructed_fired = 3
  • l2f.recovered_uncorrected = 1
  • l2f.constructed_fired = 3
  • drift.qwen.low_area.m2_regret = 3.7
  • drift.qwen.low_area.m4_regret = 22.7554
A/Bmilestoneresult
LLM architect vs NSGA-II and random, 400 evaluationsM1better selection (lower regret on 8 of 9 model × spec cells), much worse front mapping
the same, with code levers for the frontM2front mapping at parity (0.96–1.09 of NSGA-II's HV); the selection lead mostly kept
structured graph vs an LLM campaign agent on topM3no gain; 1.44–2.25× the cost; Qwen failed 4 of 30
memory on vs off (campaign agent)M3no benefit visible at 3 seeds
structured graph vs a code-orchestrated fleet, equal budget, pairedM4lower HV for the fleet (pooled p = 0.014; DeepSeek p = 0.015; Qwen and Sonnet not significant); lower regret for the fixed Qwen fleet (p = 0.0002); the fleet costs 1.41–1.60×
L2 → L1 feedback, corrected vs uncorrected bound, equal extra budgetM4recovers 3 of 3 vs 1 of 3 on the constructed spec; never needed on the committed specs

Across four milestones the gains in front mapping came from code: the levers, the L2 shortlist, the correction of the bound. The LLM's contribution that held up in every A/B is the early, typed judgement of which families can meet a spec and where its selection corner is. Giving the LLM a bigger role (the campaign agent) lost. Replacing that judgement with a fixed split by contract class (the fleet) lost front coverage; with Qwen it selected better, a result that deserves a second model before anyone builds on it.

Provider drift is real and large. Qwen on low_area_control, through a graph that replays its M2 runs identically, had +3.7% mean regret in M2 and +22.8% in this session's single arm; comparisons are therefore made only between arms run in the same session.

Limitations

The 9 named facts behind this section's numbers, in reading order
  • fpga.fmax_err_vs_vivado_max = 17
  • asic.loo_rms.fmax = 10.6
  • asic.ho.fmax_err_max = 39.6
  • asic.clock_ns = 10
  • vivado_designs = 14
  • fpga_power.spearman_synth = 0.982418
  • fpga_power.rms_synth_pct = 20.61
  • fpga_power.activity_loo_pct = 13.9313
  • infeasible.rate_msps = 400
  • Every ground truth is a model's. The FPGA one is the two-anchor M1 model, kept fixed so that four milestones are scored on the same answer; the ASIC one is the fitted sky130 model. The measurements say how far each is off (FPGA Fmax by up to 17% against Vivado; ASIC Fmax 10.6% leave-one-out and up to +39.6% on a held-out design), and specs whose winner sits closer to a constraint than that are only as settled as a measurement makes them.
  • Post-synthesis timing on the ASIC, one corner, a wire-load model, no buffering or sizing, no clock tree, no place and route; ripple adders from Yosys's default mapping. ABC maps to a relaxed 10 ns delay target (abc -D 10000) with area recovery, so the Fmax figures are those of an area-oriented netlist, not the fastest one the library allows. OpenSTA reports no clock power (there is no clock tree to power), so the power numbers miss the clock network. A commercial flow would change the absolute numbers and could change the within-family optimum again.
  • Power is a toggle-rate model everywhere. On the FPGA the index is now checked against Vivado's report_power on 14 designs, itself vectorless: it ranks them well (Spearman 0.982) but is off by 20.6% RMS post-synthesis, and a per-family activity factor (13.9% leave-one-out) is kept as a separate, non-default calibration. On the ASIC the index reproduces OpenSTA's vectorless power. Both assume uniform activity; neither has seen real switching.
  • Small samples. Five seeds per model and spec, two or three models, a handful of specs. Day-to-day provider drift is visible in the data (above), so arms are compared only within one session.
  • Single-lane designs only; the 400 MSPS spec is infeasible by construction of the registry.
  • The L2 → L1 feedback never fired on the committed specs; its evidence is one constructed spec and offline searches.

What I would do next

The 13 named facts behind this section's numbers, in reading order
  • hp.need_msps = 50
  • hp.vivado_constraint_ns = 20
  • hp.m = 8
  • hp.m8_20ns_synth_msps = 49.88
  • hp.m8_20ns_route_msps = 48.31
  • class_factor.m8_msps = 52.514
  • class_factor.vivado_fmax_loo_without = 12.5
  • class_factor.vivado_fmax_loo_with = 7.4
  • hp.m_control = 6
  • hp.m6_20ns_route_loss_pct = 17.4797
  • hp.m6_20ns_synth_msps = 65.39
  • hp.m6_20ns_route_msps = 53.96
  • infeasible.rate_msps = 400
  1. A routed timing scale and real switching activity on the FPGA. The local Vivado runs are done: high_precision misses 50 MSPS at its own 20 ns clock with m=8 (49.88 MSPS post-synthesis, 48.31 post-route), so the per-family-class path factor, which predicted it feasible (52.5), was rejected by its pre-set rule even though it cut Vivado Fmax leave-one-out from 12.5% to 7.4%. The winner m=6 loses 17% to routing (65.39 → 53.96 MSPS), which no model here sees, and the measured power is vectorless. Next: a post-route timing calibration of its own, and power from simulated switching activity.
  2. Place and route on sky130 (OpenROAD, from its prebuilt release packages pinned by URL and SHA-256, or a container image pinned by digest) for the held-out designs, to see whether the ASIC model's Fmax error is the wire-load model or the structure.
  3. An ASIC measure-and-re-explore loop: synthesise the selected designs, re-select under the measured numbers, as the FPGA path's L5 loop does. Then decide whether to refit with an unrolled_k chain term, disclosed, keeping the held-out points held out.
  4. Multi-lane and polyphase families, so that the 400 MSPS spec has an answer.
  5. Treat provider drift as a first-class measurement: pin model snapshots where the provider allows it, and re-run a fixed reference set each session before comparing arms.

The same results with their tables and animations are on the results and how it works pages; how the project was run is in the project record.