feat: migrate to bufferized voyager IR - #4
Merged
Merged
Conversation
- retire the param.proto/interstellar tiling flow; codegen emits voyager_ir and the toolchain maps fused prim-op chains directly - add the runtime that walks it under test/common: Model, Interpreter, Backend, Checker, GraphUtils, Tensor - overlap consecutive tiles in the SystemC harness: the walk pushes an operation's params and moves on, so a unit's controllers fetch the next tile while the current one computes, and two threads pace the start and done handshakes in program order - resolve a windowed TensorBoxRef -- a voyager.subview folded into an operand -- as a base address plus a contiguous run, carrying the row pitch when the innermost dimension is partial - fetch a pitched window by walking the underlying rows and slicing each to the window's run; reject one as a side operand, which has no slice - take the MAX_TILES bound from the selection rather than re-deriving it from TESTS: the selection stops the walk at the loop it bounds, and a layer's display name need not be the name of any op - key semaphores by a slot -- a bank for a scalar one, a flattened [bank, *dims] for an array -- so a per-chunk semaphore array resolves - derive an SpMM's output width from the contracted operand's trailing dimension, which is the output width for a matmul's batched `other` as well as a linear's two-dimensional weight - fold SpMM partial sums the way the unit does: its accumulate reads the sum FEEDBACK_DELAY iterations back, so a row's nonzeros land in that many partials, which an adder tree folds in the vector type - map a microscaled quantize's scale pass by the layout of the buffer it reads, so the block axis keeps its role through a relayout - apply a side-operand dequantize through the stage mac instead of the pipeline-input dequantize flag, which scaled the wrong operand - match the hardware in the gold model: the vector-unit reducer in the gemv path, the accumulator interleave in average pooling, and the reciprocal immediate a scalar quantize is lowered to - leave a fusion's intermediate in the vector type until the single narrowing at the end rather than converting it by the dtype the IR declares - grade comparisons with torch.isclose semantics -- exact for gold-vs-accelerator, RTOL/ATOL against the reference -- and skip a CSR's values and indices past the row pointers, which are allocated for the worst-case nonzero count - pass --bank_width and --scratchpad_size to codegen so the memory planner aligns allocations to the store beat and sizes the whole physical L2, retiring CACHE_SIZE - migrate the Catapult SCVerify testbench file list to the new runtime - kill a timed-out test's process group in run_regression.py so no orphan outlives the runner
The compiler now stamps box.shape in the window's coordinate system and names the operand's own shape in output_shape, so resolution reads the window dims straight off the box and matches rank exactly. Gate the bank dims on banks_of(box) so an unbanked multi-dimensional slice takes resolve_window rather than select_bank, read is_fc_layer off output_shape, and drop declared_boxes with the set_declared_boxes call that built it.
The definition compiles for CFloat -- where the harness itself cannot -- and aborts if it is actually called, so the declaration has to be visible there too.
bits_rep() is non-const on every datatype but StdFloat, so a const immediate does not compile.
Merge voyager-soc-wrapper (origin/main, the tapeout state) into this repo as soc/ and migrate the SoC flow to the bufferized-IR compiler: - test/soc/: GenerateSoCBinaries walks model.txt into per-layer RISC-V C firmware (IR loop structure, probe-located runtime patching of param fields, DMA stripped); ScheduleRecorder captures the program's data movement and semaphore protocol for the testbench to replay. - soc/ csrc/vsrc: SoCSimulation replays the recorded schedule against the DUT, paced by per-unit done events (async commits' retire posts ride the in-flight dispatch as tokens; synchronous dispatches are drain barriers, matching Harness.cc); SoCMemory reaches the scratchpad SRAM macros through the VPI backdoor and mirrors writes for stomp checking. - test/common: optional Backend data-op interception so a recording backend sees the copies/zeros the Interpreter otherwise executes in place. - regression_common.py: helpers shared by run_regression.py and test/soc/run_voyager.py (result table, layers.txt parsing, skip rules, log-scrape patterns); the SoC report now compares ideal vs actual in cycles (resnet18 E4M3x16x16: 21/21 layers, matrix utilization 0.987). - Reserve the base of the scratchpad for a program sharing it: codegen.mk plumbs SCRATCHPAD_OFFSET into the --scratchpad_offset compiler flag, and voyager-compiler moves to 9db9440, which plans above the reservation. SoCMemory reads the scratchpad geometry as SCRATCHPAD_SIZE / NUM_BANKS, retiring SOC_CACHE_SIZE / SOC_NUM_BANKS so the compiler and the testbench name one memory instead of two views of it. - Let a scratchpad-resident config set what it needs: VOYAGER_INT_ID is overridable (PLIC ids shift down when a config has no UART) and LINKER_SCRIPT comes from the environment (../htif.ld links at the scratchpad base rather than the toolchain DRAM base).
voyager-compiler renamed its LLM test_codegen targets (llm_prefill, llm_decode, llm_kivi -> llama_prefill, llama_decode, llama_decode_kivi) and replaced --enable_mixed_precision / --outlier_pct with named --qconfig tables. codegen.mk follows: mxnf4_attn_head_int6 for the _mp programs, mxnf4_outlier for _spmm. The Llama stages are now exported whole from Hugging Face (embeddings, one decoder layer, final norm and lm_head in one graph), so the decode programs carry the embedding and rotary setup ahead of the decoder. ci_skip_rules.json is reduced to one rule that skips exactly those ops (everything before input_layernorm in layers.txt) for llama_decode, llama_decode_mp and llama_decode_kivi; the old per-op rules matched layers the new programs no longer emit. Bump voyager-compiler to 759ae99.
Picks up 4c6579f, which gives a K-split GEMM with a lone-dequantize tail the in-place reduction kernel. Without it, codegen for a per-tensor INT8 GEMM feeding an unquantized consumer -- ViT's patch embed -- crashed before emitting model.txt.
read_tensor handed back a raw owning T* inside a std::any, and a std::any frees nothing it holds, so every buffer needed an explicit delete[]. Ownership was decided per kernel and had drifted: MatrixOps, SpMM, QuantizeOps and others freed their operands while LayerNorm, Microscaling, Reduction and SlicingOp did not, nothing at all freed the results run_host_operation writes back, Checker leaked both sides of every comparison, and MatrixOps' accumulations buffer was never freed. AccuracyTester runs a whole network per sample in one process, so this accumulated: ~609 MB per sample, 77.9 GB by sample 128, and a full 1000-sample ImageNet run needed ~609 GB against 377 GB of RAM. The layer tests never showed it because they run one op per process and exit. Buffers now travel as std::shared_ptr<T[]>. Refcounting handles the aliasing that made per-site ownership impossible: run_operation stores each prim's result back into kwargs and then reads the last one out again as the operation's output, and whether those are the same pointer depends on whether cast_output had a conversion to do. It also retires the dangling kwargs entries cast_input used to leave behind, since the map keeps its own reference when a local is re-typed. Kernel-local scratch that genuinely has one owner uses unique_ptr. Peak RSS is flat afterwards: resnet18/MXINT8 holds 0.55 GB across a full 1000-sample accuracy run, scoring 70.30% against a 70.3 gold, and vit/CFLOAT holds 5.61 GB.
The testbench-driven SoC modes (the plain run, --jtag-mode program and vpi) no longer replay a recorded schedule. The emitted firmware executes the layer's bufferized program in full -- loops, scalars, dispatches, the CSR bookkeeping, and the program's semaphores as counters it owns -- and asks the testbench for the DRAM<->scratchpad transfers it cannot perform through a mailbox in its own memory (firmware/common/host_request.h): - copies, zero fills and host tensor ops are requested by ordinal in a table both sides build from the same selection (test/soc/HostRequests.cc), with the run-time scalar values in the request, so run_async_copy, zero_buffer and run_host_operation stay the single implementation; - a commit's retire post is a request the testbench completes once its settled done counts reach the counts the firmware had issued; - completions increment counter cells the firmware spins on; the doorbell is hardware semaphore 7, watched by the Verilog collateral. Every mode now runs one unit configuration: each unit waits on the semaphore of its own index for a credit the firmware grants per pass. A compute pass's credit is granted just before its last params word, after the flag the grant raised has been cleared, so the flag's next rise is the unit taking the credit, which is the proof the pass has started; the vector unit's credit is posted once its group's compute passes have all started. Counting completions through those flags, as full JTAG mode did, was lossy (two dones of one unit between two polls count as one) and hung matmul_mx_default_1_fused at four tiles. DISABLE_SEMAPHORE_WAIT becomes NO_TESTBENCH, set for full JTAG mode only, under which requests and waits compile to nothing. SoCSimulation is rewritten around the mailbox; the staging handshake, the start grants and the INT_STATUS pokes are gone. ScheduleRecorder remains for JtagPreload and moves to test/soc/jtag/. run_voyager.py exports HOST_MAILBOX, the mailbox address read from each layer's ELF. Requests are assembled in place: the core has no data cache, so a request staged on the stack cost 19.9 us; written straight into the slot it costs 1.95 us, and q_proj keeps its two-tile pipelining (35188 cycles, as under the replay). quantize_mx_outlier_default_1, whose CSR running base the replay raced, passes. Also included, uncommitted since c79a65d: full JTAG mode (JtagPreload, JtagSimChecker: the two-tile image and the readback list), the interpreter's deferred scalar reads, the vector-loop cap applied to the pack count in VectorOps.h, and the voyager-compiler pointer at 4439baa.
- The SoC gates vector_unit_start_rdy on its vector inflight count being
zero (Voyager.scala), so consecutive vector passes never overlap there.
The harness released the next pass as soon as the unit offered its start
handshake, which the vector unit raises before it hands the params to
its fetcher, so the second op of a commit body (a k-split reduction's
epilogue, a multi-pass op's next pass) fetched the scratch tile the
previous pass was still writing: the first 27 port words of every such
tile came back stale. release_starts now applies the SoC's gate and
retire_dones releases it; INT8 16x16 conv1_fused and
output_bottleneck_dense_fused and MXINT8 16x16 attention_self_{q,k,v}
pass again.
- Bump voyager-compiler to 9dc7e5b: a split-K tail that opens with a
dequantize chains onto the accumulate instead of reading the tile back.
Picks up the SpMM scale-buffer veto: llama_prefill_spmm's q/k/v/o_proj under MXNF4 no longer overflow the unit's 32-row weight-scale buffer.
c79a65d moved every gold kernel's tensors to std::shared_ptr<T[]> and converted the body of DwC with them (its outputs are allocated as one) but left the signature returning a raw Buffer*. DwC is instantiated only under SUPPORT_DWC, which no build since then had set, so the mismatch first surfaced once MXINT8 mobilenet_v2 codegen succeeded: GoldModel.o failed to compile (cannot convert std::shared_ptr<StdFloat<7, 8, ...> []> to StdFloat<...>* in return), no runner was linked, and the fast-systemc run reported all 42 layers failed. Return std::shared_ptr<Buffer[]> as gemm and gemv_quantized do.
Picks up e139a80, which seeds the tiler's prefetch cache with the four-field tuple for the layers interstellar skips. Since 57bfae2 the serial path unpacks (mapping, access_list, bank_groups, scratch_slots), while prefetch_tilings still stored (None,) * 3 for a depthwise conv, so mobilenet_v2 codegen crashed before emitting model.txt and the MXINT8 CI job died on the missing layers.txt. With the DwC fix beneath it, all 42 uniquified mobilenet_v2 MXINT8 16x16 layers pass fast-systemc with SUPPORT_DWC=true.
run_accuracy graded a run with abs(final - gold) < 1, so a datatype that scored more than a point above the accuracy recorded for it in ACCURACY_RESULTS failed the job exactly as a regression would. Only a drop is a regression: pass any result above gold - 1 and keep the recorded value as the floor.
Picks up 6d4501d, which sizes a split reduction's L2 output tile in the anchor's dtype instead of the fixed 16-bit PSUM_BITS. Under CFLOAT the tile search had admitted ViT's mlp_fc2 a K=64 x N=384 tile at 15 banks that the memory planner needed 17 for, so codegen raised plan_memory (2228224 > 2097152 bytes), no model.txt was written, and the CFLOAT ImageNet CI job crashed on an empty accuracy log. ViT CFLOAT now plans into exactly 2 MB with fc2 back on its K=128 x N=256 tile and scores 84.5% on 1000 ImageNet samples (gold 84.7). Also carries 67a0e90, the llama_verify speculative-decoding step.
The firmware runs the whole program now, so ScheduleRecorder no longer records a replay schedule for the testbench: it gives JtagPreload the load sequence to bake into the scratchpad image and where the dispatches fall between them. With no testbench to pace against, the recorded order is the order, so the semaphore bookkeeping goes away with it. The grade is scratchpad-only, and readback_regions already bounds the dump list to the bytes gold wrote, so the same gold walk can also write what a passing readback must contain: <layer>_scratchpad_expected.bin.<i>, one per region. A plain byte compare against those is the verdict jtag_sim_checker gives -- gold and the accelerator are compared with no tolerance -- which lets the chip flow grade itself with no gold model, tensors or compiler. POWER_LOOP wraps the emitted program in while (1) for a steady-state power measurement. Every address is baked into the parameter blobs, so a pass repeats the same dispatches over the same scratchpad slots; the firmware never returns, so those ELFs are driven by hand rather than by the graded flow. Simulation::grade_only_partition drops every output outside one partition from the grade, which is what a scratchpad-only readback needs.
rtl_simulation was gated on pull_request, so pushes to main never uploaded regression-results-* artifacts. check_pull_request.py looks up the latest successful run on main, found one with no artifacts, and crashed on an empty frame with KeyError: 'datatype'. Run rtl_simulation on push as well, so each merge refreshes the baseline, and skip the comparison instead of crashing when main has no results yet.
test_codegen.py no longer accepts --compile_single_layer, so network-proto failed for every llama model and run_voyager.py could not compile them. Pass --num_hidden_layers 1, the flag CI already uses.
ci_skip_rules.json: name the five campaign networks in the MXNF4 decode rule so run_voyager --skip_layers and the calibration planner resolve the same list for them (they carried the same bool-operand layers under other names). codegen.mk: --compile_single_layer became --num_hidden_layers 1. Voyager.scala: comment the taped-out same-cycle start/done counter bug on the three in-flight counters; no logic change (Sphinx silicon).
get_type_width asserted, so a layer with an operand outside the configured types (le: a bool) aborted GenerateSoCBinaries and took every later layer of the batch with it -- the per-layer catch in the emitter never saw it, since abort() is not an exception. Throw std::out_of_range instead, under #ifndef __SYNTHESIS__ as the other host-side checks in src/ are, so HLS still sees straight-line code. The emitter now logs "Skipping le: ..." and carries on (verified 2026-09-12, regression_results/2026_09_12_21_28_27). The Catapult cache was re-stamped after the edit; make -q rtl is up to date.
Five commits: RTL calibration keyed by the bufferizer build cache key (e613cbe: build_key on every node, DMA-only kernels grouped structurally, one Calibration row per key, --num_hidden_layers), GEMV operand sizing and accumulator-scratch charging in the tile search, fused-tail stream break classified at the tile shape, outlier threshold over the whole tensor.
Two commits: a fused tail's fate named by K regime (fd6bd0d), and the bufferizer folding a tail's renaming transpose into a view (f9d4c49), which fixes llama_prefill matmul_mx_default_1_fused in the MXNF4 64x64 fast-systemc CI.
jeffreyyu0602
force-pushed
the
voyager-compiler-migration
branch
from
September 14, 2026 12:13
f7a7ba4 to
0733cd8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
otheras well as a linear's two-dimensional weight