Skip to content

asap7/coralnpu: register file generated, the snapshot unchanged - #4653

Draft
oharboe wants to merge 5 commits into
The-OpenROAD-Project:masterfrom
oharboe:coralnpu-showcase
Draft

oharboe wants to merge 5 commits into
The-OpenROAD-Project:masterfrom
oharboe:coralnpu-showcase

Conversation

@oharboe

@oharboe oharboe commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

A showcase of AUTO_MEMORIES_REGFILES and SYNTH_VERILOG_SURGERY on coralnpu, whose integer register file has 8 read and 6 write ports. The RTL snapshot is unchanged: the adaptation is a config, an SDC, a surgery script and a register-file spec. flow/designs/asap7/coralnpu/README.md describes all four designs.

Surgery: an exercise left to the reader, solved. In the snapshot, one execute cycle reads the register file, forwards an ALU result through it, lets dispatch decide which lanes issue, and has the load/store unit build its micro-ops and enqueue them. That one family of paths sets the period. coralnpu-exmem adds the textbook EX/MEM register on the load/store unit's inputs (Hennessy & Patterson's five-stage pipeline; Rocket and CV32E40P do the same). It is a stand-in for a fresh snapshot: upstream coralnpu has since reworked this cycle in Chisel (43d2030).

coralnpu, global route clock period std-cell area CTS + global route
coralnpu (flops) 1,900 ps 2,031 ps 16,757 µm² about 8,700 s
coralnpu-regfile 1,900 ps 1,902 ps (−6.4 %) 15,628 µm² (−6.7 %) 836 s
coralnpu-exmem 1,700 ps 1,701 ps (−16 %) 16,041 µm² (−4.3 %) 1,172 s
coralnpu-regfile-exmem 1,650 ps not yet measured; meets 1,700 ps (−16 %) 15,565 µm² (−7.1 %) at 1,700 ps 642 s at 1,700 ps

OpenROAD 22b7c537 and ORFS ef42174 with bazel-orfs's carried patches, this PR among them. Each design's clock sits just under what it reaches, so repair has a target and the WNS is slightly negative; 1,650 ps for coralnpu-regfile-exmem is a guess under the 1,700 ps it meets. In the flop build, nearly all of the CTS and global route time is repair_timing chasing the one family of paths.

The commits.

  1. The baseline fixes what made the previous setup unusable for a comparison: a 10 ps clock; fakerams with no write mask, so every masked write wrote all 16 bytes; firtool's verification layers synthesized into the core.
  2. The register file: Regfile keeps its words as separate registers among a scoreboard and bypasses. Surgery moves the storage into a module the generator replaces, and rewrites the bypass as a write-through read on the register file's ports.
  3. surgery.py reads and writes UTF-8 (review).
  4. The EX/MEM register, with each EX/MEM design's own clock: surgery.py --address-register registers LsuV1's inputs (each lane's request and valid, the effective addresses, the store data). io_req_N_ready keeps its thresholds, applied to the queue's count plus the entries the register will enqueue; io_active includes the register, so a fence waits for it. Loads and stores take one cycle longer.
  5. The README, with a diagram of the surgery, what is public about how the silicon met the same cycle (production silicon in Synaptics' Astra SL2610; no public clock for its CoralNPU core), and what is not solved here: the proper fix is a patch to coralnpu's Chisel, regenerated and run through coralnpu's own tests, or a fresh snapshot. Out of scope for this PR.

Correctness, in bazel-orfs (carried there as patches):

  • the register file as in the snapshot against the surgered one with the generated array, 20,000 random cycles, colliding writes on six ports, masks, bypass, resets; a negative control that must fail, and does;
  • LsuV1 as in the snapshot against the EX/MEM one under the same random requests, up to four lanes a cycle with the buses stalling at random: every bus transaction, writeback, flush and fault identical, channel by channel, for three seeds of 20,000 requests; a negative control without the ready adjustment loses requests and must differ, and does; the surgery changes LsuV1 and no other module.

Power is not in the table: coralnpu's top-level ClockGate synthesizes as a latch and an AND (upstream instantiates a library ICG per process; this PR leaves it as is), and the power figure depends on how that one clock gate is modelled (see the earlier comment).

Depends on various and sundry AUTO_MEMORIES PRs and on The-OpenROAD-Project/OpenROAD#11669: a draft until they are merged.

🤖 Generated with Claude Code

The snapshot is synthesized as it is; what was adapted by editing it,
or left to chance, is now the flow's configuration:

- The clock was 10.00 in a platform whose unit is the picosecond. The
  SDC sources the platform's constraints.sdc, with set_max_delay IO
  budgets, at 1,900 ps, just under the ~1,955 ps the flow reaches; the
  test enable is held at 0 and the asynchronous reset is not timed.
- `-D USE_ASAP7` swapped the TCM SRAMs onto fakeram_512x128 and
  fakeram_2048x128, which have no write mask: the wrapper computed the
  byte mask and connected it nowhere, so a masked write wrote all 16
  bytes. surgery.py (SYNTH_VERILOG_SURGERY) turns each SRAM wrapper into
  an instance of a firtool read-write memory module, and AUTO_MEMORIES
  generates a macro for each, mask and all.
- firtool's verification layers are concatenated into the snapshot: 61
  binds of *_Verification_Assert into the core. A layer is opt-in;
  surgery.py removes the verification/ files, which is firtool's output
  without layers enabled.
- The kept hierarchy was whatever exceeded SYNTH_MINIMUM_KEEP_SIZE; it is
  coralnpu's units by name, with OPENROAD_HIERARCHICAL=1.

Each edit is recognised by its text, token for token: a snapshot that is
not the one surgery.py knows stops the build.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
coralnpu's Regfile has 32 words of 32 bits with 8 read and 6 write
ports, and keeps them as 31 separate registers among a scoreboard, read
bypasses and registered read outputs: no array for yosys to infer and no
module of its own. surgery.py --regfile checks every read select, write
decode, bypass and register statement against the shape firtool wrote,
and moves the storage into Regfile_rf: x0 reading zero, eight
combinational reads, six writes ORed when two write one word, every word
reset to zero while reset is high. Regfile_rf.regfile has
AUTO_MEMORIES_REGFILES generate it.

Each read port's bypass, a select by read address over every word's
decoded write data, becomes a write-through read: the read address
compared with each write port's, the hits' data ORed. It is the same
function on the register file's ports alone, and leaves the per-word
write decode, which the generated register file does inside, with no
reader.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces configuration and surgery scripts for the coralnpu design, including a new register file specification (Regfile_rf.regfile) and a Python script (surgery.py) to post-process the generated Verilog by stripping verification layers, rewriting SRAM wrappers, and optionally restructuring the register file. Feedback on the changes highlights a critical indexing bug in the simulation-only memory initialization task within surgery.py, along with recommendations to ensure the output directory is created before writing and to specify UTF-8 encoding when reading and writing files.

MARKER = re.compile(r'^// ----- 8< ----- FILE "([^"]+)" ----- 8< -----$', re.M)


SRAM_TEMPLATE = "module Sram_{D}x128( input clock, input enable, input write, input [{AM1}:0] addr, input [127:0] wdata, input [15:0] wmask, output [127:0] rdata ); `ifdef USE_ASAP7 wire [127:0] nwmask; genvar i; generate for (i = 0; i < 16; i++) begin assign nwmask[8*i +: 8] = {8{wmask[i]}}; end endgenerate fakeram_{D}x128 u_asap7_sram ( .rd_out(rdata), .addr_in(addr), .wd_in(wdata), .we_in(write), .ce_in(enable), .clk(clock) ); `else reg [127:0] mem [0:{DM1}]; reg [{AM1}:0] raddr; assign rdata = mem[raddr]; `ifndef SYNTHESIS task randomMemoryAll; for (int i = 0; i < 128; i++) begin for (int j = 0; j < {D}; j++) begin mem[i][j] = $random; end end endtask initial begin randomMemoryAll; end `endif always @(posedge clock) begin for (int i = 0; i < 16; i++) begin if (enable & write & wmask[i]) begin mem[addr][i*8 +: 8] <= wdata[8*i +: 8]; end end if (enable & ~write) begin raddr <= addr; end end `endif endmodule"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a critical indexing bug in the simulation-only randomMemoryAll task within SRAM_TEMPLATE.\n\nIn the nested loops:\n- i loops from 0 to 127 (intended for bits, but used as the first index mem[i], which represents words). This means only the first 128 words of the memory are initialized.\n- j loops from 0 to {D} (depth, e.g., 512 or 2048, used as the second index mem[i][j], which represents bits). Since each word is only 128 bits wide, any access where j >= 128 is an out-of-bounds bit select.\n\nTo fix this and make the initialization correct and robust, we can loop over the {D} words and assign a concatenated 128-bit random value using four $random calls.

Suggested change
SRAM_TEMPLATE = "module Sram_{D}x128( input clock, input enable, input write, input [{AM1}:0] addr, input [127:0] wdata, input [15:0] wmask, output [127:0] rdata ); `ifdef USE_ASAP7 wire [127:0] nwmask; genvar i; generate for (i = 0; i < 16; i++) begin assign nwmask[8*i +: 8] = {8{wmask[i]}}; end endgenerate fakeram_{D}x128 u_asap7_sram ( .rd_out(rdata), .addr_in(addr), .wd_in(wdata), .we_in(write), .ce_in(enable), .clk(clock) ); `else reg [127:0] mem [0:{DM1}]; reg [{AM1}:0] raddr; assign rdata = mem[raddr]; `ifndef SYNTHESIS task randomMemoryAll; for (int i = 0; i < 128; i++) begin for (int j = 0; j < {D}; j++) begin mem[i][j] = $random; end end endtask initial begin randomMemoryAll; end `endif always @(posedge clock) begin for (int i = 0; i < 16; i++) begin if (enable & write & wmask[i]) begin mem[addr][i*8 +: 8] <= wdata[8*i +: 8]; end end if (enable & ~write) begin raddr <= addr; end end `endif endmodule"
SRAM_TEMPLATE = "module Sram_{D}x128( input clock, input enable, input write, input [{AM1}:0] addr, input [127:0] wdata, input [15:0] wmask, output [127:0] rdata ); `ifdef USE_ASAP7 wire [127:0] nwmask; genvar i; generate for (i = 0; i < 16; i++) begin assign nwmask[8*i +: 8] = {8{wmask[i]}}; end endgenerate fakeram_{D}x128 u_asap7_sram ( .rd_out(rdata), .addr_in(addr), .wd_in(wdata), .we_in(write), .ce_in(enable), .clk(clock) ); `else reg [127:0] mem [0:{DM1}]; reg [{AM1}:0] raddr; assign rdata = mem[raddr]; `ifndef SYNTHESIS task randomMemoryAll; for (int i = 0; i < {D}; i++) begin mem[i] = {$random, $random, $random, $random}; end endtask initial begin randomMemoryAll; end `endif always @(posedge clock) begin for (int i = 0; i < 16; i++) begin if (enable & write & wmask[i]) begin mem[addr][i*8 +: 8] <= wdata[8*i +: 8]; end end if (enable & ~write) begin raddr <= addr; end end `endif endmodule"

)
p.add_argument("files", nargs="+")
a = p.parse_args()
done = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The output directory a.out_dir is not guaranteed to exist. If it does not exist, attempting to write files to it will raise a FileNotFoundError. It is safer to ensure the directory exists by calling os.makedirs(a.out_dir, exist_ok=True) before entering the file processing loop.

    os.makedirs(a.out_dir, exist_ok=True)
    done = 0

Comment thread flow/designs/asap7/coralnpu/surgery.py Outdated
a = p.parse_args()
done = 0
for path in a.files:
with open(path) as f:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When opening files in Python 3, it is highly recommended to specify the encoding (e.g., encoding="utf-8") to ensure platform-independent behavior and avoid potential UnicodeDecodeError on systems where the default encoding is not UTF-8.

Suggested change
with open(path) as f:
with open(path, encoding="utf-8") as f:

Comment thread flow/designs/asap7/coralnpu/surgery.py Outdated
% (os.path.basename(path), len(removed))
)
done += 1
with open(os.path.join(a.out_dir, os.path.basename(path)), "w") as f:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly, specify encoding="utf-8" when writing the file to ensure consistent UTF-8 output across different platforms.

Suggested change
with open(os.path.join(a.out_dir, os.path.basename(path)), "w") as f:
with open(os.path.join(a.out_dir, os.path.basename(path)), "w", encoding="utf-8") as f:

@oharboe

oharboe commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator Author

Re-measured exactly as this PR has it, without the ClockGate mapping onto ASAP7's ICG that the first numbers used:

coralnpu, global route flops generated register file
min period 1,976 ps 1,900 ps (−3.9 %, meets the target)
std-cell area 16,563 µm² 15,512 µm² (−6.3 %)
power 63.3 mW 45.4 mW (−28 %)
stage time 3,994 s 1,112 s

Period and area hold. Power does not: with the latch-and-AND clock gate the flop build's power nearly doubles (33.0 → 63.3 mW) and the register file's less (35.8 → 45.4 mW), so the power figure depends on how that one clock gate is modelled. The description now carries these numbers, with that caveat.

🤖 Generated with Claude Code

@openroad-ci

openroad-ci commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

🔍 QoR check

Metrics reflect the PR merge build — i.e. what will land on the target branch.

Commit c7a13c2 · Jenkins build #2 · Baseline: build · View build on dashboard

62 design(s) checked — 0 with regression(s), 0 without a comparable baseline.

oharboe added a commit to The-OpenROAD-Project/bazel-orfs that referenced this pull request Oct 9, 2026
asap7/coralnpu and asap7/coralnpu-regfile are in
The-OpenROAD-Project/OpenROAD-flow-scripts#4653, its files the same as
ORFS patches 0102 and 0103 but for black's line breaks in surgery.py and
a space in constraint.sdc. The ladder stops carrying them: 0102 and
0103 go, 0104 keeps only swerv_wrapper-regfile-mux, and coralnpu's
rows, its synthesis, surgery and simulation tests leave
test/regfile/ladder. designs_by_directory_test checks the swerv mux
variant against its base.

