Fix ROCm backend (target MIGraphX EP) and add CUDA support
All checks were successful
Mirror to GitHub / mirror (push) Successful in 1s
release / build (push) Successful in 1m50s

ROCm silently fell back to CPU: the code targeted ONNX Runtime's classic
ROCMExecutionProvider, but distro ROCm-enabled ONNX Runtime builds (e.g.
Arch's onnxruntime-rocm) are commonly compiled with --use_migraphx instead,
and registration failures were invisible since breadmill never installed a
tracing subscriber. Switches the rocm feature to target MIGraphX, adds a
default tracing subscriber so EP registration success/failure is always
visible, and fixes a real crash where MIGraphX's output sequence padding
could index the attention mask out of bounds during mean-pooling.

Also adds a CUDA backend (--cuda / backend = "cuda") mirroring the same
ort execution-provider pattern, for NVIDIA hardware.

Version bump: 0.1.0 -> 0.2.0.
This commit is contained in:
Breadway 2026-07-03 21:58:39 +08:00
parent d3843f3131
commit 2618a33fd5
10 changed files with 227 additions and 96 deletions

View file

@ -19,8 +19,14 @@ pub enum Backend {
/// AMD XDNA NPU via the VitisAI ONNX Runtime execution provider.
/// `cache_dir` is used to store the compiled NPU model between runs.
Npu { cache_dir: PathBuf },
/// AMD iGPU via the ROCm ONNX Runtime execution provider.
/// AMD iGPU via the MIGraphX ONNX Runtime execution provider (ROCm-backed).
/// Distro ROCm ONNX Runtime builds (e.g. Arch's onnxruntime-rocm) are
/// commonly compiled with `--use_migraphx`, not `--use_rocm`, so this
/// targets `MIGraphXExecutionProvider` rather than the classic
/// `ROCMExecutionProvider`.
Rocm,
/// NVIDIA GPU via the CUDA ONNX Runtime execution provider.
Cuda,
}
pub struct OrtEmbedder {
@ -108,11 +114,14 @@ impl OrtEmbedder {
let actual_seq = shape[1] as usize;
let actual_dim = shape[2] as usize;
// Mean-pool over non-padding positions
// Mean-pool over non-padding positions. Some execution providers (e.g.
// MIGraphX) pad the output sequence dimension for kernel efficiency, so
// actual_seq can exceed mask.len() — only positions covered by our own
// attention mask are meaningful, so cap the loop at whichever is shorter.
let mut result = vec![0.0f32; actual_dim];
let mut count = 0usize;
for t in 0..actual_seq {
for t in 0..actual_seq.min(mask.len()) {
if mask[t] > 0 {
for d in 0..actual_dim {
result[d] += data[t * actual_dim + d];
@ -155,6 +164,7 @@ fn configure_eps(builder: SessionBuilder, backend: &Backend) -> Result<SessionBu
Backend::Cpu => Ok(builder),
Backend::Npu { cache_dir } => npu_session(builder, cache_dir),
Backend::Rocm => rocm_session(builder),
Backend::Cuda => cuda_session(builder),
}
}
@ -195,14 +205,19 @@ fn build_vitis_ep(cache_dir: &Path) -> Result<ort::ep::ExecutionProviderDispatch
.build())
}
// ---- ROCm EP (AMD iGPU) -----------------------------------------------------
// ---- MIGraphX EP (AMD iGPU, ROCm-backed) -------------------------------------
#[cfg(feature = "rocm")]
fn rocm_session(builder: SessionBuilder) -> Result<SessionBuilder, String> {
eprintln!("breadmill: using ROCm execution provider (device 0)");
eprintln!("breadmill: using MIGraphX execution provider (device 0)");
eprintln!(
"breadmill: note — check the log line above/below for \"Successfully registered \
`MIGraphXExecutionProvider`\"; if it's missing, the ONNX Runtime in use wasn't built \
with MIGraphX support and inference silently fell back to CPU"
);
builder
.with_execution_providers([
ort::execution_providers::ROCmExecutionProvider::default().build(),
ort::ep::MIGraphX::default().with_device_id(0).build(),
ort::ep::CPU::default().build(),
])
.map_err(|e| e.to_string())
@ -214,6 +229,30 @@ fn rocm_session(builder: SessionBuilder) -> Result<SessionBuilder, String> {
Ok(builder)
}
// ---- CUDA EP (NVIDIA GPU) ----------------------------------------------------
#[cfg(feature = "cuda")]
fn cuda_session(builder: SessionBuilder) -> Result<SessionBuilder, String> {
eprintln!("breadmill: using CUDA execution provider (device 0)");
eprintln!(
"breadmill: note — check the log line above/below for \"Successfully registered \
`CUDAExecutionProvider`\"; if it's missing, the ONNX Runtime in use wasn't built \
with CUDA support and inference silently fell back to CPU"
);
builder
.with_execution_providers([
ort::ep::CUDA::default().with_device_id(0).build(),
ort::ep::CPU::default().build(),
])
.map_err(|e| e.to_string())
}
#[cfg(not(feature = "cuda"))]
fn cuda_session(builder: SessionBuilder) -> Result<SessionBuilder, String> {
eprintln!("breadmill: CUDA backend requested but not compiled in (rebuild with --features cuda); using CPU");
Ok(builder)
}
/// Locate the VitisAI EP config file required by the AMD Ryzen AI SDK.
///
/// Search order:

View file

@ -24,17 +24,27 @@ const TOKENIZER_URL: &str =
"https://huggingface.co/nomic-ai/nomic-embed-text-v1.5/resolve/main/tokenizer.json";
fn main() {
// Surfaces ort's EP-registration warnings/errors (e.g. a GPU EP silently
// falling back to CPU) by default, without requiring RUST_LOG to be set.
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn,ort=info")),
)
.init();
let raw_args: Vec<String> = std::env::args().collect();
// Extract global flags before command dispatch.
let use_npu = raw_args.iter().any(|a| a == "--npu");
let use_rocm = raw_args.iter().any(|a| a == "--rocm");
let use_cuda = raw_args.iter().any(|a| a == "--cuda");
// Build a view of argv without backend flags for command matching.
let args: Vec<&str> = raw_args
.iter()
.skip(1)
.filter(|a| a.as_str() != "--npu" && a.as_str() != "--rocm")
.filter(|a| a.as_str() != "--npu" && a.as_str() != "--rocm" && a.as_str() != "--cuda")
.map(|s| s.as_str())
.collect();
@ -49,7 +59,7 @@ fn main() {
}
}
Some("--reindex") | Some("reindex") => {
if let Err(e) = run_daemon(true, use_npu, use_rocm) {
if let Err(e) = run_daemon(true, use_npu, use_rocm, use_cuda) {
eprintln!("breadmill: {}", e);
std::process::exit(1);
}
@ -66,7 +76,7 @@ fn main() {
cli_status();
}
None | Some("serve") | Some("--serve") => {
if let Err(e) = run_daemon(false, use_npu, use_rocm) {
if let Err(e) = run_daemon(false, use_npu, use_rocm, use_cuda) {
eprintln!("breadmill: {}", e);
std::process::exit(1);
}
@ -74,7 +84,7 @@ fn main() {
Some(cmd) => {
eprintln!("breadmill: unknown command: {}", cmd);
eprintln!(
"usage: breadmill [serve|reindex|fetch-model|query <text>|status] [--npu|--rocm] [--version]"
"usage: breadmill [serve|reindex|fetch-model|query <text>|status] [--npu|--rocm|--cuda] [--version]"
);
std::process::exit(1);
}
@ -83,7 +93,7 @@ fn main() {
// ---- Daemon -----------------------------------------------------------------
fn run_daemon(force_reindex: bool, use_npu: bool, use_rocm: bool) -> Result<(), String> {
fn run_daemon(force_reindex: bool, use_npu: bool, use_rocm: bool, use_cuda: bool) -> Result<(), String> {
let config = breadsearch_shared::Config::load();
let state_dir = breadsearch_shared::state_dir();
let cache_dir = breadsearch_shared::cache_dir();
@ -101,6 +111,9 @@ fn run_daemon(force_reindex: bool, use_npu: bool, use_rocm: bool) -> Result<(),
} else if use_rocm || config.model.backend == "rocm" {
eprintln!("breadmill: ROCm backend selected");
Backend::Rocm
} else if use_cuda || config.model.backend == "cuda" {
eprintln!("breadmill: CUDA backend selected");
Backend::Cuda
} else {
Backend::Cpu
};