Skip to content

feat(accelerator): add an AWS Neuron (Trainium/Inferentia) plugin - #14580

Open
hoyajigi wants to merge 6 commits into
mainfrom
feature/aws-neuron-accelerator
Open

hoyajigi wants to merge 6 commits into
mainfrom
feature/aws-neuron-accelerator

Conversation

@hoyajigi

@hoyajigi hoyajigi commented Sep 12, 2026

Copy link
Copy Markdown
Member

Hardware verification status: everything that can be checked without an
accelerator is checked — tests, mypy, lint and the alembic migration all run
and pass. What remains unverified is hardware-bound: the probe instance
had exactly one Neuron device, so multi-device renumbering is pinned by a
synthesised fixture, and no Neuron workload was ever run. Details at the
bottom. Reviewers who have a trn1.32xlarge or inf2.24xlarge handy can
close that gap quickly.

⚠️ One irreversible decision needs maintainer sign-off: the slot is neuron.core, not neuron.device

The slot name becomes a PK in resource_slot_types and lands in agent_resources / resource_allocations, so changing it later needs a data migration. I chose the NeuronCore on three independent pieces of evidence, all from a real trn1.2xlarge:

  1. AWS's own allocation primitive is the core. NEURON_RT_VISIBLE_CORES is the knob; NEURON_RT_NUM_CORES is explicitly deprecated in its favour (per nccom-test shipped on the Neuron DLAMI).
  2. Every runtime-varying metric is keyed per core. In the captured sysfs tree the device level exposes only static capacity plus driver-owned host memory; memory_usage, status and other_info counters all live under neuron_core{N}/.
  3. sysfs models cores as first-class objects — real nested directories neuron0/neuron_core{0,1}/, each with its own full stat tree and arch_type (NCv2 vs the device's NDv2).

Device identity (bdf, serial_number, device index) is carried as metadata on each core device, not as the key. Core-granular slots aggregate upward to whole devices for free; device-granular could never be subdivided without a migration.

If maintainers prefer neuron.device, the change is mechanical but must happen before this merges.

What this was built against

A real trn1.2xlarge (1 Trainium device, 2 NeuronCores) on the Neuron DLAMI — driver aws-neuronx-dkms 2.26.5.0, tools aws-neuronx-tools 2.28.23.0, PCI 1d0f:7164. Captured neuron-ls --json-output, neuron-monitor, and the complete sysfs tree, and used them as test fixtures.

[{"neuron_device": 0, "bdf": "0000:00:1e.0", "cpu_affinity": "0-7", "numa_node": "-1",
  "connected_to": null, "nc_count": 2, "memory_size": 34359738368,
  "neuroncore_ids": [0, 1], "neuron_processes": []}]

Shape traps handled: neuron_device is an int but numa_node is a string ("-1", clamped to 0 as tenstorrent does); connected_to is null on single-device instances; memory_size is bare bytes (34359738368 = exactly 32 GiB, though the human table prints "32 GB").

Notable implementation choices

  • CLI-absence handling follows rebellions, not tenstorrent/rngd. Those two catch only ImportError. neuron-ls is a CLI, so a missing tool raises FileNotFoundError — and since an agent loads all installed accelerator plugins unless allow-compute-plugins is set, that would abort agent startup on every host without Neuron tooling. This plugin resolves the path, logs, sets enabled = False and returns.
  • Absolute path /opt/aws/neuron/bin/neuron-ls. That directory is injected into PATH only by DLAMI profile scripts; under sudo, systemd or a container entrypoint the bare name is command not found (exit 127). Measured, not assumed.
  • gather_node_measures is implemented from sysfs, not stubbed. Per-core device_mem/host_mem are world-readable and populated with no workload attached, so no vendor daemon is needed. neuron-monitor is deliberately not run: it is a 5-second-cadence stream with no one-shot flag, no existing plugin runs a persistent vendor daemon, and gather_node_measures(ctx) is a per-tick pull. Per-core utilization needs neuron-monitor plus an attached runtime process, so gather_process_measures returns [] for now.
  • NEURON_RT_VISIBLE_CORES is injected via "Env", which I verified is honoured: agent/utils.py:91-111 update_nested_dict deep-merges and extends lists, docker/agent.py:1206 merges plugin args into container_config with no whitelist, and the agent sets its own Env at :1133 before that merge, so plugin entries are appended rather than clobbered. tpu/plugin.py:166-171, ipu:390, cuda_open:327 and mock:600 already rely on this.
    • Caveat: the Kubernetes backend drops it. kubernetes/agent.py:488-496 has generate_docker_args commented out entirely with # TODO: add support for accelerator allocation, so every key is dropped there — not just Env, and not specific to this plugin.
  • resource_slot_types seeding is mandatory, not cosmetic. agent_resources.slot_name has a hard FK to resource_slot_types.slot_name and the manager upserts one row per reported slot on every heartbeat, with no runtime insert path. Both fixtures are updated (kept byte-identical) plus a new alembic migration. Its downgrade is guarded against rows still referenced by agent_resources / resource_allocations, deliberately unlike ccf8ae5c90fe's unguarded delete.
  • Drive-by: the migration also seeds the missing tt-n300.device row. I checked a live production manager DB — all 14 existing rows share one created_at, and tt-n300.device is absent, so a Tenstorrent agent takes an FK violation on heartbeat today. Happy to split this into its own PR if preferred.

Validation

Everything below was run locally and passes:

check result
pants test tests/unit/accelerator/neuron:: ✅ pass
pants check src/ai/backend/accelerator/neuron:: tests/unit/accelerator:: (mypy) ✅ pass — caught and fixed 2 real type errors
ruff check / ruff format --check (repo config) ✅ clean
pants tailor --check, target graph, accelerator/wheel tags ✅ correct
alembic migration against real PostgreSQL 15 ✅ see below

Getting pants test green needed a fix worth calling out: tools/pants-plugins/accelerator_wheels strips every src/ai/backend/* dependency from targets tagged accelerator so each wheel builds standalone, and that stripping also keeps ai.backend.agent out of a test sandbox that only depends on the accelerator lib. This is very likely why no accelerator plugin in the tree has had tests. The test target now names the agent modules explicitly.

The migration was executed, not just written

Against PostgreSQL 15.19 with the full 73-table schema built from the models:

step result
downgrade a1c7e4b93f20 -> 3b6297b1bd75
upgrade 3b6297b1bd75 -> a1c7e4b93f20 ✅ both rows inserted with the expected values
INSERT INTO agent_resources ... 'neuron.core' (what a heartbeat does) ✅ accepted
the same insert with an unseeded slot name ❌ rejected by fk_agent_resources_slot_name_resource_slot_types
guarded downgrade while agent_resources still references neuron.core neuron.core preserved, tt-n300.device removed
downgrade once nothing references it ✅ both rows removed

The fourth row is the point: without this migration an agent reporting neuron.core cannot complete a heartbeat. The same is true of tt-n300.device today, which is why it is seeded here as a drive-by.

What is not verified — read before merging

  • Multi-device renumbering. The probe had exactly one device. The two-device fixture in the tests is synthesized, not captured, so it pins my renumbering logic rather than AWS's real trn1.32xlarge output. Specifically, I assume the runtime numbers container-local cores as (position of device among visible devices) * nc_count + core index. Untested on hardware.
  • connected_to / NeuronLink topology is parsed and stored but used for nothing, so it does not influence allocation.
  • Live per-core utilization and memory. No workload was ever run; device_mem/* read 0 throughout, so the non-zero path is exercised only against synthetic values.
  • NEURON_RT_VISIBLE_CORES actually confining the runtime. I verified the variable reaches the container by reading the merge path; I did not run a Neuron workload to confirm the runtime honours it when the whole device node is mounted (each /dev/neuron{N} carries all of its cores).
  • Pre-existing, not introduced here: agent/agent.py:2752-2754 skips mount_krunner when restarting, so apply_accelerator_allocation never runs and a plugin's whole generate_docker_args output appears to be dropped on kernel restart. Affects all accelerator plugins equally.

@hoyajigi
hoyajigi marked this pull request as ready for review September 12, 2026 12:09
@hoyajigi
hoyajigi requested a review from a team as a code owner September 12, 2026 12:09
Copilot AI balanced review requested due to automatic review settings September 12, 2026 12:09

Copilot AI 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.

🟡 Changes recommended

Core isolation is not enforceable, container accounting is inaccurate for shared devices, and several lifecycle and build-policy issues remain.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds an AWS Neuron accelerator plugin with per-NeuronCore discovery, allocation, metrics, container integration, and manager slot registration.

Changes:

  • Implements Neuron host discovery, sysfs metrics, and Docker allocation.
  • Seeds Neuron and Tenstorrent resource slot metadata.
  • Adds documentation, packaging metadata, and unit tests.
File summaries
File Description
README.md Lists AWS Neuron support.
changes/14580.feature.md Adds the feature changelog.
fixtures/manager/example-resource-slot-types.json Seeds new slot metadata.
src/ai/backend/install/fixtures/example-resource-slot-types.json Mirrors installer slot metadata.
src/ai/backend/accelerator/neuron/__init__.py Exposes package version.
src/ai/backend/accelerator/neuron/BUILD Defines package and wheel targets.
src/ai/backend/accelerator/neuron/README.md Documents design and operation.
src/ai/backend/accelerator/neuron/neuron_api.py Wraps Neuron CLI and sysfs access.
src/ai/backend/accelerator/neuron/plugin.py Implements the accelerator plugin.
src/ai/backend/accelerator/neuron/py.typed Marks the package as typed.
src/ai/backend/accelerator/neuron/types.py Defines NeuronCore devices.
src/ai/backend/manager/models/alembic/versions/a1c7e4b93f20_add_neuron_core_and_tt_n300_resource_slot_types.py Migrates slot definitions.
tests/unit/accelerator/BUILD Adds accelerator test targets.
tests/unit/accelerator/__init__.py Initializes the test package.
tests/unit/accelerator/neuron/BUILD Configures Neuron unit tests.
tests/unit/accelerator/neuron/__init__.py Initializes Neuron tests.
tests/unit/accelerator/neuron/test_neuron_plugin.py Tests discovery, metrics, and container arguments.
Review details
  • Files reviewed: 15/18 changed files
  • Comments generated: 7
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +375 to +378
# A device node carries *all* of its cores, so allocating a subset of a
# device's cores still mounts the whole device node -- the container can
# see sibling cores it was not allocated. NEURON_RT_VISIBLE_CORES below
# is what confines the runtime to the allocated ones.

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.

Please check this review about enforcing core isolation

Comment on lines +389 to +393
if not _device_node_exists(host_path):
# Just skip mounting without raising an error, matching the
# other NPU plugins' behaviour for hot-removed devices.
log.warning("device node {} is missing; not mounting it", host_path)
continue

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.

Please check this review, about partial failure handling when assigning a device

@@ -0,0 +1,40 @@
python_sources(
Comment on lines +288 to +294
for dev in devices:
stats = core_stats[(dev.neuron_device_index, dev.core_index)]
node_path = dev.device_node_path
usage_by_node[node_path] = (
usage_by_node.get(node_path, 0) + stats.device_mem_present
)
capacity_by_node[node_path] = capacity_by_node.get(node_path, 0) + dev.memory_size
Comment on lines +544 to +545
async def update_plugin_config(self, new_plugin_config: Mapping[str, Any]) -> None:
pass
Comment on lines +17 to +20
try:
from ai.backend.agent.resources import get_resource_spec_from_container # type: ignore
except ImportError:
from ai.backend.agent.docker.resources import get_resource_spec_from_container
Comment thread src/ai/backend/accelerator/neuron/plugin.py Outdated
Adds `backend.ai-accelerator-neuron`, registering the `neuron` entry point
under `backendai_accelerator_v21`.

The allocation unit is the NeuronCore (slot `neuron.core`, SlotTypes.COUNT),
not the Neuron device. Three independent pieces of evidence point at the core:
AWS's own allocation primitive is `NEURON_RT_VISIBLE_CORES` (with
`NEURON_RT_NUM_CORES` deprecated in its favour); every runtime-varying metric
the driver exposes is keyed per core while the device level carries only static
capacity; and sysfs models cores as first-class nested objects. Device identity
(`bdf`, `serial_number`, the device index) is kept as metadata on each core
rather than as the allocation key, so cores aggregate upward to whole devices
without a later data migration.

Discovery shells out to `neuron-ls --json-output`, whose payload is a
top-level array with an int `neuron_device`, a *string* `numa_node`, a null
`connected_to` on single-device instances and a `memory_size` in bare bytes.
The device capacity is split evenly across the device's cores.

`neuron-ls` is resolved at /opt/aws/neuron/bin/neuron-ls before $PATH,
because only the DLAMI profile scripts put that directory on PATH and the bare
name is unresolvable under systemd or a container entrypoint. A missing tool or
an unloaded driver logs the reason and leaves the plugin disabled instead of
raising: agents load every installed accelerator plugin unless
`allow-compute-plugins` is set, and the plugin loader calls
`entrypoint.load()` with no exception handling, so an escaping
FileNotFoundError would abort the agent's whole accelerator plugin load.

`gather_node_measures` is implemented from the per-core sysfs memory counters,
which are world-readable and populated with no workload attached, rather than
stubbed. `neuron-monitor` is deliberately not used: it is a streaming
collector with no one-shot mode, so consuming it would mean running a persistent
vendor daemon behind a per-tick pull hook. Per-core utilization, which needs
`neuron-monitor` plus an attached process, is therefore left out of v1.

`generate_docker_args` mounts each allocated device node renumbered to
/dev/neuron0..k-1, sets IPC_LOCK, host IPC and an unlimited memlock for the
runtime's pinned-memory registration, and confines the runtime with
`NEURON_RT_VISIBLE_CORES`. A device node carries all of its cores, so
allocating a subset of a device's cores still mounts the whole node.
…types

`agent_resources.slot_name` carries a hard FK to
`resource_slot_types.slot_name`, and the manager upserts one `agent_resources`
row per reported slot on every agent heartbeat. There is no runtime insert path
into `resource_slot_types`, so a slot type that is not seeded makes the
heartbeat of any agent reporting it fail on the FK. Register `neuron.core` in
the example fixture and in a new migration.

Also adds the missing `tt-n300.device` row as a drive-by fix: the Tenstorrent
n300 plugin has reported that slot since it was added but it was never seeded,
so a Tenstorrent agent takes the same FK violation today. This part is separable
from the Neuron work if a reviewer would rather see it split out.

`display_icon` uses asset names that actually exist under
web/static/resources/icons: `aws` for Neuron, and `npu_generic` for
tt-n300 (no `npu.svg` or `tenstorrent.svg` exists, despite the plugin
declaring `npu` and web/static/resources/device_metadata.json declaring
`tenstorrent`).

The uuids are pinned to the ones the fixture assigns, following `8f21c46a0b73`,
so an upgraded deployment ends up with the same slot identity a fresh install
gets instead of a random one per database. `required` and `enabled` keep their
server defaults, matching every other accelerator slot row.

The downgrade only deletes rows that nothing references, so it does not abort on
a deployment with a live agent, a historical allocation, or a model card /
preset / deployment revision naming either slot. All five tables that carry an
FK onto `resource_slot_types.slot_name` are guarded.
Creates `tests/unit/accelerator/`, which did not exist: no accelerator plugin
in this repo had tests.

`tests/unit/accelerator/neuron/` exercises discovery against the verbatim
`neuron-ls --json-output` payload captured from a trn1.2xlarge, with the
subprocess call monkeypatched so no hardware is needed. It pins the value shapes
that are easy to get wrong (int `neuron_device` vs string `numa_node`, null
`connected_to`, `memory_size` as bare bytes), that one device with
`nc_count: 2` yields two core devices splitting the capacity, that a missing
CLI and a driver-absent host each leave the plugin disabled without raising, and
the container device renumbering and `NEURON_RT_VISIBLE_CORES` value.

Making `pants test` green needed an explicit dependency in the test target:
`tools/pants-plugins/accelerator_wheels` strips every `src/ai/backend/*`
dependency from targets tagged `accelerator` so each wheel can build
standalone. That stripping also keeps `ai.backend.agent` out of a test sandbox
that only depends on the accelerator lib, so the plugin's base classes have to
be named explicitly. This is why no accelerator plugin in the tree has had
tests.
@hoyajigi
hoyajigi force-pushed the feature/aws-neuron-accelerator branch from ce41883 to 08db97f Compare September 12, 2026 12:35
@github-actions github-actions Bot added size:XL 500~ LoC comp:manager Related to Manager component require:db-migration Automatically set when alembic migrations are added or updated labels Sep 12, 2026
Comment on lines +52 to +76
def upgrade() -> None:
# Use exec_driver_sql to avoid sa.text() parsing the JSON colons as bind params.
# `required` and `enabled` keep their server defaults (false / true), matching
# every other accelerator slot row.
conn = op.get_bind()
conn.exec_driver_sql("""
INSERT INTO resource_slot_types
(uuid, slot_name, slot_type, display_name, description,
display_unit, display_icon, number_format, rank)
VALUES
('ef63fa11-609e-4b96-8f90-a08f97a5f04b'::uuid,
'tt-n300.device','count','Tenstorrent n300 Device','Tenstorrent n300',
'n300', 'npu_generic', '{"binary":false,"round_length":0}', 1500),
('7b968b58-f7cc-472d-b191-d4b19f417efd'::uuid,
'neuron.core','count','AWS Neuron Core','AWS Neuron NeuronCore',
'Core', 'aws', '{"binary":false,"round_length":0}', 1600)
ON CONFLICT (slot_name) DO UPDATE SET
slot_type = EXCLUDED.slot_type,
display_name = EXCLUDED.display_name,
description = EXCLUDED.description,
display_unit = EXCLUDED.display_unit,
display_icon = EXCLUDED.display_icon,
number_format = EXCLUDED.number_format,
rank = EXCLUDED.rank
""")

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.

This migration seems to insert the new resource slot type for every site but I don't think it is installed globally

Comment on lines +84 to +97
guards = "\n".join(
f" AND NOT EXISTS ("
f"SELECT 1 FROM {table} r WHERE r.slot_name = resource_slot_types.slot_name)"
for table in _referencing_tables
)
conn = op.get_bind()
conn.execute(
sa.text(f"""
DELETE FROM resource_slot_types
WHERE slot_name = ANY(:names)
{guards}
"""),
{"names": list(_added_slot_names)},
)

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.

Such deleting migration can be dangerous

Comment on lines +17 to +20
try:
from ai.backend.agent.resources import get_resource_spec_from_container # type: ignore
except ImportError:
from ai.backend.agent.docker.resources import get_resource_spec_from_container

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.

Such import error handling is not required anymore

Comment on lines +375 to +378
# A device node carries *all* of its cores, so allocating a subset of a
# device's cores still mounts the whole device node -- the container can
# see sibling cores it was not allocated. NEURON_RT_VISIBLE_CORES below
# is what confines the runtime to the allocated ones.

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.

Please check this review about enforcing core isolation

Comment on lines +389 to +393
if not _device_node_exists(host_path):
# Just skip mounting without raising an error, matching the
# other NPU plugins' behaviour for hot-removed devices.
log.warning("device node {} is missing; not mounting it", host_path)
continue

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.

Please check this review, about partial failure handling when assigning a device

Five points from the review:

- The optional `ai.backend.agent.resources.get_resource_spec_from_container`
  import no longer resolves anywhere in the tree, so the try/except was dead
  and its `# type: ignore` unnecessary. Import from `agent.docker.resources`
  directly, as the IPU plugin already does.
- A missing `/dev/neuron{N}` was skipped when building the container spec.
  Unlike the other NPU plugins this is not safe here: the device has already
  been counted when renumbering, so the NEURON_RT_VISIBLE_CORES indices assume
  it is mounted, and the kernel would start with a reserved core absent and
  the remaining ones addressed under wrong numbers. Raise ResourceError.
- Core isolation cannot be enforced through a device node that carries every
  core of its device. Allocate with AllocationStrategy.FILL so a device is
  split between sessions only when no free device is left, and state the
  residual limitation in README.md rather than implying it is enforced.
- The downgrade deleted the seeded rows under NOT EXISTS guards. Five tables
  carry an FK onto `resource_slot_types.slot_name`, which makes the delete
  destructive rather than reversible; leave both rows in place instead.
- `update_plugin_config` was a no-op while config watching stayed on, so etcd
  changes were acknowledged and dropped. Set `config_watch_enabled = False`,
  matching the ROCm and CUDA plugins.

Also replaces `__spec__.name` with `__name__` for the logger, dropping the
last `# type: ignore` in the module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hoyajigi

Copy link
Copy Markdown
Member Author

리뷰 감사합니다. 지적하신 5건을 3200f55 에서 처리했습니다. 항목별로 정리합니다.


1. 마이그레이션이 슬롯 타입을 전역으로 삽입하는 문제 — 현행 유지, 근거 설명

말씀하신 "전역 설치가 아닌데 왜 모든 사이트에 넣느냐"는 지적은, resource_slot_types설치된 하드웨어의 기록이 아니라 클러스터가 이름을 알고 렌더링할 수 있는 슬롯의 카탈로그라는 점에서 갈립니다.

  • 선례: ccf8ae5c90fe (add_device_metadata_to_resource_slot_types) 는 ipu.device, atom.device, atom-plus.device, atom-max.device, gaudi2.device, warboy.device, rngd.device, hyperaccel-lpu.device 8종을 해당 플러그인 설치 여부와 무관하게 모든 배포에 심습니다. neuron.core 만 다르게 취급할 근거가 없습니다.
  • 기능적 필요: agent_resources.slot_namefk_agent_resources_slot_name_resource_slot_types 하드 FK 가 있고, 매니저는 모든 agent heartbeat 마다 보고된 슬롯별로 agent_resources 행을 upsert 합니다. 런타임에 resource_slot_types 로 insert 하는 경로는 없습니다. 즉 시드되지 않은 슬롯을 보고하는 에이전트는 heartbeat 자체가 FK 위반으로 실패합니다.
  • tt-n300.device 를 같이 넣은 것도 같은 이유입니다. Tenstorrent n300 플러그인은 추가된 시점부터 그 슬롯을 보고해 왔는데 시드된 적이 없어서, 지금도 Tenstorrent 에이전트는 이 FK 에 걸립니다.

행 하나가 추가되는 비용은 UI 카탈로그에 이름이 하나 더 있는 것뿐이고(enabled 는 서버 기본값을 그대로 씁니다), 빠졌을 때의 비용은 에이전트 기동 실패입니다. 이 비대칭 때문에 현행을 유지했습니다. 그래도 "설치된 플러그인만 시드" 정책을 원하시면, 그건 이 PR 하나가 아니라 위 8종 + tt-n300 을 포함한 별도 정리 PR 이 맞다고 보고, 그 경우 런타임 등록 경로부터 만들어야 합니다. 방향을 정해주시면 그쪽으로 따로 올리겠습니다.

2. 삭제하는 downgrade 가 위험하다 — 동의, no-op 으로 변경

동의합니다. 가드(NOT EXISTS)를 붙여도 결국 그 순간 무엇이 참조 중이었는지에 따라 결과가 달라지는, 재현되지 않는 downgrade 였습니다. downgrade() 를 명시적 no-op 으로 바꾸고 사유를 주석과 모듈 docstring 에 남겼습니다. 구버전 매니저는 이 두 행을 조회하지 않으므로 남겨도 무해하고, 실제 제거는 스키마 결정이 아니라 운영자 결정입니다. 이에 따라 _added_slot_names / _referencing_tables 상수와 sqlalchemy import 도 제거했습니다.

3. import 예외 처리 불필요 — 동의, 제거

확인 결과 get_resource_spec_from_container 는 트리 전체에서 ai.backend.agent.docker.resourcesai.backend.agent.kubernetes.resources 에만 정의돼 있고 ai.backend.agent.resources 에는 없습니다. 즉 try 절은 항상 실패하는 죽은 코드였고, 거기 붙은 # type: ignore 도 불필요했습니다. ai.backend.accelerator.ipu.plugin 이 이미 쓰는 방식대로 직접 import 로 바꿨습니다.

(같은 맥락에서 로거의 __spec__.name # type: ignore__name__ 으로 바꿔 이 모듈의 마지막 # type: ignore 를 없앴습니다.)

4. 코어 격리 강제 — 강제 불가함을 인정하고, 완화 + 명시

지적이 맞습니다. /dev/neuronN 은 그 디바이스의 모든 코어를 담고 있고 Docker 가 넘길 수 있는 최소 단위가 노드이므로, 코어 단위 격리는 NEURON_RT_VISIBLE_CORES 라는 워크로드가 덮어쓸 수 있는 프로세스 환경변수에 의존합니다. 강제가 아니라 협조입니다. 숨기지 않고 다음과 같이 처리했습니다.

  • 완화: alloc map 의 할당 전략을 기본 EVENLY 에서 AllocationStrategy.FILL 로 바꿨습니다. 기본값은 코어를 디바이스에 분산시켜 노드 공유를 최대화하는 방향이었습니다. FILL 이면 한 디바이스를 채운 뒤 다음으로 넘어가므로, 완전히 빈 디바이스가 없을 때만 분할됩니다. 부수적으로 한 세션의 코어가 한 디바이스에 모이므로 collectives 가 NeuronLink 를 건너지 않습니다.
  • 명시: README.md## Isolation 절을 추가해 (a) 격리가 강제가 아니라는 점, (b) gather_container_measures 의 디바이스 노드 단위 귀속 때문에 한 디바이스를 나눠 쓰는 두 세션이 각각 디바이스 전체 사용량으로 보고된다는 점(Tenstorrent·Rebellions 플러그인과 동일)을 적었습니다.

참고로 이 신뢰 모델 자체는 이 플러그인이 처음 도입하는 것이 아닙니다. cuda.shares/dev/nvidia{N} 를 여러 세션이 공유하며 소프트 강제에 의존합니다.

강제 격리를 이번 릴리스에 넣지 않은 이유: 방법이 두 가지인데 둘 다 이 PR 범위를 넘습니다.

  1. 슬롯을 neuron.device (디바이스 단위) 로 바꾸기 — 슬롯 이름이 resource_slot_types 의 PK 이고 agent_resources/resource_allocations 가 참조하므로 나중에 세분화가 불가능해집니다(README 의 설계 근거 절 참고). Trainium1 기준 최소 할당 단위가 2코어가 됩니다.
  2. 디바이스를 통째로 할당하되 요청한 코어 수만 과금 — DiscretePropertyAllocMap.allocate() 의 "요청량만큼만 할당한다" 계약을 깨므로 매니저 스케줄러 쪽 지원이 필요합니다.

1번을 원하시면 지금 바꾸는 편이 낫습니다(머지 후에는 데이터 마이그레이션 비용이 붙습니다). 어느 쪽을 택할지 결정해 주시면 반영하겠습니다. 결정 전까지는 core 단위 + 문서화된 한계로 두는 것을 제안합니다.

5. 디바이스 할당 부분 실패 처리 — 동의, 실패하도록 변경

동의합니다. 그리고 이 플러그인에서는 다른 NPU 플러그인보다 더 나쁩니다. 노드를 건너뛰어도 container_index_of 는 이미 그 디바이스를 번호에 포함시킨 뒤이므로, 남은 코어의 NEURON_RT_VISIBLE_CORES 컨테이너-로컬 인덱스가 런타임이 실제로 보는 번호와 어긋납니다. 즉 "가속기 없이 시작" 에 더해 "남은 가속기도 잘못된 번호로 지정" 이 됩니다.

할당된 /dev/neuron{N} 이 없으면 ai.backend.agent.errors.resources.ResourceError 를 던져 컨테이너 생성을 실패시키도록 바꿨습니다(RuntimeError 대신 BackendAIError 계열을 쓰라는 AGENTS.md 규칙에 맞춤). 기존 테스트 test_missing_device_node_is_skipped_not_fataltest_missing_device_node_fails_container_creation 으로 교체했습니다.


Copilot 리뷰 중 반영하지 않은 항목

  • src/ 아래 BUILD 파일 금지: BUILDING.md 의 해당 규칙은 "새 파이썬 모듈/패키지"를 대상으로 하고, "최상위 컴포넌트 BUILD 파일만 존재한다" 고 명시합니다. src/ai/backend/accelerator/neuron/BUILDcuda_open, rocm, tenstorrent, furiosa 등과 같은 레벨의 배포 단위 최상위 BUILD 입니다. 이게 없으면 python_distributionbackendai_accelerator_v21 엔트리포인트가 없어서 휠 자체가 만들어지지 않습니다. 유지했습니다.
  • 컨테이너 메트릭 중복 집계: 실재하는 한계가 맞지만 Tenstorrent·Rebellions 플러그인과 동일한 동작이고, Docker inspect 가 노드 단위 정보만 주는 데서 오는 제약입니다. 컨테이너별 neuron.core 할당을 읽어오려면 stat 수집 경로에 resource spec 조회를 추가해야 해서, 세 플러그인을 함께 고치는 별도 PR 이 맞다고 봅니다. 우선 README 에 한계로 명시했습니다.

검증 범위 (중요)

이 박스에는 Neuron 하드웨어도 aws-neuronx-dkms 드라이버도 없습니다. 실행한 것은 다음뿐입니다.

  • pants fmt lint — 통과
  • pants check (mypy, CPython 3.13.7) — Success: no issues found in 6 source files
  • pants test tests/unit/accelerator/neuron:: — 통과 (모두 neuron-ls JSON 과 sysfs 를 스텁으로 대체한 테스트입니다)

실기 검증은 하지 못했습니다. trn1.2xlarge 이상에서 다음 두 가지를 확인해 주시면 좋겠습니다.

  1. 한 디바이스의 코어 일부만 할당한 세션에서 echo $NEURON_RT_VISIBLE_CORES 와 컨테이너 내 /dev/neuron* 목록이 일치하는지 (FILL 전환으로 할당 순서가 바뀌었습니다).
  2. neuron.core 를 보고하는 에이전트의 heartbeat 가 마이그레이션 적용 후 FK 위반 없이 통과하는지.

알렘빅 데이터 마이그레이션 검증도 로컬 DB 에서 수행하지 못했습니다. 다만 이번 변경은 downgrade() 를 no-op 으로 만든 것뿐이고 upgrade() 의 SQL 은 손대지 않았습니다.

@hoyajigi
hoyajigi requested a review from fregataa September 15, 2026 00:16
hoyajigi and others added 2 commits September 15, 2026 00:17
`f4a1c9d20b73` (sync the seed roles) landed on main with the same
`down_revision` as this PR's `a1c7e4b93f20`, leaving two alembic heads on
the merge commit and failing `check-alembic-migrations`.  Repoint this
branch's own unmerged revision onto it, per the diverged-heads rule in
`models/alembic/AGENTS.md` -- no merge migration, since main itself has a
single head.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hoyajigi

Copy link
Copy Markdown
Member Author

추가로, check-alembic-migrations 가 실패해서 한 건 더 처리했습니다 (a441b63).

main 에 머지된 f4a1c9d20b73 (sync the seed roles) 가 이 PR 의 a1c7e4b93f20같은 down_revision(c58b0d3a9e14) 을 쓰고 있어서, PR 머지 커밋 기준으로 alembic head 가 두 개가 되었습니다. models/alembic/AGENTS.md 의 diverged-heads 규칙대로 — main 자체는 head 가 하나이므로 merge migration 없이 — 이 브랜치의 미머지 리비전 쪽 down_revisionf4a1c9d20b73 으로 옮겼습니다. origin/main 은 rebase 가 아니라 merge 로 가져왔습니다(리뷰 앵커 보존).

python scripts/check-multiple-alembic-heads.py 로컬 실행 결과: Detected head revisions: a1c7e4b93f20 (단일 head).

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

Labels

comp:manager Related to Manager component require:db-migration Automatically set when alembic migrations are added or updated size:XL 500~ LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants