fix: gate the AMD backend behind a default-on `amd` cargo feature (#358)
## Summary
`libamdgpu_top` was a non-optional dependency, and it pulls in `libdrm_amdgpu_sys`, which links `libdrm.so.2` and `libdrm_amdgpu.so.1` unconditionally. Every Linux binary that depends on all-smi therefore inherited both as hard `NEEDED` entries, so a host without AMD's userspace DRM libraries (the overwhelming majority of deployments) fails to start with a loader error before `main` runs. The program cannot catch that or degrade to "no AMD GPU detected".
This implements option 1 from the issue: the AMD backend moves behind a new `amd` cargo feature, **enabled by default**.
## Why default-on, and why not `dlopen`
Default-on means every existing distribution path is byte-for-byte unchanged and keeps AMD support: the glibc release binaries, `cargo install all-smi`, Homebrew, and a plain `cargo build`. `Cargo.lock` does not move. Release workflows, packaging, and the Homebrew formula need no changes, which is the main reason default-on was chosen over default-off.
A downstream crate that declares `default-features = false` now stops inheriting `libdrm.so.2` and `libdrm_amdgpu.so.1`. `lablup/backend.ai-go` already declares `all-smi = { version = "0.25.0", default-features = false }`, so this fixes their reported startup failure with no change on their side.
Option 2 in the issue (runtime `dlopen` of `libdrm`) was evaluated and rejected for this PR. `libdrm` is linked by the third-party `libamdgpu_top` crate, and `src/device/readers/amd.rs` uses its types throughout, so runtime loading means replacing that crate with hand-rolled FFI. That is tracked as a separate follow-up.
## What changed
**`Cargo.toml`** makes `libamdgpu_top` optional and adds the feature. The target gate is unchanged, so the dep is still only declared for glibc Linux:
```toml
[target.'cfg(all(target_os = "linux", not(target_env = "musl")))'.dependencies]
libamdgpu_top = { version = "=0.11.5", optional = true }
[features]
default = ["cli", "amd"]
amd = ["dep:libamdgpu_top"]
```
The existing pin comment (semver-violating patch releases, the 0.11.5 fd-leak fix) is preserved and extended. The `[features]` block documents `amd` in the same voice as `furiosa` and `level_zero`, including why it is default-on where those two are default-off.
**Twelve `cfg` sites widened** from `all(target_os = "linux", not(target_env = "musl"))` to also require `feature = "amd"`:
| File | Site |
|------|------|
| `src/device/readers/mod.rs` | `pub mod amd;` |
| `src/device/reader_factory.rs` | `has_amd` import, `amd` import, the `AmdGpuReader` push |
| `src/device/platform_detection.rs` | `has_amd`, `detect_amd`, and the `introspection::detect_amd` pair |
| `src/utils/system.rs` | both sudo-permission blocks |
| `src/doctor/checks/amd.rs` | `check_libamdgpu_top`, `check_build_gate` |
| `src/doctor/checks/platform.rs` | `check_runtime` |
The negated complement in `introspection` is `not(all(target_os = "linux", not(target_env = "musl"), feature = "amd")))`, which stays an exact complement of the positive arm; a comment marks it so drift does not silently drop or duplicate `detect_amd`. The codebase has no `cfg` alias precedent (no `cfg_aliases` build dependency), so the predicate is written out at each site rather than introducing a new mechanism.
This is not a new configuration. `libamdgpu_top` was already excluded from musl builds, and `release.yml` ships `all-smi-linux-x86_64-musl` and `all-smi-linux-aarch64-musl` on every release. The change makes an already-shipping shape reachable on glibc.
**Diagnosability.** A glibc build with the feature off is a third state the doctor previously could not express, and it would have reported a false musl explanation. `all-smi doctor` on a feature-off build now reports:
```
WARN amd.build.target_env glibc build without the `amd` cargo feature: AMD support compiled out
-> Fix: rebuild with the default features, or add `--features amd`, for AMD GPU
monitoring; the feature is off here because something disabled it (typically a
downstream `default-features = false`) to avoid linking libdrm
SKIP amd.libamdgpu_top.abi libamdgpu_top not linked: built without the `amd` cargo feature
(see amd.build.target_env)
WARN platform.runtime target aarch64-unknown-linux-gnu (env=gnu), built without the
`amd` cargo feature so AMD GPU support is compiled out
```
On the default build all three of those checks pass instead, with their existing messages unchanged: `amd.build.target_env` reports AMD support available, `amd.libamdgpu_top.abi` reports the linked crate, and `platform.runtime` reports the plain target triple with no warning.
`amd.build.target_env` keeps its check id; only the internal function was renamed from `check_musl_gate` to `check_build_gate` since it now reports two independent gates. `doctor --bundle` records the feature too, so a support bundle answers the question directly: `features: cli,amd` versus `features: cli`.
Warning on a deliberately feature-off build (and the resulting exit code 1) matches the existing musl behaviour exactly, so this introduces no new convention.
**Docs**: `README.md`, `DEVELOPERS.md`, `docs/ARCHITECTURE.md`, and `docs/LIB_mode.md`. All state that `amd` is on by default, that disabling it removes the `libdrm` runtime dependency, that `--no-default-features` also drops `cli` (so a consumer wanting the CLI but not AMD needs `default-features = false, features = ["cli"]`), and that the musl artifacts have never had AMD support and remain the existing option for minimal containers. `docs/ARCHITECTURE.md` had a stale `[features]` block claiming `default = []`; it now matches `Cargo.toml`.
**CI** gains a regression guard in the existing `build-check` job that builds `--no-default-features --features cli` and fails if any `NEEDED` entry matches `libdrm`.
## Test plan
Run on `aarch64-unknown-linux-gnu`.
### 1. `cargo tree -e normal -i libamdgpu_top`
Default features, present:
```
$ cargo tree -e normal -i libamdgpu_top
libamdgpu_top v0.11.5
└── all-smi v0.25.0 (/home/inureyes/Development/backend.ai/all-smi)
exit=0
```
`--no-default-features`, absent (the "package not found" message is the pass condition):
```
$ cargo tree -e normal -i libamdgpu_top --no-default-features
error: package ID specification `libamdgpu_top` did not match any packages
exit=101
```
`--no-default-features --features cli`, also absent:
```
$ cargo tree -e normal -i libamdgpu_top --no-default-features --features cli
error: package ID specification `libamdgpu_top` did not match any packages
exit=101
```
### 2. Cross-target resolution with default features
All three resolve with no error despite `amd` being on for targets where the dep is not declared:
```
$ cargo tree -e normal --target x86_64-pc-windows-msvc
exit=0 libamdgpu_top occurrences: 0
$ cargo tree -e normal --target aarch64-apple-darwin
exit=0 libamdgpu_top occurrences: 0
$ cargo tree -e normal --target x86_64-unknown-linux-musl
exit=0 libamdgpu_top occurrences: 0
```
The musl result confirms the existing musl exclusion still holds.
### 3. `cargo check` for the downstream configurations
```
$ cargo check --no-default-features
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
$ cargo check --no-default-features --features cli
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.01s
```
### 4. `cargo check` with default features
```
$ cargo check
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.07s
```
### 5. `NEEDED` comparison (decisive)
```
$ cargo build --release
Finished `release` profile [optimized] target(s) in 1m 28s
$ objdump -p target/release/all-smi | grep NEEDED
NEEDED libdrm.so.2
NEEDED libdrm_amdgpu.so.1
NEEDED libgcc_s.so.1
NEEDED libm.so.6
NEEDED libc.so.6
NEEDED ld-linux-aarch64.so.1
$ cargo build --release --no-default-features --features cli
Finished `release` profile [optimized] target(s) in 1m 07s
$ objdump -p target/release/all-smi | grep NEEDED
NEEDED libgcc_s.so.1
NEEDED libm.so.6
NEEDED libc.so.6
NEEDED ld-linux-aarch64.so.1
```
The measured configuration is `--no-default-features --features cli`, because `--no-default-features` alone drops `cli` and therefore the binary. Both `libdrm` entries are gone; nothing else changed.
### 6. Clippy, both configurations
```
$ cargo clippy --lib --tests -- -D warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.19s
exit=0
$ cargo clippy --lib --tests --no-default-features --features cli -- -D warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.98s
exit=0
```
No dead-code or unused-import fallout appeared behind the disabled feature, and no blanket `#[allow]` was added.
### 7. Narrow tests for the touched modules
```
$ cargo test --lib device::platform_detection
test device::platform_detection::introspection::tests::snapshot_is_default_friendly ... ok
test device::platform_detection::tests::apple_silicon_detection_is_total_and_stable ... ok
test device::platform_detection::introspection::tests::snapshot_os_matches_consts ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 1502 filtered out; finished in 0.12s
$ cargo test --lib doctor
test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 1481 filtered out; finished in 0.02s
$ cargo test --lib utils::system
test utils::system::tests::test_calculate_adaptive_interval ... ok
test utils::system::tests::test_get_hostname ... ok
test utils::system::tests::test_ensure_sudo_permissions_non_macos ... ok
test utils::system::tests::test_ensure_sudo_permissions_with_fallback_returns_bool ... ok
test utils::system::tests::test_has_sudo_privileges_on_non_macos ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 1500 filtered out; finished in 0.03s
```
All three also pass under `--no-default-features --features cli` (3, 24, and 5 tests respectively).
`cargo fmt --check` is clean, and `Cargo.lock` is unmodified.
### Checklist
- [x] `cargo tree -e normal -i libamdgpu_top` present by default, absent with `--no-default-features`
- [x] `cargo tree` resolves for windows-msvc, apple-darwin, and linux-musl under default features
- [x] `cargo check --no-default-features` and `--no-default-features --features cli` pass
- [x] `cargo check` with default features passes
- [x] `libdrm.so.2` and `libdrm_amdgpu.so.1` present by default, gone with `--no-default-features --features cli`
- [x] `cargo clippy --lib --tests -- -D warnings` clean in both configurations
- [x] `cargo test --lib` for `device::platform_detection`, `doctor`, and `utils::system` pass in both configurations
- [x] `all-smi doctor` reports the feature-off state distinctly from the musl state
## Notes for review
Two pre-existing defects were found next to this work and deliberately left alone rather than folded into this PR:
1. `amd.libamdgpu_top.abi` prints `env!("CARGO_PKG_VERSION")`, which is all-smi's own version, not `libamdgpu_top`'s. It reports "linked libamdgpu_top 0.25.0" when the crate is 0.11.5. Cargo exposes no dependency version to the compiler without a build script, so a correct fix needs more than a one-line change.
2. `doctor --bundle`'s `enabled_features()` still does not list `level_zero`. This PR adds `amd` to it because that is what the change is about.
Not verified: nothing was built or run for Windows, macOS, or musl targets. Those configurations were checked only through `cargo tree` resolution, and their `cfg` arms through review of the complement predicates. A broad `cargo test --workspace` and `cargo clippy --workspace --all-targets` were not run.
Closes #345 J
Jeongkyu Shin committed
7320e5c2b19300593f2c17dfd7ca33808bbb1830
Parent: cafb054
Committed by GitHub <noreply@github.com>
on 8/7/2026, 4:55:52 PM