Repository navigation
Report cards held by host processes as busy (fix #9) - #10
Merged
Merged
Conversation
…Profiles) A process started on a node outside Kubernetes can hold an RNGD/ATOM card that the device plugin and the AcceleratorProfiles still report as free; a pod placed there fails with EBUSY (ai-rm#32). The provider now finds each card's holders read-only (furiosa-smi ps, the rbln ctx_info / rbln-stat contexts, /proc/<pid>/fd), classifies them by cgroup as k8s or host, and publishes inUse/holders in spec.extra. A host-held card gets health=busy and healthy="false", which rm-controller already skips. Also: node label airm.ai-computing.io/host-held, rm_accel_accelerators_in_use and _host_held on /metrics, chart values hostPID and occupancy (0.5.0 / chart 0.6.0). Closes #9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gspark-etri
added a commit
to ai-computing/ai-rm
that referenced
this pull request
Oct 20, 2026
## Background rm-accel v0.5.0 (ai-computing/rm-accel#10, fixing rm-accel#9) reports cards held by host processes as busy (`healthy: false`), so rm-controller no longer places pods on them. The umbrella chart pins rm-accel through the submodule. ## Changes The `components/l1-accel/rm-accel` submodule moves to v0.5.0 (8aa955b; chart 0.6.0, which defaults to `hostPID: true` and `occupancy: true`). `charts/ai-rm` keeps its `>=0.5.0` constraint. ## Verification - sim-e2e (kind): the umbrella chart installs with the new subchart and the full flow passes. That is this PR's `chart` and `e2e` checks. - The live behaviour is in rm-accel#10: both host-held cards were reported busy on the second RNGD node, and rm-controller excluded them. ## Compatibility and limits The live cluster's DaemonSet is unchanged until it is upgraded with the 0.6.0 chart. ## Related Issue Related to #34 (cross-node NPU experiment; it needs busy cards to be visible). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gspark-etri
added a commit
that referenced
this pull request
Oct 20, 2026
## Background In the ai-rm end-to-end run (ai-computing/ai-rm#32, docs/baselines.md HUB-34), two training processes started directly on the second RNGD node, outside Kubernetes, held both of its cards. The Furiosa device plugin (allocatable 2, none allocated) and rm-accel's AcceleratorProfiles still showed both cards as free. The scheduler and rm-controller could therefore pick them, and the decode worker failed with `EBUSY`. ## Changes - **`rm_abstract/airm/occupancy.py`** (new) finds who holds each card. Everything it does is read-only. - Furiosa: `furiosa-smi ps --format json`. `dev_name` looks like `npu0:[0, 7]`. - Rebellions: the driver's `/sys/class/rebellions/rbln<N>/ctx_info`, which is the source `rbln-stat` reads. `rbln-stat --json` `contexts` is used as well, but only when its `npu` index maps to exactly one device. rbln-stat restarts `npu` at 0 in each group, so on a 4-card host every device reports `npu: 0` and a context cannot be matched to a card from that field alone. - `/proc/<pid>/fd` links to compute device nodes (`/dev/rngd/npu<N>ch*|pe*`, `/dev/rbln<N>`). Management and BAR nodes are ignored because exporters open those too. - Each holder is classified from `/proc/<pid>/cgroup`: a path containing `kubepods` means `k8s`, anything else means `host`. Only the executable's basename is kept. Arguments, users and hostnames are dropped. Monitoring tools such as `furiosa-smi` and `rbln-stat` are not counted as holders. - Host processes are only visible from the host PID namespace. This is detected by checking whether PID 1's cgroup contains `kubepods`. When they are not visible, the daemon logs a warning and publishes **no** occupancy keys rather than reporting every card as free. - **The provider** adds these keys to each accelerator's `extra`, which ends up in the AcceleratorProfile `spec.extra`. All values are strings, as the CRD requires. - `inUse`: `"true"` or `"false"`. - `holders`: a JSON list such as `[{"pid":…,"command":"python3","managed":"host"}]`. - A card held by a host process also gets **`health: busy`** and **`healthy: "false"`**. rm-controller's `policy.healthy()` already skips `healthy: false`, so it needs no changes. `busy` is not in its `UNHEALTHY` list; it is there to describe the state. - A card held only by pods stays available. The device plugin already accounts for those cards. - `AIRM_OCCUPANCY=0` turns all of this off. - **The daemon** adds the node label `airm.ai-computing.io/host-held=<n>`, which the daemon owns and prunes. It also serves two new metrics on `/metrics`: `rm_accel_accelerators_in_use` and `rm_accel_accelerators_host_held`, with labels `{component,vendor,model}`. They use an rm-accel prefix because `rm_core.metrics.METRICS` does not list them. Promoting them to shared metrics would be an rm-core change. - **Chart 0.6.0** adds two values: `hostPID: true`, needed for `furiosa-smi ps` and `/proc` to see host processes, and `occupancy: true`, which sets `AIRM_OCCUPANCY`. The release is 0.5.0 (`pyproject`, `__version__`, chart `appVersion`). No tag is pushed. - README in EN and KO (new "Card occupancy" section, env and chart tables), and CHANGELOG. - Tests: `tests/unit/test_occupancy.py` (13 tests) with fixtures recorded from the nodes in `tests/fixtures/occupancy/`. Ids, serials and argument paths are replaced with placeholders. The existing `test_airm.py` fixture now sets `AIRM_OCCUPANCY=0` so it stays deterministic on Linux CI. ## Verification Node tools: furiosa-smi 2026.4.0 (firmware 2026.4.0), rbln-stat 3.0.0 (KMD 3.0.0), Python 3.10.12 on both nodes, cgroup v2 + containerd (systemd driver). Comparison: ai-rm master 9446511, rm-core v0.1.2. **Real tool formats**, read-only. These are the formats behind the fixtures: - On the second RNGD node's host, `furiosa-smi ps --format json` returned two entries: `{"pid":37839,"dev_name":"npu0:[0, 7]",…}` and `{"pid":38166,"dev_name":"npu1:[0, 7]",…}`. The holders' cgroup is `0::/user.slice/user-1001.slice/session-4.scope`, which is classified as a host process. - In the same node's existing `rm-abstract-node` pod, which runs without hostPID, `furiosa-smi ps --format json` returned `[]`. This is the root cause: the pod cannot see host processes. - On the RNGD+ATOM node, `rbln-stat --json` showed `"contexts": []` and every device at `"npu": 0`. The context JSON keys (`ctx_id, npu, process, pid, priority, ptid, memalloc, status, util_info`) and the `ctx_info` line format (`pid, ctx_id, priority, ptid, memalloc, status, util`) were read from rbln-stat 3.0.0's bundled `rblndevice.py` bytecode. No ATOM was in use while recording, so `rbln-stat.contexts.json` is constructed in that shape and labelled as such. **Unit tests** - Windows, Python 3.11.9, rm-core v0.1.2: `python -m pytest tests/unit` gave 113 passed and 1 skipped. The skipped test is the fd scan, which needs symlinks. - Linux, Python 3.10: `pytest tests/unit/test_occupancy.py tests/unit/test_airm.py` gave 30 passed, fd scan included. - `black --check`, `ruff check` and `tools/check_sensitive.py` all passed. `helm lint` passed, and `helm template` renders `hostPID: true` / `AIRM_OCCUPANCY "1"` by default and `false` / `"0"` with the values turned off. **Live, read-only dry-run of this branch** (`python -m rm_abstract.airm.daemon --once --dry-run`, source copied to a temp dir and removed afterwards; the training processes were not touched): | where | result | |---|---| | second RNGD node, host PID namespace | `furiosa:0` and `furiosa:1` are `inUse=true health=busy healthy=false`, holders `[{"pid":37839,"command":"python3","managed":"host"}]` and `[{"pid":38166,…}]`. Label `host-held=2`. | | RNGD+ATOM node, host PID namespace | RNGD 0/1/3 and ATOM 0-3 are all `inUse=false holders=[]`. Label `host-held=0`. | | existing `rm-abstract-node` pods on both nodes (`kubectl exec`, deployed chart has no hostPID) | Warning "host processes are not visible (run with hostPID): occupancy not reported". No occupancy keys are published, so cards are not wrongly reported as free. | **rm-controller excludes the host-held cards.** rm-controller from ai-rm master was run with `run_once(..., dry_run=True)` through its `kube.Kubectl` parsing, fed the 11 profiles above with both nodes Ready. The spec asked for 2 prefill and 3 decode RNGD over `tcp`. - Profiles as rm-accel 0.4.2 published them (occupancy keys stripped): decode was placed on `rngd-node-2/furiosa:0` and `rngd-node-2/furiosa:1`. That reproduces the `EBUSY` placement. - Profiles from this branch: `no free furiosa.RNGD x2 for prefill and furiosa.RNGD x3 for decode (tcp); skipped unhealthy rngd-node-2/furiosa:0, rngd-node-2/furiosa:1`. - With 1 prefill and 2 decode: prefill went to `rngd-atom-node/furiosa:0` and decode to `rngd-atom-node/furiosa:1,3`. Only free cards were used. ## Compatibility and limits - Additive: new `spec.extra` keys, one label and two metrics. Profiles still parse with rm-core `from_profile`. Deployments that set `hostPID: false` or `occupancy: false` behave as before, apart from the warning. - `hostPID: true` lets the daemon, which is already privileged, see all host processes. It only reads `cmdline`, `cgroup` and `fd` links. - Not verified live: - **k8s-held** cards. No NPU pod was running anywhere in the cluster during the run. The case is covered by the real `kubepods` cgroup line in the fixtures. - A **busy ATOM**. All contexts were empty. - The deployed DaemonSet needs this chart (hostPID) before the cluster sees occupancy. Until then the live pods publish nothing new, as shown above. - NVIDIA GPUs get `inUse`/`holders` only from the fd scan, which has no `/dev/nvidia<N>` pattern yet, so they always read free. GPUs are out of scope for #9. - A host process that opens a card only briefly can mark it busy for one round (60 s). Known monitoring tools are excluded. - `health: busy` is not in rm-controller's `UNHEALTHY` list. Exclusion relies on `healthy: "false"`. Adding `busy` there, and showing "busy" rather than "unhealthy" in the controller's reason, would be an optional ai-rm follow-up. ## Related Issue Closes #9 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gspark-etri
added a commit
to ai-computing/ai-rm
that referenced
this pull request
Oct 20, 2026
## Background rm-accel v0.5.0 (ai-computing/rm-accel#10, fixing rm-accel#9) reports cards held by host processes as busy (`healthy: false`), so rm-controller no longer places pods on them. The umbrella chart pins rm-accel through the submodule. ## Changes The `components/l1-accel/rm-accel` submodule moves to v0.5.0 (8aa955b; chart 0.6.0, which defaults to `hostPID: true` and `occupancy: true`). `charts/ai-rm` keeps its `>=0.5.0` constraint. ## Verification - sim-e2e (kind): the umbrella chart installs with the new subchart and the full flow passes. That is this PR's `chart` and `e2e` checks. - The live behaviour is in rm-accel#10: both host-held cards were reported busy on the second RNGD node, and rm-controller excluded them. ## Compatibility and limits The live cluster's DaemonSet is unchanged until it is upgraded with the 0.6.0 chart. ## Related Issue Related to #34 (cross-node NPU experiment; it needs busy cards to be visible). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.
Background
In the ai-rm end-to-end run (ai-computing/ai-rm#32, docs/baselines.md HUB-34), two training processes started directly on the second RNGD node, outside Kubernetes, held both of its cards. The Furiosa device plugin (allocatable 2, none allocated) and rm-accel's AcceleratorProfiles still showed both cards as free. The scheduler and rm-controller could therefore pick them, and the decode worker failed with
EBUSY.Changes
rm_abstract/airm/occupancy.py(new) finds who holds each card. Everything it does is read-only.furiosa-smi ps --format json.dev_namelooks likenpu0:[0, 7]./sys/class/rebellions/rbln<N>/ctx_info, which is the sourcerbln-statreads.rbln-stat --jsoncontextsis used as well, but only when itsnpuindex maps to exactly one device. rbln-stat restartsnpuat 0 in each group, so on a 4-card host every device reportsnpu: 0and a context cannot be matched to a card from that field alone./proc/<pid>/fdlinks to compute device nodes (/dev/rngd/npu<N>ch*|pe*,/dev/rbln<N>). Management and BAR nodes are ignored because exporters open those too./proc/<pid>/cgroup: a path containingkubepodsmeansk8s, anything else meanshost. Only the executable's basename is kept. Arguments, users and hostnames are dropped. Monitoring tools such asfuriosa-smiandrbln-statare not counted as holders.kubepods. When they are not visible, the daemon logs a warning and publishes no occupancy keys rather than reporting every card as free.extra, which ends up in the AcceleratorProfilespec.extra. All values are strings, as the CRD requires.inUse:"true"or"false".holders: a JSON list such as[{"pid":…,"command":"python3","managed":"host"}].health: busyandhealthy: "false". rm-controller'spolicy.healthy()already skipshealthy: false, so it needs no changes.busyis not in itsUNHEALTHYlist; it is there to describe the state.AIRM_OCCUPANCY=0turns all of this off.airm.ai-computing.io/host-held=<n>, which the daemon owns and prunes. It also serves two new metrics on/metrics:rm_accel_accelerators_in_useandrm_accel_accelerators_host_held, with labels{component,vendor,model}. They use an rm-accel prefix becauserm_core.metrics.METRICSdoes not list them. Promoting them to shared metrics would be an rm-core change.hostPID: true, needed forfuriosa-smi psand/procto see host processes, andoccupancy: true, which setsAIRM_OCCUPANCY. The release is 0.5.0 (pyproject,__version__, chartappVersion). No tag is pushed.tests/unit/test_occupancy.py(13 tests) with fixtures recorded from the nodes intests/fixtures/occupancy/. Ids, serials and argument paths are replaced with placeholders. The existingtest_airm.pyfixture now setsAIRM_OCCUPANCY=0so it stays deterministic on Linux CI.Verification
Node tools: furiosa-smi 2026.4.0 (firmware 2026.4.0), rbln-stat 3.0.0 (KMD 3.0.0), Python 3.10.12 on both nodes, cgroup v2 + containerd (systemd driver). Comparison: ai-rm master 9446511, rm-core v0.1.2.
Real tool formats, read-only. These are the formats behind the fixtures:
furiosa-smi ps --format jsonreturned two entries:{"pid":37839,"dev_name":"npu0:[0, 7]",…}and{"pid":38166,"dev_name":"npu1:[0, 7]",…}. The holders' cgroup is0::/user.slice/user-1001.slice/session-4.scope, which is classified as a host process.rm-abstract-nodepod, which runs without hostPID,furiosa-smi ps --format jsonreturned[]. This is the root cause: the pod cannot see host processes.rbln-stat --jsonshowed"contexts": []and every device at"npu": 0. The context JSON keys (ctx_id, npu, process, pid, priority, ptid, memalloc, status, util_info) and thectx_infoline format (pid, ctx_id, priority, ptid, memalloc, status, util) were read from rbln-stat 3.0.0's bundledrblndevice.pybytecode. No ATOM was in use while recording, sorbln-stat.contexts.jsonis constructed in that shape and labelled as such.Unit tests
python -m pytest tests/unitgave 113 passed and 1 skipped. The skipped test is the fd scan, which needs symlinks.pytest tests/unit/test_occupancy.py tests/unit/test_airm.pygave 30 passed, fd scan included.black --check,ruff checkandtools/check_sensitive.pyall passed.helm lintpassed, andhelm templaterendershostPID: true/AIRM_OCCUPANCY "1"by default andfalse/"0"with the values turned off.Live, read-only dry-run of this branch (
python -m rm_abstract.airm.daemon --once --dry-run, source copied to a temp dir and removed afterwards; the training processes were not touched):furiosa:0andfuriosa:1areinUse=true health=busy healthy=false, holders[{"pid":37839,"command":"python3","managed":"host"}]and[{"pid":38166,…}]. Labelhost-held=2.inUse=false holders=[]. Labelhost-held=0.rm-abstract-nodepods on both nodes (kubectl exec, deployed chart has no hostPID)rm-controller excludes the host-held cards. rm-controller from ai-rm master was run with
run_once(..., dry_run=True)through itskube.Kubectlparsing, fed the 11 profiles above with both nodes Ready. The spec asked for 2 prefill and 3 decode RNGD overtcp.rngd-node-2/furiosa:0andrngd-node-2/furiosa:1. That reproduces theEBUSYplacement.no free furiosa.RNGD x2 for prefill and furiosa.RNGD x3 for decode (tcp); skipped unhealthy rngd-node-2/furiosa:0, rngd-node-2/furiosa:1.rngd-atom-node/furiosa:0and decode torngd-atom-node/furiosa:1,3. Only free cards were used.Compatibility and limits
spec.extrakeys, one label and two metrics. Profiles still parse with rm-corefrom_profile. Deployments that sethostPID: falseoroccupancy: falsebehave as before, apart from the warning.hostPID: truelets the daemon, which is already privileged, see all host processes. It only readscmdline,cgroupandfdlinks.kubepodscgroup line in the fixtures.inUse/holdersonly from the fd scan, which has no/dev/nvidia<N>pattern yet, so they always read free. GPUs are out of scope for Cards held by host processes outside Kubernetes are reported as free #9.health: busyis not in rm-controller'sUNHEALTHYlist. Exclusion relies onhealthy: "false". Addingbusythere, and showing "busy" rather than "unhealthy" in the controller's reason, would be an optional ai-rm follow-up.Related Issue
Closes #9
🤖 Generated with Claude Code