Skip to content

ANE: H16 cold boot and first inference - #750

Merged
RyanTheTide merged 46 commits into
aurora-wipfrom
ane/h16-first-inference
Nov 1, 2026
Merged

RyanTheTide merged 46 commits into
aurora-wipfrom
ane/h16-first-inference

Conversation

@RyanTheTide

@RyanTheTide RyanTheTide commented Oct 31, 2026 •

Copy link
Copy Markdown

The complete H16 (M4 Pro, T6041) ANE feature in one PR: staged cold boot of the iBoot-preloaded firmware, the shared firmware transport, preload identity, program LOAD/CREATE/EXECUTE through the ABI 3 accel node, the host-side LOAD section builder and the T6040/J616s DT. It replaces the per-milestone drafts #720, #705 and #699 (all of their commits are in this branch). The shared transport from #706 is included too, since the H17 work in #714 builds on the same code.

What it does

  • Staged bring-up, no module parameter. A valid H16 device binds idle with no power or MMIO access. Stages run from <debugfs>/ane-h16/<device>/stage (root only): preflight, map, boot, startup, program, each running every stage not yet run. Pre-RUN failures roll back; from RUN on everything is held until reboot.
  • Firmware builds are data. Each supported firmware build is a profile: a name, a preload rule (patchbay cells, the DATA range iBoot rewrites, the pinned SHA-256 of TEXT plus normalized DATA) and its boot values. The backend picks the profile whose rule matches the live preload, then takes the DVA layout from it. A preload whose layout matches but whose digest is not pinned is refused and its digest logged, so a new build is one table entry. Today: 27.0 (26A428). The 26.6.2 (25G83) entry lands once its digest is read from the J616s.
  • Preload ranges come from the memory-region nodes and must be no-map and outside System RAM. aura fills them from the ADT at boot (kboot: hand off the ANE firmware preload on the M4 Pro aura#37).
  • Program path. H16_LOAD validates the original HWX, places the four host-built sections and the HWX regions in private mappings, sends LOAD and CREATE. H16_EXEC copies inputs in, sends the gated EXECUTE, waits for the matching completion report and copies the output out. One program per firmware boot (the stale-binding rule from the H17 runs).
  • HWX validation fix. The validator paired the n-th tensor with the n-th surface segment and required the output last, so it refused the pinned add-128 program (segments x, y, z; tensors x, z, y). Tensors now pair with surfaces by name, and surfaces keep segment order.
  • Command arena reuse. Each completed EXECUTE gives its pool space back, so long runs no longer stop after about 75 requests.
  • LOAD section builder. tools/testing/selftests/drivers/ane/h16-sections.py derives the generic, operation, procedure and text-properties sections from the HWX (layouts after libane, MIT, by Joshua Warren). h16-run.py uses it and runs add-128 twice against the CPU oracle.
  • DT. The seven ANE power-state providers (ane_sys, ane_mpm, ane_cpu, ane_td, ane_base and the fab4_soc/fab5_soc parents) are enabled, the ANE lists stream 0 of each DART only, the DARTs keep apple,dma-range <0 0 0x400 0>, DAPF ranges are 64-bit pairs. The ANE and its DARTs stay disabled until the hardware run passes. t6040-j616s-ane-test.dts enables them for a test boot.

Testing

  • KUnit (transport.kunitconfig, KASAN/UBSAN): 125/125 on x86_64 and arm64, including the real add-128 HWX loaded and executed twice against the fake firmware, profile selection, HWX surface order and 1000 recycled requests.
  • dt_binding_check (npu/apple,ane.yaml) and dtbs_check (t6040-j616s, -tether, -ane-test) clean.
  • Modules build against the exact 7.3.0-aurora-tt3 config and are tt3-signed for the J616s kit.
  • J616s hardware: first exact inference on 2026-11-01 (7.3.0-aurora-fleet14c, modules from 992dbda, ANE enabled by runtime overlay). Preflight, map (DAPF written and read back 4/4), boot, startup (four acks, CONFIG_GET tick 0x16e3600) and program pass, and the add-128 HWX runs twice with 16384/16384 fp16 values exact against the CPU reference (output SHA-256 d113d9ae...). LOAD, CREATE, TERMINATE and UNLOAD complete. Details in the PR comment.

Latest commits

  • 26.6.2 (25G83) profile. Same patchbay records and rewritten range as 26A428, 0x3f80 later in DATA, same boot values, pinned to the live J616s preload digest.
  • Per-revision DART/DAPF table. Every DART and DAPF offset the stages touch comes from a table keyed by DART revision (T6041 0x0202). A revision without an entry is refused after the PARAMS3 read. One DAPF window, and preflight only reads it.
  • DAPF written at map. iBoot leaves the bank unprogrammed and it loses its records with ANE_SYS, which genpd and the pre-RUN rollback power off. So the driver owns it, as on T8140: at map it writes each differing record from apple,dapf-ranges (r4, start, end, r0, r20, as the t8110 bank takes them), reads it back and fails before RUN on a mismatch. A bank with lock bit 2 set is refused before any write.
  • Accel node opens. The hand-written fops lacked FOP_UNSIGNED_OFFSET, so drm_open_helper() refused every open. A new KUnit suite registers the real driver on a mock device and opens the node through its fops (without the flag it reproduces the J616s warning). h16-run.py now fails with the open() error instead of skipping.

🤖 Generated with Claude Code

RyanTheTide and others added 30 commits October 31, 2026 21:53
Track pending execution UUIDs before channel-7 publication and retire only
matching terminal channel-6 events. Keep one deadline across acknowledgment
and completion, order coherent DMA accesses, and quarantine failed or
uncertain requests without exposing private output mappings.

Validate complete HWX containers, tensor and program descriptors, aperture
records, symbols and payload-only kernel relocations. Match execute buffers
to kernel-owned process resources and revoke per-file authority on close.
Add hardware-free KUnit coverage for validation and pending request paths.

Keep H16 registration and section-only loading refused until preload,
startup, the original-container ABI and process-resource decoding exist.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Authenticate stable, owned resident firmware snapshots against the pinned
normalized digest, checking every patchbay tag and width and masking only
the reviewed mutable value cells and exact tunables interval. Keep live
page acquisition behind the boot backend ownership gate.

Import original HWX bytes once before validation and hashing, retain whole
segment geometry, and bound private load plans before DMA allocation.
Validate backend mapping geometry and decoded process bindings without
accepting userspace resource authority or publishing partial results.

Keep H16 registration, section-only loading and process creation refused:
the load/process wire bodies, allocation replies and physical boot backend
still require reviewed providers. Add hardware-free KUnit coverage for the
identity, import, planning and resource paths.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The accepted dependency patch is already applied verbatim as 4700e91,
with SHA-256 89bdf5072e468dd750580348679c0caa552ad26a07911ed218ac809495a1e8fd.
Record the original PR #681 head as an ancestor so merging it upstream
does not create add/add conflicts for the already extended HWX files.
Preserve the current source tree unchanged.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Describe the four inclusive DAPF ranges in record order so that the
firmware backend can derive both its filters and identity MMIO pages
from DT. Require them for T6041 and exercise the preload properties in
a T6041 example. The preload DVAs are fixed; only physical reservations
need to be supplied by the bootloader.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Describe the four DAPF ranges and preload device addresses without fixing
boot-specific physical addresses. Hook the J616s ANE to disabled no-map
TEXT and DATA placeholders for the bootloader to fill from segment-ranges.

Widen the three DART windows down to zero, preserving their upper bound
at 0x3ffffffffff. apple-dart uses the window for forced domain geometry,
including private paging domains. Raw iommu_map() currently permits low
mappings outside that geometry, but relying on that leaves the domain's
advertised aperture inconsistent with the firmware's identity MMIO pages.
The widened window covers both those pages and the high firmware DVAs.

Add a tether-only test DTS that enables the existing ANE and three DART
nodes. The shipping board tree keeps them disabled. Preload reservations
remain disabled in both trees until a loader completes the handoff.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Enable the ANE and its three DARTs only for the tether-loaded test DTB.
Keep the shipping board tree disabled and require the loader to fill
the preload reservations before the driver can pass preflight.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Validate the dynamic channel table before arming host rings, publish pool-backed commands, check replies and consume owned reports. Provide bounded SHAREDMALLOC allocations and match-data IRQ helpers with absolute command deadlines and retained failure state.

Exercise malformed descriptors, ring ordering, reply corruption, report ownership, allocation ceilings and deadline expiry using synthetic KUnit accessors. Keep probe and hardware ownership unchanged.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Include the existing tether DTS so the ANE test boot keeps its debug
console and the host-owned ATC0 path. Layer only the four ANE and DART
status enables on top of that established tether configuration.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Clarify that the backend resolves bindings from validated program metadata.
CREATE_PROCESS reply IDs do not describe a resource map. Keep wire
serialization and process installation separate from geometry validation,
with the existing default refusal unchanged.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Record the wrapped replacement description for commit 0a3bdfc.
Keep the published history intact.

Validate the dynamic channel table before arming host rings, publish
pool-backed commands, check replies and consume owned reports. Provide
bounded SHAREDMALLOC allocations and match-data IRQ helpers with absolute
command deadlines and retained failure state.

Exercise malformed descriptors, ring ordering, reply corruption, report
ownership, allocation ceilings and deadline expiry using synthetic KUnit
accessors. Keep probe and hardware ownership unchanged.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Use the shared DAPF and preload property names for staged boot.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Reuse the accepted normalized preload checker for the pinned 26A428 image.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Keep default registration refused and add explicit preflight, map, boot
and startup stages for the pinned H16 27.0 26A428 preload. Reuse the shared
transport and immutable preload checker, keep genpd transitions separate
from read-only power checks, and run the CE and ordered tunables before
scratch publication. Keep all firmware-visible state and a module reference
after RUN, including every uncertain failure.

Exercise the sequence with a simulated engine and document each tether
rung. Require a SID-0-only test description rather than silently claiming
SID 11 with the generic IOMMU attachment API.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Simulate offered RTKit traffic, absent heaps, power and RVBAR refusals,
post-RUN DART faults and mapping failures. Keep pool pointer arithmetic
inside its allocation and handle a failed domain allocation explicitly.

Require coherent clients for the private surfaces. Architecture cache
preparation helpers are not exported for a module, so refuse an unsupported
memory model rather than leave a cached alias without synchronization.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Opt the pinned H16 build into the native FREE tuple established by the
round-5 facts. Resolve only live provider-owned DVAs, publish the zero reply,
and retire report authority while retaining backing mappings and quota.
Keep other firmware profiles on the conservative default and refuse control
requests whose lookup fields have no proved wire layout.

Cover locked matching filters, heap/shared overlap, native and foreign FREE,
and control refusal in the simulated engine. Complete the polled interrupt
acknowledgment before ring reads and keep the coherent-client refusal at
pool allocation so the map rung remains available.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Expose CE acknowledgment, tunable group completion and locked mismatches
before scratch release, and log a validated nonzero firmware acknowledgment.
Clarify that native FREE is enabled only by the pinned H16 profile while
the generic transport retains its conservative default.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Brings in the shared transport (#706), the H16 loading providers (#699),
the T6040 test DT (#705) and the staged cold-boot backend (t-3870).
The task-stream gate now requires the fixed H16 prologue, u32 1 followed
by twelve zero bytes, but the synthetic HWX fixture still started its
stream with sixteen zero bytes. Every suite case that validates the
fixture therefore failed, including the load-plan and resource cases.

Write the prologue word and move the fixture builder into a shared test
header so the program suite can build the same containers.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
EXECUTE descriptor indices are local to the descriptor class, matching
the separate input and output request arrays. The add-128 program has an
input and the output that both use index 0. The resource check refused
any repeated index, so such a program could never be described.

Refuse a repeated index only within one class. Cover the add-128 shape
of input, output and input with indices 0, 0 and 1.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Add the program lifecycle on the shared firmware transport, following the
H16 cold-boot facts (program load and lifecycle, round 5 slot sources) and
the SPEC-r4 EXECUTE layout.

LOAD_PROGRAM is 0x1c0 bytes with nine 0x30 slots from one table. Slot 0 is
the host-built generic section (index 1002), slot 1 the HWX kernel section
when present (1007, class 3), slot 2 the task stream (1006), slots 3, 4
and 6 the operation, procedure and text-property sections (1003 to 1005).
The request carries 0xffffffff at +0x1b8 and the reply must echo every
slot and replace that word. CREATE_PROCESS is 0x258 bytes with a zero
body, which the reply must keep. Program and process ID 0 are valid.
TERMINATE (0x10) and UNLOAD (0xc) close a program.

EXECUTE resources come from a kernel-owned map, with the class from the
resource usage mapping and an index local to that class. The linear program
descriptor's mcache tuple adds one custom-BAR record. Each request sends
0x002d with 0, keeps its UUID pending before publishing on channel 7,
completes only on a state-1 0x0300 report that matches the UUID and the
program, process and procedure IDs, then sends 0x002d with 1 and issues a
DMA read barrier. Failure reports, mismatched IDs, malformed reports and
timeouts quarantine the session with every mapping retained.

A firmware session accepts one program lifetime. On this firmware family
the first EXECUTE of a newly loaded program writes into the previous
program's output buffer, so a second LOAD fails with -EBUSY, also after
UNLOAD. The transport now refuses to publish secure-mode commands 0x27,
0x28, 0x34 and exclave commands 0x30, 0x31. Sequences that cannot fit the
monotonic command arena are refused before the first command.

KUnit drives a synthetic firmware through two executions and teardown,
and covers add-128 and kernel-section slot packing, reply refusals, the
deny list, the custom-BAR record, the second-LOAD refusal, mismatched
UUIDs and IDs, state-2 quarantine and arena exhaustion.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Extend the staged H16 opt-in with h16_stage=program. After the startup
commands it opens one program session and registers an accel node with
ABI version 3 and the GET_CAPS, H16_LOAD and H16_EXEC ioctls. Without
that stage there is no node, so the default refusal and the H17 refusal
are unchanged.

H16_LOAD imports the original HWX once, validates it, places each region
and each host-built section in its own private mapping in a 1 GiB window
above the SHAREDMALLOC range, then loads the program and creates its
process. Only the output surface and scratch are device-writable. The
program belongs to the file and is torn down when the file closes.
H16_EXEC copies inputs in, poisons the output mapping, runs one request
and copies the output out. Userspace never passes DVAs, firmware IDs,
resource indices or commands, and a second load in one firmware boot is
refused with a log line that asks for a reboot.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Add h16-run.py. It checks the add-128 HWX identity, finds the ABI 3 H16
accel node, loads the program with its four host-built sections, and runs
it twice. The output is poisoned before each run and must equal
expected.bin afterwards. Without the program stage it reports a skip.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
…d preloads

The H16 bring-up stages were selected with the h16_stage module
parameter. Module parameters are not allowed on shipping paths, and a
parameter fixed at load time forced a reboot between the boot, startup
and program rungs.

Bind a valid H16 device idle, with no power or MMIO access, and drive
the stages from <debugfs>/ane-h16/<device>/stage instead. A write runs
every stage that has not run yet up to the one written, so one firmware
session can go from preflight to the program stage one rung at a time.
A pre-RUN failure rolls back as before. A stage at or below the
completed one is refused.

The preload check looked the TEXT and DATA nodes up in the boot-time
reserved memory table, which covers only nodes present at early boot.
Take the range from the node itself and refuse it when any part is
Linux System RAM. That holds for a no-map reservation made at boot and
for a range the loader left outside the memory map, and it is a direct
check that Linux never owns the firmware memory.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
EXECUTE descriptors name each buffer by its ID in the generic section
that LOAD installs. The driver used an index local to the class instead,
so a firmware that resolves the ID against the loaded program would not
find the add-128 buffers.

Take the IDs from the generic section. Entry k describes surface k in
HWX surface order, and the kernel refuses a section whose count,
direction, type or size disagrees with the program, that repeats an ID
or that uses the reserved IDs 2 and 3.

Every captured macOS load populated LOAD slot 1 with at least 0x4000
bytes at a sub-page DVA, which is the __TEXT,__const section of a
program without a kernel section. Send the constants there when the
program has no __KERN_0 section instead of leaving the slot empty.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
A /bits/ 64 prefix applies only to the cell list that follows it, so the
second to fourth apple,dapf-ranges pairs were stored as 32-bit cells and
lost their upper bits. The driver reads eight 64-bit values and would
refuse the property. Prefix every pair.

The staged H16 backend maps SID 0 only and refuses a node that lists
more streams, so the J616s ANE test tree now lists SID 0 on the three
DARTs. The shipping node keeps both streams and stays disabled.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The H16 backend knew one firmware build, 27.0 (26A428). Its preload
identity rule (the patchbay records, the range the bootloader rewrites
and the pinned digest) was code in ane_h16_preload.c, and the platform
pointed straight at that build. Supporting another iBoot preload, such
as the 26.6.2 firmware the J616s Linux volume boots with, would have
meant another code path.

Describe each supported build as data instead: a profile names the
build, gives its preload rule and the boot values to use. The preload
module now hashes a snapshot by whatever rule it is given and checks
the rule shape first, so a malformed table entry is -EINVAL and never a
mismatch. The backend tries every profile in turn, selects the build
whose rule matches, and only then takes the firmware DVA layout from
it. When a layout matches but its digest is not pinned, it logs the
digest so a new build can be added from one boot log. Adding a build is
a new table entry.

KUnit covers rule shape, selection between two patchbay layouts and the
shape of every shipped profile.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The HWX validator matched the n-th tensor descriptor to the n-th surface
segment and required the only output to be the last segment. The pinned
add-128 program does not look like that: its segments are x (input), y
(output) and z (input), in buffer-reference order, while its tensors
list x, z and y, inputs first. The validator refused it with -ENOEXEC,
so H16_LOAD could never accept the program the J616s first inference
uses.

Keep the surfaces in segment order, which is the order the program
descriptor binds BAR slots in, and pair each one with the single tensor
whose short name is the name its buffer reference gives. Require inputs
and exactly one output in any order.

Checked against the real add-128 HWX (SHA-256 231f54c2...), which now
validates with surfaces x, y, z. KUnit adds tensors out of segment
order, an output segment first, a reference naming no tensor and a
program with no output.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
H16_LOAD takes four host-built sections next to the HWX, and h16-run.py
expected them as files with no tool to make them. Add h16-sections.py,
which derives them from the HWX alone: a generic section naming every
surface (output ID 4, inputs 5 and up, in segment order), one operation
record binding each BAR slot the task stream references to the surface
or kernel at the program descriptor base for that slot, the fixed
procedure form, and text properties with one block per task.

The layouts follow libane (omarchy-ane, MIT, by Joshua Warren) and the
H16/H17 program facts. For add-128 it gives IDs x=5, y=4, z=6 and BAR
refs 6->5, 7->4, 8->6, the binding the H17 add ran exact with.

h16-run.py now builds the sections itself unless --sections is given.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The program KUnit suite only loaded synthetic HWX images that the test
builds itself. Add the pinned add-128 program and the four sections
h16-sections.py builds for it, stored as runs of nonzero bytes, and load
them through ane_h16_session_load() against the fake firmware.

The test checks the HWX digest, the nine LOAD slots (constants in slot 1
as class 3, the 0x130-byte task stream in slot 2), that only the output
is device-writable, and that two EXECUTEs name x, y and z by IDs 5, 4
and 6 with classes 0, 1 and 0 at their mapped DVAs.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Every command was copied into a monotonic 256 KiB pool arena that was
never recycled before reset. With an EXECUTE sequence of about 3.3 KiB
(two gates and the request), a firmware boot ran out after roughly 75
requests, enough for a first inference but not for benchmarks.

Add ane_fw_transport_mark() and ane_fw_transport_release(). Release
gives back the arena above a mark, and refuses while any channel still
has a command in flight. The H16 session marks the arena before each
EXECUTE sequence and releases it once the completion report and the
closing gate reply have both arrived, when the firmware no longer reads
those commands. LOAD and CREATE keep their space, and a failed request
keeps its space too.

KUnit runs 1000 requests on one program with the arena back at the same
mark after each, and checks the refusals.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The binding required six iommus entries on T6041: stream 0 of the three
ANE DARTs, then stream 11 of each, which the ANE MPM uses. The firmware
drives the MPM stream itself and the host never maps through it, and
listing it puts it in the host's IOMMU group, so the H16 driver refuses
a node that lists it.

Describe stream 0 of each DART on every SoC, three entries. Also give
every pair in the T6041 example apple,dapf-ranges its own /bits/ 64, as
the property is a 64-bit matrix.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The ANE DARTs and the ANE could not probe even when enabled: their power
domains (ane_sys, and under it ane_mpm, ane_cpu, ane_td and ane_base)
and the two fabric domains above ane_sys (fab4_soc, fab5_soc) were left
disabled, because no Linux consumer existed. The H16 backend is that
consumer now, and the J616s bring-up showed the DARTs bind once the
seven providers are registered. fab4_soc and fab5_soc stay always-on.

Enable the seven power-state providers, give the ANE stream 0 of each
DART only, and drop the now redundant iommus override from the test
DTS. The DARTs keep apple,dma-range <0 0 0x400 0> so the firmware
identity MMIO pages fit the aperture, and the DAPF ranges and firmware
IOVAs stay as SoC data. The ANE and its DARTs stay disabled until the
H16 path is proven on hardware. aura fills the preload reservations and
firmware IOVAs at boot, so enabling them then is a status change.

dt_binding_check and dtbs_check are clean for t6040-j616s,
t6040-j616s-tether and t6040-j616s-ane-test.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
@RyanTheTide RyanTheTide changed the title accel/ane: H16 program stage, debugfs bring-up control and J616s DT fixes ANE: H16 cold boot and first inference Nov 1, 2026
The J616s Linux volume boots with the 26.6.2 (25G83) ANE firmware,
RTKit-3255.160.4. Its iBoot preload has the same 42 patchbay records and
the same boot-rewritten tunables range as 26A428, each 0x3f80 bytes later
in DATA, and the same cold boot values, so the profile shares the 26A428
build data and only adds the preload rule.

The digest is the normalized SHA-256 of the live J616s preload. With it
the driver accepts the preload on the J616s ("H16 preload identity H16
26.6.2 25G83 accepted").

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The J616s ANE DART node has one DAPF bank, the reg entry the ADT tags
FPAD at DART0 + 0x10000. The driver assumed a bank above each of the
three translation instances and read records at DART1 + 0x10000 and
DART2 + 0x10000 too. Those addresses are outside every ADT reg entry,
and once the DART revision check let preflight get that far, the read
above DART1 reset the machine with an SError.

Describe every DART and DAPF offset the stages touch in a table keyed by
the DART revision (PARAMS3 bits 15:0), with one entry for the T6041
revision 0x0202, and give the platform the location of the single bank.
Preflight reads PARAMS3 of the three instances first and refuses a
revision without a table before anything else in the DARTs or the bank
is touched. Only the bank's own window is mapped. The protect and lock
words are read on the owning instance only, record bounds are read as
64-bit words, and an end is compared without its low two bits, which a
t8110 bank does not read back.

On this revision the loader programs the bank from the ADT, so the
driver only reads it. Preflight reports how many records match, and map
refuses before any write unless all four match apple,dapf-ranges. The
kernel write path stays behind a table field for a revision that needs
it, with KUnit coverage. The DART and DAPF offsets leave the firmware
build data, since they are not properties of a firmware build.

KUnit checks that no DART offset outside the selected table and no DAPF
access outside the four records happens, and covers the loader-owned,
kernel-owned and unknown-revision cases.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
A preflight on the J616s read the single ANE DAPF bank and found none of
its four records programmed. Every r0 is 0, and the bounds and enables
hold power-on values. iBoot leaves the bank alone, and since the bank is
in the ANE_SYS domain it keeps nothing across a power-down. Linux genpd
and the driver's own pre-RUN rollback both power that domain off, so
only the driver that powers it up can program the bank in time, as on
T8140.

Drop the loader-owned mode. At map the driver writes every record that
differs from apple,dapf-ranges, in the order the t8110 bank takes them
(r4, start, end, r0, r20), with r0, r4 and r20 from the revision table
(the ADT dapf-instance values, 0x31, 0 and 1 on T6041), and reads each
record back. A record that does not read back fails map before RUN. A
bank whose lock word has bit 2 set is still refused before any write.
Preflight still only reads.

KUnit starts from an unprogrammed bank and checks the write order and
values, that only differing records are written, a read-back failure
and the locked cases.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The DRM core refuses to open a file whose file_operations lack
FOP_UNSIGNED_OFFSET. drm_open_helper() warns and returns -EINVAL. The
H16 accel driver lists its fops by hand, without DRM_ACCEL_FOPS because
it has no GEM objects and must not route mmap to drm_gem_mmap(), and it
missed the flag. On the J616s the program stage registered the node and
then every open() of /dev/accel/accel0 failed.

Add the flag. Add a KUnit suite that registers the real driver on a mock
device and opens the node through its own fops, the way
accel_stub_open() hands it over. Without the flag it hits the same
drm_open_helper() warning as the J616s did. The suite also checks that
the fops have no mmap.

h16-run.py treated an accel node it could not open as a missing node
and skipped. Report the open() error and fail instead.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
@RyanTheTide

Copy link
Copy Markdown
Author

First exact inference on the J616s (M4 Pro), from this branch at 992dbda.

Kernel 7.3.0-aurora-fleet14c with the ANE modules built from this branch, ANE enabled through a runtime overlay (no boot chain change). Every stage passed in one go:

  • preflight: preload identity "H16 26.6.2 25G83" accepted, power words on, CPU control 0 and RVBAR 0x10000000001, DART revision 0x0202, no faults
  • map: preload TEXT/DATA and the four identity pages mapped, DAPF bank written and read back (4/4; iBoot leaves it unprogrammed)
  • boot: CE ack 3, tunables applied, READY 0x08042006, requests 8 / 0xc440 / 0xf00000 / 0, channel table at 0x100003d0000 validated
  • startup: acks 0 for 0x0004, 0x0000, 0x0003 and 0x0022, CONFIG_GET tick 0x16e3600 (24 MHz)
  • program: accel node registered (ABI 3)
  • run: the pinned add-128 HWX loaded (3 surfaces of 32 KiB), executed twice, and both runs matched the CPU reference exactly, 16384/16384 fp16 values, output SHA-256 d113d9ae6734b7a7e233458229f64f382da99a3f93868eef3abb06a29f7f4ce8. LOAD, CREATE, TERMINATE and UNLOAD all completed.

Still to do before merge: the shipping DT keeps the ANE disabled until the Stage 2 loader carries the preload handoff, and the one-program-per-boot limit gets lifted next.

🤖 Generated with Claude Code

Tasks in an H16 task stream start 16-byte aligned. The validator also
required the last task to be padded out to 16 bytes inside the section.
Compiled linked programs end the __text section right after the last task,
so a 28-task MobileNetV1 stream of 0x305c bytes was refused. The firmware
reads the task count and the stream length from the program descriptor,
and the same streams run on the J700.

Pad the last task only up to the end of the stream. Any bytes between it
and the end must still be zero.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
A linked program keeps its intermediates in DRAM scratch: the HWX has a
__DATA,__bss segment, the program descriptor is the 0x8c0 form and every
task reaches the scratch through BAR slot 3. The validator already
accepted that form and the loader mapped the segment, but nothing told
the firmware where the scratch was.

The firmware learns it the way it learns a surface. The generic section
gets one more entry, last, for the scratch: its own ID, input usage and
the segment size. The operation section binds slot 3 to that ID, and every
EXECUTE carries one more descriptor with the scratch mapping. Validate
that entry against the segment and send the descriptor from the kernel.
The scratch stays kernel memory, zeroed and device-writable, and is never
a userspace surface. h16-sections.py writes the entry and the slot 3
binding.

Programs also no longer need one firmware boot each. A LOAD is refused
only while another program is still loaded, and accepted once it closed.
Firmware reuses program IDs, so the session numbers its programs itself.
The first EXECUTE after the engine last ran a different program sets bit 0
of the request flags word (+0x30). That makes the firmware queue the job,
which installs the new program's BAR context, instead of taking the
direct path, which reuses the previous program's. On the J700 every
program switch with the bit set was exact and every switch without it
after a direct-path job was stale. Later requests of the same program
keep the direct path.

Add h16-mobilenet.py, which loads a linked MobileNetV1 224 package once
and runs one EXECUTE per image, checking top-1 against the package's
references.

KUnit covers the scratch entry (accepted, missing, output-like, short,
duplicate ID), the scratch descriptor in EXECUTE, a second program after
the first closed, the refusal while one is loaded, and the flag on the
first request after a switch only.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
Resolve the ANE Kconfig help, Makefile and documentation against the
H14 reset and lifetime work: keep both test sets and the H14 restart
text, and drop the old note that H14 reset needed its own test.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The H16 firmware span is config_span bytes from the TEXT DVA. The
bootloader preload covers TEXT and DATA only, and the firmware keeps
runtime heap state in the rest of the span, 0x14000 bytes past the end
of DATA on the 26.6.2 build. Larger programs such as MobileNetV1 write
there and fault on the DART when nothing is mapped.

Allocate that remainder zeroed and map it device-writable in the map
stage, right after the preload, and keep it for the life of the boot
like the other boot-owned mappings.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
The first EXECUTE after boot went down the firmware's direct submission
path. That path reuses the reserved direct network context and the
per-priority L2-spill mapping, and only the queued EXELOOP path sets
them up. add-128 never spills and passed, but the 28-task MobileNetV1
started cold on the direct path and never sent a terminal report: the
driver gave up after 3 s with -ETIMEDOUT (logged as "EXECUTE failed
6e"). Every job of the research harness that ran MobileNet, including
the first one after boot, set the request flag.

Set +0x30 bit 0 whenever the engine did not last run this program,
which now includes a cold engine, and keep the direct path for the
requests that follow.

A failed EXECUTE now also logs the step it stopped at (opening gate,
channel-7 ack, report wait, closing gate), the request flags, how many
reports arrived for its UUID with the last state, and how many other
reports arrived, so the next hang names what the firmware got to.

KUnit: the session test expects the first request queued; a new case
checks the failure trace for a request that starts and never ends.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
… log on a stall

On the J616s the first MobileNet EXECUTE was acknowledged on channel 7
and then nothing came back: no start report, no fault, no crash line.
Two changes.

Each EXECUTE descriptor now carries the size of the surface's HWX
section instead of the tensor span. They differed only for a padded
input (MobileNet: 0x49800 of 0x4c000), which the H16 path had never
sent. The J700 path that ran MobileNet always described the whole
section. Userspace still sees and copies the span.

A failed EXECUTE now also prints the non-zero rows of the command it
sent and of the channel-7 reply, then every non-empty record of the
firmware's text log (the SHAREDMALLOC buffer tagged LOG, 0x80-byte
records). The transport keeps each shared allocation's tag so the log
can be found. The session is quarantined after a failure, so this runs
at most once per program.

KUnit: log record decoding, finding allocations by tag, the descriptor
size, and a stalled request without a log buffer.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
…f the preload

Once the bootloader has filled and enabled the TEXT and DATA preload
reservations, run the staged bring-up through the program stage from a
work item after probe, so the accel node exists on every boot without a
debugfs write. The debugfs stage file stays for reading the state and,
before the boot bring-up starts, for diagnosis.

While the reservations are still the disabled placeholders of the board
DT (an older loader), refuse with -ENODEV and one informational line
instead of an error.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
MobileNetV1 runs top-1 16/16 on the J616s through the H16 driver. Enable
the ANE and its three DARTs on the board. The engine stays idle until
the loader fills and enables the two preload reservations, so an older
loader leaves it refused with one informational line. Drop the separate
ane-test tree, which only did this, and update the documentation.

Assisted-by: LLM
Signed-off-by: Ryan Murray <ryan@aurorasilicon.org>
@RyanTheTide

Copy link
Copy Markdown
Author

Hardware results on the J616s (M4 Pro), 2026-11-02:

  • fleet14f, kit v4.3 (boot d9ef5ba9): H16G MobileNetV1 224 top-1 16/16 against the CPU reference, then add-128 as the second program of the same boot, 2 x 16384/16384 exact. Host EXECUTE latency p50 6.56 ms, p90 7.67 ms.
  • fleet14f, this head a53bbd9 (boot 45ee581a): with the preload reservations present, the engine came up by itself to the program stage at probe, no debugfs writes. MobileNet top-1 16/16, add-128 exact. No DART faults.

What made MobileNet work: the zeroed 0x14000 firmware heap tail mapped after DATA (7040ae9), the first EXECUTE of every program on the queued path (df11bfb), and whole input sections in the EXECUTE descriptor (916532b).

The board DT now enables the ANE and its DARTs (a53bbd9). Until the loader fills the two preload reservations the device is refused with one informational line (085586e), so this is safe with the current Stage 2.

KUnit 159/159 (x86_64) on the merged tree, arm64 W=1 module build clean, j616s and j616s-tether DTBs build.

🤖 Generated with Claude Code

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.

1 participant