feat: AWS Neuron (Trainium/Inferentia) device backend - #417
Open
hoyajigi wants to merge 5 commits into
Open
Conversation
Enumerate Neuron devices with `neuron-ls --json-output` and emit one GpuInfo row per NeuronCore, with per-core device HBM usage read from the driver sysfs tree and best-effort per-core utilization from a `neuron-monitor` one-shot. Built against a trn1.2xlarge (1 Trainium device, 2 NeuronCores, aws-neuronx-dkms 2.26.5.0, aws-neuronx-tools 2.28.23.0). - Detection: /dev/neuron0 (mode 0666, no group gate), else PCI ID 1d0f:7164 via `lspci -nn`. Whole-module Linux gate, no cargo feature. - Both CLIs are called by absolute path under /opt/aws/neuron/bin, which only the DLAMI profile scripts put on PATH; under sudo, systemd, or a container entrypoint an unqualified call exits 127. - `neuron-monitor` has no one-shot flag but emits a complete NDJSON record at t~=0, so the child is killed once the first record is read rather than adding a background streaming subsystem. Its core keys are a global flat index (nd_idx * cores_per_device + nc_idx), per AWS own neuron-monitor-device-view.py. - Absence contract: utilization and power report GPU_METRIC_UNAVAILABLE, temperature and frequency report 0, and no device yields no rows. Trainium exposes no temperature anywhere; sysfs stats/power/utilization carries three unlabelled floats that read 0.00 on idle hardware, so the raw line is kept in `detail` instead of being guessed at.
Add the vendor exporter for AWS Neuron rows. Shared all_smi_npu_* series come from CommonNpuExporter because the reader tags every row as "NPU"; this adds device identity, driver version, and NeuronCore topology. The exporter is appended to the end of EXPORTER_POOL, never inserted: find_exporter addresses the pool by hardcoded index, so a mid-list insert would silently re-route another vendor. A new test pins every vendor index, including the Neuron slot.
Four read-only checks: /dev/neuron* nodes, the neuron kernel module and its version, the driver sysfs tree the reader reads memory from, and the aws-neuronx-tools binaries at their absolute paths. doctor is the only vendor-specific diagnostic surface in the project, so the PATH trap that makes the CLIs exit 127 outside a DLAMI login shell is reported here. Also report AWS Neuron in the platform.hardware check.
`--platform neuron` (also trainium / inferentia) serves one row per NeuronCore with the all_smi_neuron_* topology series and a host CPU profile taken from the captured trn1.2xlarge (Xeon Platinum 8375C). The mock deliberately omits temperature, power, and frequency: Trainium exposes none of the three and the real exporter omits those series, so mocking them would validate consumers against data the hardware never produces.
Add AWS Neuron to the supported-device lists in README, the man page, and the crate docs, register the neuron.* doctor check IDs, and suggest aws-neuronx-tools from the Debian packaging.
hoyajigi
marked this pull request as ready for review
September 12, 2026 12:09
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.
feat: AWS Neuron (Trainium / Inferentia) device backend
Adds a Neuron backend to
all-smi: device reader, Prometheus exporter,doctorchecks, and a mock platform. Branch:feature/aws-neuron-reader.Hardware this was built against
Everything below was measured on a real instance, not inferred from docs:
trn1.2xlargeNDv2/NCv2,device_name = Trainium1)aws-neuronx-dkms2.26.5.0 (neuronmodule,/sys/module/neuron/version)aws-neuronx-tools2.28.23.0 (neuron-ls,neuron-monitor2.28.23.0)The full capture (63 artifacts: command output, exit codes, every sysfs
leaf and its value, non-root and device-absent behaviour) lives outside
this repo and is the source for every claim in this description.
What is implemented
One
GpuInforow per NeuronCore,device_type = "NPU"(which is whatmakes
CommonNpuExporteremit the sharedall_smi_npu_*family for free).GpuInfofielduuidneuron-<serial>-nc<flat core>; falls back to the PCI BDF when sysfs has no serialnameinfo/architecture/device_name→"AWS Trainium1"; falls back toneuron-monitor'sneuron_device_type, then"AWS Neuron Device"total_memoryneuron-lsmemory_size÷nc_count(34359738368 B ÷ 2 = 16 GiB)used_memoryneuron_core<N>/stats/memory_usage/device_mem/presentutilizationneuron-monitorneuroncore_counters.neuroncores_in_use[<flat idx>].neuroncore_utilization— best effortnuma_node_idneuron-lsnuma_nodestring;"-1"canonicalised toNonetemperature0) — no source existsfrequency0) — no source existspower_consumption-1.0) — see belowdetailarch_type,device_name,instance_type, corearch_type,connected_to(when non-null), driver version, raw power line,utilization_source,memory_usage_source, andneuron-monitorhardware infoNone/ empty, as infuriosa.rsandtenstorrent.rsExporter emits
all_smi_neuron_device_info,_serial_info,_arch_info,_instance_info,_driver_info,_device_index,_core_index,_core_count,_device_memory_bytes. Every series is conditional on itsdetailkey being present, so an unsourced value is absent from theexposition rather than exported as 0.
doctoraddsneuron.dev_node,neuron.driver,neuron.sysfs,neuron.tools. The mock server gains--platform neuron(aliasestrainium,inferentia).Design decisions worth reviewing
neuron-ls+ sysfs, notneuron-monitor, for everything static.neuron-ls --json-outputis a natural one-shot (exits 0) and per-corememory is in sysfs, world-readable and populated with no workload
attached. Only per-core utilization needs
neuron-monitor.neuron-monitoris shelled out one-shot, not backgrounded. It has no--once/--count/--periodflag, but it emits a complete NDJSONrecord immediately at t≈0 (then every 5.000 s — measured: 02:41:18.673,
:23.678, :28.678). The reader spawns it, reads the first line with a 2 s
deadline on a helper thread, then kills the child. A single failure sets
a process-global flag so a host without the tool never pays the timeout
again. This deliberately avoids a background-process subsystem; the
existing sanctioned command helpers could not be used here because
run_command_with_timeoutdiscards stdout when it kills a child, andneuron-monitornever exits on its own.Core keys are a global flat index. Per AWS's own shipped
neuron-monitor-device-view.py:nc_idx = nd_idx * neuroncore_per_device_count + nc_idx_counter, thenneuroncores_in_use[str(nc_idx)]. On a 16-devicetrn1.32xlarge, device 3core 1 is key
"7". The reader computes the same expression rather thanassuming device-local keys.
Binaries by absolute path.
/opt/aws/neuron/binis added toPATHonly by the DLAMI profile scripts. Measured: an unqualified
neuron-lsunder
sudo -uexits 127. Both the reader and the doctor check use/opt/aws/neuron/bin/neuron-lsand.../neuron-monitor.Detection.
/dev/neuron0(mode 0666, world rw — no group gate;every tool works fully as
nobody, verified) elselspci -nncontaining1d0f:7164(Amazon.com, Inc. NeuronDevice (Trainium)). No CLI probe inthe detector, because the PATH trap above would make it a false negative.
Platform gate. Whole-module
#[cfg(target_os = "linux")]at themoddeclaration and every use site, like
tenstorrent. Notarget_archgate(no reader has one) and no cargo feature (this repo's rule is that a
feature exists only for an optional external crate; this links nothing).
Absence-contract choices (issue #325)
not in
neuron-ls, not inneuron-monitor, not in sysfs (checked everyleaf;
lm-sensorsis not even installed). Reported as0, theunavailable marker for
temperature.-1.0), deliberately. A field does exist:/sys/devices/virtual/neuron_device/neuron0/stats/power/utilization=POWER_STATUS_VALID,1789180860,0.00,0.00,0.00. Three unlabelled floats,all
0.00on real idle hardware, and a Trainium device plainly does notdraw 0 W. Rather than guess which float is watts, the raw line is kept
in
detail["power_utilization_raw"]and the reading is marked absent.This is the most reviewable call in the PR — if someone can confirm
the field order against a loaded device, promoting it to a real reading
is a small change.
neuron_runtime_data: []the map is empty and the row reports absent,never 0 %.
and clean, one logfmt line on stderr. "Empty stdout + non-zero rc" is
therefore the absence test. A bad flag prints its message on stdout
with exit 1, and the non-zero rc catches that too. Missing tools: 127.
GpuInfo(u64), so an unreadable sysfsleaf yields
0like every other reader.detail["memory_usage_source"]distinguishes
sysfsfromunavailableso the 0 is attributable.Judgement calls a reviewer might overrule
utilization are both per-core in the driver and in AWS's own tooling.
The cost is item 2.
neuron-lsreports HBM per device; every consumer oftotal_memorysums rows, so charging the full device size to each core would double
the host's reported memory. The even split is a derivation, not a
reading. The undivided value is preserved in
detail["device_memory_bytes"]and exported asall_smi_neuron_device_memory_bytes.neuron-monitoris spawned on every refresh tick when the binaryexists (~2 process spawns per tick total, in line with the Furiosa
reader's 3). An alternative is to skip it unless
neuron-lsreports anon-empty
neuron_processes, which would be cheaper but relies on anarray whose element shape is unverified.
new_commandis used directly forneuron-monitorinstead ofexecute_command_default, for the stdout-on-kill reason above.neuron-lsgoes throughexecute_command_defaultas normal. Nostd::process::Command::newanywhere.privileges.dev_neurondoctor check, unlikeprivileges.dev_tenstorrent./dev/neuron0is world-rw with no groupgate, so there is no privilege question to report, and
neuron.dev_nodealready covers presence.get_*_status_message/*_notification_shown), unlike Tenstorrent and TPU. Neuron has no"initializing"/"failed" state to surface; add one later if a real
failure mode turns up.
detailkeys are snake_case (pci_bdf,serial_number),following
furiosa.rs. Some other readers use Title Case keys; therepo is not consistent and this picked the NPU-family convention.
DeviceType::AwsNeuronNpuwas added to the publicclient.rsenum. Additive, but it is public API.
the hardware, but it makes the Neuron mock the only one whose metric
set is a strict subset of the others.
What could NOT be verified
flat_core_indeximplements AWS's own formula and is unit-tested(device 3 core 1 → 7), but no multi-device output was ever observed.
Whether
neuroncore_idsis global or device-local on a 16-device box isunknown; the reader does not depend on it except as a fallback for a
missing
nc_count.connected_totopology.nullon a single-device instance. Itsshape on
trn1.32xlargeis unknown, so it is carried through as opaqueJSON (stringified into
detail) rather than modelled. It is not fedinto the topology tab.
neuron_runtime_datawas always[]. The populated-record test literalis synthesised from AWS's own shipped frontends
(
neuron-monitor-prometheus.py,neuron-monitor-device-view.py), whichare authoritative for the field names but are not captured output. It is
labelled as such in the test. The empty-record literal is verbatim
capture.
neuron_processeselement shape. Always[]with no workloadattached, so
get_process_infoparsespid/command/nameleniently and drops any entry without a PID rather than emitting PID 0.
It may simply return nothing on a busy host; that degrades to "no
process rows", never to wrong rows.
serial_number(
9ff5434815c8bd80) and the PCI BDF both look stable and the UUID iscomposed from them, but the instance was never rebooted. No comment in
the code claims stability.
NDv2) hardware was available. Inf1/Inf2use the same driver, tools, and sysfs layout, and the code paths are
generic, but no Inferentia device was tested. The
1d0f:7164PCI ID inthe fallback detector is Trainium's; an Inferentia-only host with no
/dev/neuron0would not be detected.logical_neuroncore_config> 1 (LNC / "logical NeuronCore" mode ontrn2). Observed as
1. It is recorded indetailbut does not affectrow expansion.
Validation
Run on macOS (aarch64-apple-darwin) and on a Linux box, because the whole
Neuron module is behind
#[cfg(target_os = "linux")]and therefore is notcompiled at all on macOS.
cargo fmt --all -- --checkcargo clippy --all-targets -- -D warningscargo testcargo build --no-default-features --libAdditionally on Linux, because
src/mocksits behind the non-defaultmockfeature and neither plaincargo testnor CI'scargo clippy --all-targetsbuilds it:cargo clippy --all-targets --features mock -- -D warnings— exit 0(this is what caught a non-exhaustive
matchonPlatformTypeinsrc/mock/generator.rs)cargo test --features mock --bin all-smi-mock-server— 65 passedCaveat on the Linux run:
cargo testthere was invoked with--skip test_ensure_sudo_permissions. Those two tests callensure_sudo_permissions(), which spawnssudo -vandexit(1)s when itfails; they abort the harness on any host without an interactive TTY.
Verified pre-existing by running them from a pristine
mainon the samebox:
sudo: A terminal is required to authenticate, same failure. They donot run on macOS (one is
cfg(not(target_os = "macos")), the other isignored there), which is why baselinemainis green locally.Tests added (14 in the lib, 2 in the mock bin)
device::readers::neuron:neuron-lsarray parses;memory_sizeread asbare bytes (34359738368 → 32 GiB, not multiplied);
numa_node: "-1"string parses and canonicalises to absent;
connected_to: nulltolerated;empty stdout yields an empty list (not a row of zeros); per-core rows split
device memory and mark every unsourced metric absent; monitor record with
no runtime reports no utilization; monitor record with a runtime keys cores
by global flat index; flat index arithmetic; core-count fallback;
device-name fallback chain; processes without a PID dropped.
api::metrics::npu: every vendor's hardcoded pool index pinned (Gaudi,Rebellions, Furiosa, TPU, Tenstorrent, Neuron, plus pool length); Neuron
rows route to the Neuron exporter.
doctor::checks::neuron: ids namespaced and every check total on a hostwith no Neuron hardware.
mock::templates::neuron: cores numbered globally and grouped by device;temperature/power/frequency series absent.
Real captured output is embedded as string literals (house pattern, cf.
src/device/hlsmi/parser.rs), with the one synthesised literal explicitlymarked.