The design DSL stops exporting .ys files: no design in the series names
a SYNTH_POST_HIERARCHY_SCRIPTS script from another package since
coralnpu took #4653's form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
oharboe added a commit to The-OpenROAD-Project/bazel-orfs that referenced this pull request Oct 9, 2026
asap7/coralnpu and asap7/coralnpu-regfile are in
The-OpenROAD-Project/OpenROAD-flow-scripts#4653, its files the same as
ORFS patches 0102 and 0103 but for black's line breaks in surgery.py and
a space in constraint.sdc. The ladder stops carrying them: 0102 and
0103 go, 0104 keeps only swerv_wrapper-regfile-mux, and coralnpu's
rows, its synthesis, surgery and simulation tests leave
test/regfile/ladder. designs_by_directory_test checks the swerv mux
variant against its base.

The design DSL stops exporting .ys files: no design in the series names
a SYNTH_POST_HIERARCHY_SCRIPTS script from another package since
coralnpu took #4653's form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
@oharboe

oharboe commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator Author

Where the flop baseline's period goes, and why the register file is the lever

I took this PR's flop baseline to global route and asked where its reg2reg period goes: OpenROAD 22b7c537, ORFS ef42174 with this PR applied, built with bazel-orfs. Here it reaches 2,031 ps against the 1,900 ps clock. The description reports 1,976 ps; the OpenROAD commit and flow differ.

One path family sets the period. All of the worst 200 reg2reg paths start at the register file's read data (core.score/regfile/io_readData_*) and end in the LSU's input queue (core.score/lsu/ctrl.mem_*). 810 endpoints sit between 2,000 and 2,099 ps, with the rest of the design mostly at 1,200–1,500 ps.

It is logic, not placement. Mean over those 200 paths:

ps
logic 1,392
repeaters (about 31 per path) 481
wire 69
clock skew 26

The modules on the path are compact. Measured by the radius holding half of each module's cell area, against a disc at the design's density, the LSU is 0.83× and the register file 1.18×. A perfect placement would recover at most the roughly 550 ps of repeaters and wire.

It is a mass, not one path. Removing the worst launching register 40 times (set_false_path -from the register, as a measurement in a thrown-away session, never in the SDC) moves the period only from 2,031 to 1,993 ps.

This is the cycle the design chose. CoralNPU is an in-order three-stage pipeline: fetch, decode/dispatch, execute/writeback, up to 4 instructions per cycle (microarch.md). Operands are registered at the dispatch → execute boundary. Then one execute cycle does the rest: the register file adds the immediate (busAddr = rdata + imm, priority-encoded across lanes, Regfile.scala), the LSU builds its µop from it, and the µop is enqueued in the LSU's input buffer (lsu.md, "Ingestion"). The documented load latency is 2+ cycles.

Compared with the literature, the following appears to be left as an exercise for the reader:

  1. The address register. The classic five-stage pipeline, and Rocket and CV32E40P after it, registers the effective address after address generation, before any memory-side logic: the EX/MEM latch (Hennessy & Patterson). CoralNPU puts address generation, µop construction and the enqueue in one cycle, much as Ibex does with its combined ID/EX stage. That buys a cycle of load latency and leaves this cycle's depth to the implementation.
  2. The multiported register file. The integer register file has 8 read and 6 write ports and is written as flops behind mux trees. The literature treats register files with this many ports as a structure of their own, because delay, area and energy grow with port count: banked, replicated or custom arrays (Rixner et al., "Register Organization for Media Processing", HPCA 2000; Zyuban & Kogge, "The Energy Complexity of Register Files", ISLPED 1998). Generating it as an array is that exercise, and it is this PR's second commit.
  3. Issue width in the critical cycle. Bypass and select logic grow with issue width and become the cycle-limiting paths of wide machines; the literature's answer is to partition them (Palacharla, Jouppi & Smith, "Complexity-Effective Superscalar Processors", ISCA 1997). Here the address cycle carries the 4-lane priority encoding of the bus port. The LSU's input buffer is built as a multi-enqueue buffer for all instruction lanes (CircularBufferMulti(…, p.instructionLanes, …)), while the dispatch documentation says memory instructions dispatch at most one per cycle (dispatch.md). Whether synthesis removes the enqueue lanes that never carry data is not measured here.
  4. Retiming. Retiming (Leiserson & Saxe, "Retiming Synchronous Circuitry", Algorithmica 1991) moves existing registers; it cannot add a stage. The RTL has no empty pipeline stages for it to fill. The register file's output registers hold their value through a feedback mux, which blocks moving them. The LSU's one mention of retiming is a designer's TODO for a manual change (// TODO: if timing violation, retime this). Retiming in synthesis has nothing to work with on this path.

What silicon exists. Synaptics' Astra SL2610 (12 nm) is the first production implementation of Coral NPU, "a tightly-coupled CPU optimized for Synaptics' PPA requirements", with the scalar core only; the vector and matrix units are still in development. Google offers the design as IP, with VeriSilicon productising it, and states a target of 800 MHz (1,250 ps) at 22 nm. This snapshot is the open reference design that production silicon is derived from, not a taped-out netlist; how Synaptics' implementation handled the points above is not public.

What follows for this PR. The period is set by the register file's read and address logic feeding the LSU, in a cycle the design intends to close as written. Of the exercises above, the register file is the one a flow can take on without changing the RTL's behaviour, and building it as a generated array is what the second commit does (1,976 → 1,900 ps in the description). The remaining gap to the design's own target, 1,250 ps at 22 nm against 2,031 ps here on asap7, a faster node, is either implementation (how synthesis maps this one cycle) or this snapshot's configuration (instruction lanes, FPU and vector enabled), which may be heavier than the 800 MHz one. I have not separated the two.

Sources: CoralNPU Chisel and docs, Google Developers Blog, EE Times, VeriSilicon, Ibex ID/EX, CV32E40P LSU.

@oharboe

oharboe commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator Author

Notes for later: getting the most testing value out of CoralNPU with one timing budget

These are notes on what to investigate later, written up here for now so they sit next to the design they're about. Nothing below has been tried in this PR yet: the budget, its value and its effect on WNS are proposals to check, not results.

CoralNPU is a valuable addition to ORFS's asap7 set. It's an open RISC-V design with production silicon behind it (Synaptics' Astra SL2610), it's hierarchical, with this PR its SRAMs are generated macros and its register file can be a generated array, and it runs synthesis to global route in about an hour. Few designs exercise memory generation, macro placement and timing closure together at a turnaround a flow developer can iterate on.

Where its WNS comes from today. Taken to global route with this PR's flop baseline, its reg2reg period is about 2,031 ps (OpenROAD 22b7c537, ORFS ef42174). That number comes from one family: the register file's read data into the LSU's input queue (core.score/regfile/io_readData_* → core.score/lsu/ctrl.mem_*), 810 endpoints within 100 ps of the worst. Below it, about 1,200–1,500 ps of paths across fetch, dispatch, the FPU and the bus bridges. Whether floorplanning, macro placement, global placement and timing repair show there is one of the things to investigate.

Why that family is special. CoralNPU's pipeline does the register file read, the address add and the LSU enqueue in one execute cycle (microarch.md), with an 8-read, 6-write register file written as flops. The literature resolves this cycle in one of two ways. Classic pipelines register the effective address before the memory side (the EX/MEM latch of Hennessy & Patterson's five-stage pipeline, as in Rocket and CV32E40P). Register files with this many ports are built as a structure of their own, banked or custom arrays (Rixner et al., HPCA 2000), rather than flops behind mux trees. The open RTL leaves that step to the implementer. An implementation for silicon would most likely take one of those routes. Synaptics describes its version as "optimized for Synaptics' PPA requirements"; the details aren't public.

The idea: time that family against its own budget. Without touching the RTL, constraint.sdc could stand in for that implementation step, timing these paths against a budget of their own, just above where they are today, while the rest of the design is timed against the clock. A sketch, untested:

# The register file's read data into the LSU's input queue: one execute
# cycle in the open RTL, a cycle an implementation for silicon resolves
# (an address register, or a register file built as an array). Timed
# against its own budget, so the rest of the design sets this test
# case's WNS; the paths stay timed and repaired, and a regression shows
# as negative slack here.
set rf_read [get_cells core.score/regfile/io_readData_*]
set lsu_q   [get_pins  core.score/lsu/ctrl.mem_*/D]
if { [llength $rf_read] == 0 || [llength $lsu_q] == 0 } {
  error "constraint.sdc: the register-file -> LSU family matched nothing"
}
set_max_delay 2100 -from $rf_read -to $lsu_q

What it should give, to be confirmed:

  • An idiomatic WNS. The reported number would be the one the design has once that cycle is resolved, as an implementation for silicon would resolve it. Floorplanning, macro placement, global placement and repair would then move it, instead of sitting under one family.
  • Nothing unoptimised or unwatched. Unlike an exception that removes paths from timing, set_max_delay is an optimisation target. The paths stay in placement and repair against their budget, a regression shows as negative slack, and an improvement (a generated register file, a better-mapped address add) shows as positive slack.
  • No change to the RTL. The snapshot stays byte-identical to upstream; the budget lives in the SDC beside the clock.
  • A rename fails loudly. The guard turns a moved instance name into an error instead of a silently empty constraint. It relies on this PR's kept modules (Regfile, LsuV1) and OPENROAD_HIERARCHICAL=1.
  • Repair effort goes where it can help. This PR's description notes how much of the flop build's CTS and global route goes into this one family.

Two numbers per run: WNS for the design against the clock, and the register-file family's slack against its budget. Together they would say how the flow does on the design as a whole, and how the one-cycle read-to-LSU path does on its own.

With the generated register file. This PR's second commit takes one of the literature's routes for real: coralnpu-regfile builds the register file as an array. Its slack against the budget would then measure what the array buys, while the design's WNS keeps tracking everything else. coralnpu and coralnpu-regfile side by side could make a compact regression set: memory generation, register-file generation, macro placement and timing closure, in one fast design.

