mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix: skip Magika backend on x86 CPUs without AVX2 (#1162)
## Description Adds a narrow runtime AVX2 guard before initializing the Magika/ONNX Runtime detector on x86/x86_64. On x86/x86_64 CPUs without AVX2, Headroom falls back to existing non-Magika detection tiers instead of crashing during ONNX Runtime initialization. AVX2-capable systems retain existing behavior. Refs #1005 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - Adds a Magika/ONNX Runtime CPU support guard before `Session::new()`. - Returns a normal Magika init error on x86/x86_64 hosts without AVX2, allowing the existing detection chain to fall through to non-Magika tiers. - Keeps AVX2-capable x86/x86_64 behavior unchanged. - Does not apply the x86-specific AVX2 gate on non-x86 targets. - Adds CPU-aware Rust tests and a short troubleshooting note. ## Testing - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed ### Test Output ```text $ cargo test -p headroom-core --lib --locked 833 passed; 0 failed; 1 ignored $ cargo test --workspace --locked passed $ cargo clippy -p headroom-core --locked -- -D warnings clean ``` ## Real Behavior Proof - Environment: x86_64 Linux host with AVX but no AVX2 (Intel Xeon E5-2697 v2 on Proxmox), local build from this branch. - Exact command / steps: `python -X faulthandler -c 'from headroom._core import detect_content_type; print(detect_content_type("hello world"))'` - Observed result: before — process exited with `Fatal Python error: Illegal instruction`; after — command completed successfully returning `DetectionResult(content_type="text", ...)`, and full `cargo test -p headroom-core --lib --locked` passed with 833/0/1. - Not tested: generic no-AVX CPUs, alternate ONNX Runtime builds, non-x86 platforms. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md if applicable ## Screenshots (if applicable) N/A ## Additional Notes This partially addresses #1005 by handling one concrete native crash class: the Magika detector initializes ONNX Runtime through ort/ort-sys, whose precompiled runtime can contain AVX2-family instructions. On AVX-only x86_64 hosts, that initialization can SIGILL before Headroom can fall back. Scope: - This does not introduce generic no-AVX wheels. - This does not redesign Rust-core packaging. - This does not disable the Rust core globally. - This only prevents the Magika/ONNX detector tier from loading on x86/x86_64 CPUs where AVX2 is unavailable. - Non-Magika detection tiers continue to run. - On non-x86 targets, this x86-specific AVX2 gate is not applied. Changelog omitted: small native detector fallback fix with no public API change. Co-authored-by: AI Agent <ai-agent@homelab.internal>
This commit is contained in:
parent
abab3ccbfc
commit
64783d8824
3 changed files with 147 additions and 19 deletions
|
|
@ -28,6 +28,14 @@
|
|||
//! issue. Loud-on-error stays at the [`magika_detect`] entry point for
|
||||
//! callers who care; the chain swallows the err with a log line.
|
||||
//!
|
||||
//! # CPU compatibility
|
||||
//!
|
||||
//! Precompiled ONNX Runtime binaries from `ort-sys` may contain AVX2-family
|
||||
//! instructions. On x86/x86_64 CPUs where AVX2 is unavailable, the magika
|
||||
//! session init returns an init error before touching ONNX. The chain
|
||||
//! handles this identically to any other tier-1 error — logs it and falls
|
||||
//! through to Tier 2 / Tier 3.
|
||||
//!
|
||||
//! # SearchResults / BuildOutput
|
||||
//!
|
||||
//! The retired regex detector recognized grep-style search output
|
||||
|
|
@ -84,6 +92,7 @@ pub fn detect(content: &str) -> ContentType {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::transforms::magika_detector::magika_onnx_runtime_supported_by_cpu;
|
||||
|
||||
#[test]
|
||||
fn empty_input_short_circuits_to_plain_text() {
|
||||
|
|
@ -93,19 +102,38 @@ mod tests {
|
|||
#[test]
|
||||
fn json_array_routes_via_tier_1() {
|
||||
let payload = r#"[{"id": 1}, {"id": 2}, {"id": 3}]"#;
|
||||
assert_eq!(detect(payload), ContentType::JsonArray);
|
||||
if !magika_onnx_runtime_supported_by_cpu() {
|
||||
// On no-AVX2 hosts, magika returns Err and the chain
|
||||
// falls through to Tier 2 (unidiff — no match for JSON)
|
||||
// then Tier 3 (PlainText).
|
||||
assert_eq!(detect(payload), ContentType::PlainText);
|
||||
} else {
|
||||
assert_eq!(detect(payload), ContentType::JsonArray);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_code_routes_via_tier_1() {
|
||||
let py = "def hello():\n print('world')\n\nclass Foo:\n pass\n";
|
||||
assert_eq!(detect(py), ContentType::SourceCode);
|
||||
if !magika_onnx_runtime_supported_by_cpu() {
|
||||
// Magika fallthrough — unidiff won't catch Python source,
|
||||
// so the chain lands on PlainText.
|
||||
assert_eq!(detect(py), ContentType::PlainText);
|
||||
} else {
|
||||
assert_eq!(detect(py), ContentType::SourceCode);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn html_routes_via_tier_1() {
|
||||
let html = "<!DOCTYPE html><html><body><h1>x</h1></body></html>";
|
||||
assert_eq!(detect(html), ContentType::Html);
|
||||
if !magika_onnx_runtime_supported_by_cpu() {
|
||||
// Magika fallthrough — unidiff won't catch HTML,
|
||||
// so the chain lands on PlainText.
|
||||
assert_eq!(detect(html), ContentType::PlainText);
|
||||
} else {
|
||||
assert_eq!(detect(html), ContentType::Html);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -118,7 +146,8 @@ mod tests {
|
|||
+ print(\"new\")\n";
|
||||
// Either magika tags it `diff` (Tier 1 hit) or magika
|
||||
// mis-classifies as text and unidiff catches it (Tier 2).
|
||||
// Both paths produce GitDiff.
|
||||
// On no-AVX2 hosts, magika is unavailable so Tier 2 still
|
||||
// catches the diff. Both paths produce GitDiff.
|
||||
assert_eq!(detect(diff), ContentType::GitDiff);
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +156,7 @@ mod tests {
|
|||
// Magika often mis-classifies naked hunks (no `diff --git`
|
||||
// wrapper) because the visible bytes look like ordinary
|
||||
// patch lines mixed with code. Tier 2 (unidiff parser)
|
||||
// catches these.
|
||||
// catches these — even when magika is unavailable.
|
||||
let diff = "--- a/foo.py\n\
|
||||
+++ b/foo.py\n\
|
||||
@@ -1,2 +1,2 @@\n\
|
||||
|
|
@ -195,7 +224,13 @@ mod tests {
|
|||
// YAML lives in magika's `code` group; the chain returns it
|
||||
// as SourceCode so the router picks the code-aware compressor.
|
||||
let yaml = "name: my-app\nversion: 1.0\ndependencies:\n - foo\n";
|
||||
assert_eq!(detect(yaml), ContentType::SourceCode);
|
||||
if !magika_onnx_runtime_supported_by_cpu() {
|
||||
// Magika fallthrough — unidiff won't catch YAML,
|
||||
// so the chain lands on PlainText.
|
||||
assert_eq!(detect(yaml), ContentType::PlainText);
|
||||
} else {
|
||||
assert_eq!(detect(yaml), ContentType::SourceCode);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -205,13 +240,21 @@ mod tests {
|
|||
impl Counter {\n \
|
||||
pub fn new() -> Self { Self { counts: HashMap::new() } }\n\
|
||||
}\n";
|
||||
assert_eq!(detect(rs), ContentType::SourceCode);
|
||||
if !magika_onnx_runtime_supported_by_cpu() {
|
||||
// Magika fallthrough — unidiff won't catch Rust source,
|
||||
// so the chain lands on PlainText.
|
||||
assert_eq!(detect(rs), ContentType::PlainText);
|
||||
} else {
|
||||
assert_eq!(detect(rs), ContentType::SourceCode);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chain_is_deterministic_across_repeated_calls() {
|
||||
// Magika returns the same label for identical input on
|
||||
// repeated calls; the chain wraps that determinism.
|
||||
// On no-AVX2 hosts, the chain always falls through to
|
||||
// PlainText — which is equally deterministic.
|
||||
let payload = r#"{"users": [{"id": 1}, {"id": 2}]}"#;
|
||||
let a = detect(payload);
|
||||
let b = detect(payload);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@
|
|||
//! - **No router rewiring here.** PR3 lands the detector + tests
|
||||
//! only. PR5 flips the ContentRouter to call us instead of the
|
||||
//! regex-based [`crate::transforms::content_detector`].
|
||||
//!
|
||||
//! - **CPU compatibility.** The precompiled ONNX Runtime binary shipped by
|
||||
//! `ort-sys` may contain AVX2-family instructions on x86/x86_64. Where
|
||||
//! AVX2 is unavailable on those targets, the session init returns an
|
||||
//! error early instead of crashing with SIGILL; the detection chain then
|
||||
//! falls through to Tier 2 and Tier 3 normally.
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
|
@ -39,6 +45,25 @@ use tracing;
|
|||
|
||||
use crate::transforms::content_detector::ContentType;
|
||||
|
||||
/// Check whether the CPU can run the precompiled ONNX Runtime binary
|
||||
/// that magika depends on.
|
||||
///
|
||||
/// On x86/x86_64 without AVX2, the `onnxruntime` shared library shipped
|
||||
/// by `ort-sys` can contain AVX2-family instructions that will SIGILL.
|
||||
/// We detect this up front so the magika session init can fail gracefully
|
||||
/// instead of crashing.
|
||||
///
|
||||
/// On non-x86 targets, this x86-specific AVX2 gate is not applied.
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
pub(crate) fn magika_onnx_runtime_supported_by_cpu() -> bool {
|
||||
std::is_x86_feature_detected!("avx2")
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
|
||||
pub(crate) fn magika_onnx_runtime_supported_by_cpu() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Errors from the magika detector. Wraps the underlying `magika::Error`
|
||||
/// so callers can match on whether init or inference broke without
|
||||
/// pulling magika types into their imports.
|
||||
|
|
@ -102,6 +127,17 @@ fn magika_init_timeout() -> Duration {
|
|||
|
||||
fn session() -> &'static Mutex<Result<Session, String>> {
|
||||
MAGIKA_SESSION.get_or_init(|| {
|
||||
// Early-out if the CPU can't run the precompiled ONNX Runtime.
|
||||
// Without this check the `onnxruntime` shared library will crash
|
||||
// with SIGILL on x86 CPUs lacking AVX2.
|
||||
if !magika_onnx_runtime_supported_by_cpu() {
|
||||
return Mutex::new(Err(
|
||||
"Magika ONNX Runtime backend requires AVX2 on this platform; \
|
||||
falling back to non-Magika detection"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let timeout = magika_init_timeout();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
// Run the (potentially hanging) ONNX init on a side thread so we
|
||||
|
|
@ -234,9 +270,26 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
fn assert_detect(content: &str, expected: ContentType, hint: &str) {
|
||||
match magika_detect(content) {
|
||||
Ok(got) => assert_eq!(got, expected, "{hint}: expected {expected:?}, got {got:?}"),
|
||||
Err(e) => panic!("{hint}: detection failed: {e}"),
|
||||
if !magika_onnx_runtime_supported_by_cpu() {
|
||||
// On x86 hosts without AVX2 the magika session returns Err
|
||||
// before any ONNX init — assert graceful degradation
|
||||
// rather than panicking.
|
||||
match magika_detect(content) {
|
||||
Err(MagikaDetectorError::Init(msg)) => {
|
||||
assert!(
|
||||
msg.contains("AVX2"),
|
||||
"{hint}: expected AVX2 error, got: {msg}"
|
||||
);
|
||||
}
|
||||
other => panic!("{hint}: on no-AVX2 host expected Init(AVX2) error, got {other:?}"),
|
||||
}
|
||||
} else {
|
||||
match magika_detect(content) {
|
||||
Ok(got) => {
|
||||
assert_eq!(got, expected, "{hint}: expected {expected:?}, got {got:?}")
|
||||
}
|
||||
Err(e) => panic!("{hint}: detection failed: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,15 +421,32 @@ index abc123..def456 100644
|
|||
|
||||
#[test]
|
||||
fn singleton_session_is_reused_across_calls() {
|
||||
// Two back-to-back calls should both succeed without re-initing
|
||||
// the model (no panic, no error). We can't directly observe
|
||||
// the singleton hit-rate without instrumenting the test, but
|
||||
// wall-clock asymmetry between the first and second call is
|
||||
// strong evidence (cold ~50 ms, warm <1 ms). For the unit
|
||||
// suite, just prove neither call errors.
|
||||
magika_detect("hello world").unwrap();
|
||||
magika_detect("def f(): pass").unwrap();
|
||||
magika_detect(r#"{"a":1}"#).unwrap();
|
||||
// Two back-to-back calls should reuse the same session
|
||||
// (or same cached error). On AVX2 hosts the session is
|
||||
// Ok and repeated calls succeed; on no-AVX2 hosts the
|
||||
// session is Err and repeated calls return the same Err.
|
||||
if !magika_onnx_runtime_supported_by_cpu() {
|
||||
// On no-AVX2 the singleton caches the init error;
|
||||
// repeated calls all return the same Init error.
|
||||
let r1 = magika_detect("hello world");
|
||||
let r2 = magika_detect("def f(): pass");
|
||||
let r3 = magika_detect(r#"{"a":1}"#);
|
||||
for r in [&r1, &r2, &r3] {
|
||||
match r {
|
||||
Err(MagikaDetectorError::Init(msg)) => {
|
||||
assert!(msg.contains("AVX2"), "expected AVX2 error, got: {msg}");
|
||||
}
|
||||
other => panic!("on no-AVX2 host expected Init(AVX2) error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// On AVX2 hosts the session loads once and all calls
|
||||
// succeed. Wall-clock asymmetry (cold ~50 ms, warm
|
||||
// <1 ms) confirms reuse.
|
||||
magika_detect("hello world").unwrap();
|
||||
magika_detect("def f(): pass").unwrap();
|
||||
magika_detect(r#"{"a":1}"#).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -441,6 +441,21 @@ print(f"Compressed content: {result.messages[0]['content'][:200]}...")
|
|||
|
||||
---
|
||||
|
||||
### "Native detector crashes with illegal instruction"
|
||||
|
||||
On some older or virtualized x86_64 CPUs, AVX2 may be unavailable. The
|
||||
Magika/ONNX Runtime detector can require AVX2 through its precompiled runtime
|
||||
binary. Headroom skips that detector tier on x86/x86_64 hosts without AVX2 and
|
||||
falls back to non-Magika detection tiers instead of crashing.
|
||||
|
||||
If native startup still fails on an older CPU, set:
|
||||
|
||||
```bash
|
||||
export HEADROOM_REQUIRE_RUST_CORE=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Reference
|
||||
|
||||
| Exception | Meaning | Solution |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue