The ASIC manifest¶
allo/backend/asic (see its PROVENANCE.md) has two entry points, and
they sit on opposite sides of one question.
The flat flow consumes a directory of Verilog and an sv2v_manifest.f
file list, entered at the design collector. It knows nothing about Allo, and
every ASIC number the fork has published came through it — which is why those
runs were fed by committed RTL exports rather than by the compiler.
AAAH, the allo-* nodes, consumes Allo-level architecture, and not
through files. Its compilation node loads an Allo design and calls
build(project, target, mode, configs) with
configs["asic_manifest"] = {...}: Allo is asked to emit, as part of
compilation, the architecture it just compiled. Downstream planners then place
macros on a grid from PID row/column deltas, weight the wires between them by
pre-HLS stream bit width, and decide which RTL modules are one macro by
semantic identity rather than by name. None of that survives in emitted
Verilog, so nothing later in the flow can recover it or check it.
Until 2026-09-24 this fork did not emit that manifest, which is why the TinyTPU
runs were routed around the compilation node. allo.backend.asic_manifest
is the emitter.
Important
Emitting the manifest is not the same as closing the loop. Allo now emits what its own ASIC backend consumes, verified against the consumer and against TinyTPU’s known post-HLS structure; an end-to-end PD run from the specification has not been performed, and cannot be on a host without a Synopsys DC licence. What still has to be proved is at the bottom of this page.
What is emitted, and when¶
Two files, matching what nodes/allo-asic-compilation/validate_build.py
checks for, each with a .tcl sibling because the flow’s nodes stage both:
asic-manifest.json—stage: "pre_hls"The architecture. Written inside
allo.dataflow.build(), from the schedule that is about to be handed to the backend, before any HLS tool runs — so it exists whether or not a licence does.asic-manifest-final.json—stage: "post_hls_enriched"The same records joined to the RTL the backend produced. Written from
HLSModuleoncecsynhas succeeded, because only then do the module names exist to join to.
asic-debug/ holds flat TSV views of the same records, for reading by eye.
Nothing happens unless configs["asic_manifest"]["enabled"] is set, so the
ordinary Vitis and Catapult paths are unchanged.
import allo.dataflow as df
df.build(
design,
target="vitis_hls",
mode="csyn",
project="top.prj",
configs={
"frequency": 300.0,
"device": "u280",
"asic_manifest": {
"enabled": True,
"path": "asic-manifest.json",
"debug_artifacts": True,
"debug_dir": "asic-debug",
},
},
)
That configs dict is not an invention of this page: it is verbatim what
nodes/allo-asic-compilation/backend.py builds and passes in.
The schema, as the consumers define it¶
The schema is not documented anywhere in the flow; it is whatever its readers read. The fields below are the ones actually consumed, with the consumer that demands each.
Envelope¶
stage"pre_hls"/"post_hls_enriched".validate_buildrejects anything else.topThe RTL top module name.
plan_assemblyrequires it to be both the node’stop_moduleparameter and a module present indesign.v.backendvitis/catapult/systemc;plan_macrosandplan_assemblyrefuse a mismatch with their own parameter.summaryunmatched_or_ambiguousandunjoined_post_hls_recordsmust both be 0, checked twice (validate_build,plan_macros).top_arguments[]Order is load-bearing: it must equal the frozen workload’s
call_signature. Each entry carriesordinal,name,shape,type,direction.
pe_instances[]¶
semantic_id<top>/<kernel>/pid=<i>[,<j>]. The format is fixed byplan_physical_intent: it takes the first two/-segments as the kernel key and regex-matchespid=([0-9,-]+). Must be unique – several consumers key a dict on it with a bare[]lookup.kernelThe bare kernel name (not
top/kernel).pidList of ints.
plan_macrosturns a +-1 delta on the last axis intoE/Wand on the second-to-last intoS/N; the row index increases southward.ports[]The stream interface, in a dense 0-based
ordinalorder, each withchannel_id,stream,direction(in/out) andtype. The flow zips this positionally against the RTL’s handshake bundles, so anm_aximemory argument is not one of them.post_hls_records[]Post-HLS only:
rtl_root_module,rtl_modules[].name,rtl_equivalence_hash.
channels[]¶
channel_idandstreamProvenance;
ports[].channel_idjoins to it, and anullthere means a SystemC-internal channel.typeThe MLIR stream type.
plan_physical_intenttakes the firsti<N>/u<N>in it as the placement edge weight, so!allo.stream<i32, 16>weighs 32 and the depth is ignored.endpoints[]pe(asemantic_id),role(producer/consumer– read by physical intent),direction(in/out– read by macro planning), andaccesses[].port_ordinal. Both vocabularies are emitted, because the two nodes read different ones.
macro_groups[] (post-HLS)¶
macro_class_id, representative (must be in pe_instances),
member_count, members[] (semantic_id, rtl_module,
orientation), and proof.status — which must be exactly "proven" or
the class is silently dropped.
What the emitter derives it from¶
Everything comes out of the realized dataflow IR, never from a table someone typed. A hand-written manifest would be a second editing surface for the architecture, which is precisely what having one Allo specification exists to remove.
After allo.dataflow.customize, the module is one func.func per kernel
instance plus a top function whose body constructs every stream and then calls
each instance with the streams it is wired to. That body is the netlist.
PE instances — every
func.funccarryingdf.kernel. Its name is<kernel>_<i>_<j>; the region’s ownmappingsdict gives the split exactly (a kernel whose name ends in_<digits>is otherwise indistinguishable from one grid axis).Ports — the
stypesattribute, stamped per argument bymove_stream_to_interface:iread,owritten,_not a link.Channels and their endpoints — the SSA value each
allo.stream_constructdefines, matched against the operands of eachfunc.call.Top arguments —
allo.dataflow._build_manifest_top_argumentspairs the realized argument order withanalyze_arg_load_store’s directions. A region’s arguments are not necessarily emitted in declaration order (Dataflow Region Pitfalls), and direction is a fact about the body.Memories —
postprocess_hls_codepragmas argument i ontobundle=gmem{i}, so the manifest names the<top>_gmem<i>_m_axiadapter Vitis will generate for it.
The emitter refuses what the consumers assume and never state: a duplicate
semantic_id, a non-dense port ordinal, an endpoint naming an unknown PE or
an ordinal that PE does not have, and a channel with two writers or two
readers. Each of those is otherwise a KeyError, a StopIteration or a
silently misplaced macro several nodes downstream.
Post-HLS, the join walks Vitis’s naming: a kernel becomes <top>_<instance>
(sometimes with a numeric disambiguator — dma_ld_0 becomes
tinytpu_isa_dma_ld_0_1), with <root>_... children for each pipelined
loop it split out. Which modules belong to no kernel is decided by this
design’s kernel names rather than by a list of vendor module-name patterns:
an unclaimed module named after one of the design’s kernels is the error, and
everything else is plumbing.
Validated against TinyTPU¶
At the shipped configuration (TPU_T=4, TPU_MAXDIM=64) the emitter
produces, entirely from the design:
8 kernels and 38 PE instances —
peandwldat 4×4, the other six singletons — matchingTPU.architecture’sinstances=;85 channels, one per element of the 16 declared
Channels including the array ones, of which 12 (3 * T) are the terminal elements of the three chains and carry no endpoints — the last column forwards nothing east and the bottom row sendscwrather than a partial sum south;4 memories on
gmem0–gmem3.
The post-HLS join, run against the committed 145-file RTL export, reproduces
the module names in the shipped T4_MAXDIM64_shipped hierarchy report
exactly: accu_0, spm_0, vru_0, dma_ld_0_1, sequencer_0_1
and gmem0..3_m_axi, with 0 unmatched PEs and 0 unjoined records.
The sharpest check is that the flow’s own planner reads the array back out.
plan_macros.graph_pin_sides is given PIDs and channel endpoints and nothing
about systolic arrays; on pe(1,1) it returns a_fwd in from the W,
out to the E, and p_fwd in from the N, out to the S. 90 of the
146 stream pins resolve as same_kernel_neighbor.
tests/dataflow/test_asic_manifest.py runs all of this, computing its
expectations from TPU.architecture rather than hard-coding them, and loads
plan_macros and plan_physical_intent from
allo/backend/asic/nodes/ so that “the consumer accepts it” is executed
rather than asserted.
What is not emitted¶
Per-argument Catapult RTL protocol capture. allo-testbench-generation
wants catapult_argument, data_ports/triosy_ports, packing and
interface.roles on the catapult/systemc path. Those are a parse of
Catapult’s generated RTL, not an architectural fact, and they belong to a
separate post-HLS extractor. The Vitis path — the one every TinyTPU ASIC number
came from — is complete; the post-HLS join is hooked only to Vitis csyn
for the same reason, because emitting a wrong join would be worse than
emitting none.
Arithmetic. A composed region declares what a unit is wired to and never what it computes, so the manifest carries no compute ports. That is the Action layer’s to add (Actions: an Instruction as Composed Unit Effects).
Macro reuse, on the Vitis path. Each of TinyTPU’s 38 PEs hashes to its own
macro class, so member_count is 1 everywhere. This is not a defect in the
hashing: Vitis specializes every instance, and two interior PEs that are the
same hardware are emitted with different port orders, which
plan_macros.validate_and_map_ports correctly refuses to call equivalent.
Macro planning at the default min_macro_reuse=2 would therefore find no
class to harden. Getting reuse back is a question about how the design is
emitted, not about the manifest.
What a licensed host would still have to prove¶
Success here is that the consumer accepts the manifest. It is not a PD run. On a host with Vitis HLS and a Synopsys DC licence, the path from specification to PPA would have to show, in order:
run_design.py→validate_build.pypassing on a realcsyn: the four manifest files present, both stages correct, both summary counters 0, and the frozen workload’s interface matchingtop_arguments. Everything but thecsynis exercised offline today.sv2v-rtl-alloandallo-asic-macro-planrunning on that manifest. This is where the reuse ceiling above bites first, and where the failure would beno proven macro classes meet reuse thresholdrather than anything about the manifest’s shape.allo-asic-assembly-plan→allo-asic-physical-intent→allo-asic-rtl-assembly, which additionally require everystable_instance_nameto survive into the synthesized netlist, and themacro-link.rptcount to matchelaborated_macro_instance_count.A DC run that does not dissolve the boundaries. At the flattening effort the published runs use, synthesis flattens the hierarchy again unless told otherwise — and changing that makes new numbers non-comparable with the four already published. The manifest buys planning and attribution; it does not by itself buy a hierarchy-preserving netlist.
What could still be wrong at that point, honestly: the Catapult/SystemC
per-argument capture is absent, so those two backends cannot reach the
testbench node; the post-HLS join is validated against one design’s naming and
a second design could exercise a Vitis renaming this walk does not cover (the
counters would say so rather than the join guessing); and top_interface on
the non-Vitis paths is a documented default rather than an observation.