To investigate: whether the budget matches the paths it should and only those; what WNS the rest of the design reports with it; whether that number responds to floorplan and placement changes; and what budget value keeps the family met today while still catching regressions.

@oharboe

oharboe commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

@maliberty TL;DR, no action needed now:

  • The coralnpu snapshot is stale. CoreMiniAxi.sv (firtool 1.114, added 2026-08-14) records no upstream commit or recipe. Its worst timing path, register-file read to the LSU's input queue, is one upstream has since reworked in Chisel (43d2030 and an LSU timing series, targeting 800 MHz in 12 nm). Problems measured on this snapshot may not exist upstream.
  • In lieu of a fresh upstream snapshot, I'm testing surgery that resolves this ghost WNS outlier the textbook way: an EX/MEM address register on the LSU's inputs, so the rest of the design sets the WNS. Results to follow here.
  • coralnpu isn't in CI. It has no rules-base.json, so nothing depends on it today.
  • Upstream takes outside fixes slowly. It imports outside PRs, and 15 of 37 have landed. Several RTL fixes have waited months with no reply.
  • This PR stays a draft until various and sundry AUTO_MEMORIES PRs land. It carries data for a later decision on coralnpu in ORFS: the baseline fixes, register file vs flops, and where the period goes.
  • One option for later: regenerate the Verilog from a pinned coralnpu commit and check it in, so testers need neither Bazel nor Chisel.

🤖 Generated with Claude Code

oharboe and others added 3 commits October 12, 2026 11:15
The encoding is no longer the platform default's, as the review asked.
The docstring no longer calls the surgery a hack: it is the step that
puts the build's edits to the snapshot on the books.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The open RTL resolves a register-file read, an ALU result forwarded
through the register file, dispatch's issue decision and the load/store
unit's enqueue in one cycle. surgery.py --address-register registers
LsuV1's inputs (each lane's request and valid, the effective addresses
and the store data), the EX/MEM register of the classic five-stage
pipeline; LsuV1 builds and enqueues its uops from it a cycle later.

io_req_N_ready keeps its thresholds, applied to the entries the queue
holds plus those the register will enqueue, and io_active includes the
register, so a fence waits for it. Loads and stores take one cycle
longer.

coralnpu-regfile-exmem combines it with coralnpu-regfile.

Each has its own clock just under what it reaches, as coralnpu's is:
1,700 ps for coralnpu-exmem, which reaches 1,701 ps at global route,
and 1,650 ps for coralnpu-regfile-exmem, which meets 1,700 ps.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ader

What the four coralnpu designs build and their results at global route;
where the RTL comes from and what is missing (a recipe that regenerates
it from a pinned upstream commit); which surgery closes the gap between
upstream and an idiomatic ORFS build, and which stands in for a fresh
snapshot; the EX/MEM register with a diagram, what is public about how
the silicon met the same cycle, and what is not solved here.

Signed-off-by: Øyvind Harboe <oyvind.harboe@zylin.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@oharboe

oharboe commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Results: with the EX/MEM register, coralnpu goes from 2,031 ps to 1,701 ps at global route (1,700 ps clock); with the generated register file too, it meets 1,700 ps. The worst path moves to register file → CSR minstret. Each EX/MEM design now has its own clock just under what it reaches. Details in the description and the new README.

Correction to my earlier analysis: the queue is FifoX, not CircularBufferMulti, and the path enters it through dispatch's lane issue, not the address.

🤖 Generated with Claude Code

# concatenated in: 61 binds of *_Verification_Assert into the core. A
# layer is opt-in, so a synthesis build leaves them out; surgery.py
# removes them, as firtool's output without layers enabled would be.
export SYNTH_VERILOG_SURGERY = $(DESIGN_HOME)/$(PLATFORM)/coralnpu/surgery.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this file? I don't see it in the PR

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a draft where I keep notes, the file is still in a patch in bazel-orfs for now.

Once all fhe auto memories land in ORFS, I will clean this up if you want it, otherwise it has been useful for my testing when working on auto memories.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants