Files
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

336 lines
12 KiB
Rust

//! Integration tests for the `rocjitsu-dbt` (Dynamic Binary Translation)
//! emulator backend.
//!
//! These cover the parts that do not need a real GPU or a built
//! rocjitsu: registry wiring, ISA resolution, and the `HSA_TOOLS_LIB`
//! env contract `injection_def` produces (exercised with a stand-in
//! hook library so the contract is checked even when rocjitsu is not
//! installed).
//!
//! The final test attempts a *real* end-to-end translation with the
//! `rj_dbt_translate` CLI when both the tool and a sample code object
//! are discoverable on this host; it skips cleanly otherwise.
use std::path::PathBuf;
use std::process::Command;
use mirage_core::common::{MaybeRef, SimpleValue};
use mirage_core::emulator::{EmulatorDef, ExecMode, get_emulator_backend};
use mirage_core::profile::ProfileDef;
use mirage_core::session::{SessionDef, SessionId};
use mirage_core::topology::TopologyDef;
use mirage_rocjitsu::dbt;
/// Build a single-GPU emulator def pinning a `gfx_target_version` guest.
fn def_for_guest(gfx_target_version: u32) -> EmulatorDef {
let mut agent = mirage_core::agent::AgentDef::default();
agent.vm.gpu.device.gfx_target_version = gfx_target_version;
EmulatorDef {
emulator: dbt::NAME.to_string(),
plugins: Default::default(),
exec_mode: ExecMode::Functional,
options: Default::default(),
topology: MaybeRef::Owned(TopologyDef {
num_nodes: 1,
gpus_per_node: 1,
agent: MaybeRef::Owned(agent),
}),
}
}
/// Persist a session whose inline profile uses `emulator`, returning its
/// id so `injection_def` can resolve it.
fn write_session(emulator: EmulatorDef) -> SessionId {
let id = SessionId::new("dbt-test").unwrap();
let profile = ProfileDef {
name: "dbt-test".to_string(),
description: None,
emulator,
containerize: None,
};
let def = SessionDef {
id: id.clone(),
profile: MaybeRef::Owned(profile),
workdir: ".".to_string(),
daemon: false,
created_at: chrono::Utc::now(),
};
let layout = mirage_core::paths::SessionLayout::for_id(&id);
mirage_core::state::write_json(&layout.def(), &def).unwrap();
id
}
#[test]
fn backend_is_registered() {
let backend =
get_emulator_backend(dbt::NAME).expect("rocjitsu-dbt backend must be in the registry");
let desc = backend.description();
assert_eq!(desc.name, dbt::NAME);
// The backend advertises the target/source ISA options.
let opt_names: Vec<&str> = desc
.options_schema
.iter()
.map(|o| o.name.as_str())
.collect();
assert!(opt_names.contains(&dbt::OPT_TARGET_ISA));
assert!(opt_names.contains(&dbt::OPT_SOURCE_ISA));
}
#[test]
fn injection_emits_hsa_tools_env_contract() {
let _g = mirage_core::paths::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
mirage_core::paths::set_test_root(tmp.path());
// Stand in for the real hook library so discovery succeeds without a
// built rocjitsu; `ROCJITSU_HOOKS_LIB` is an explicit file override.
let fake_hook = tmp.path().join("librocjitsu_hooks.so");
std::fs::write(&fake_hook, b"\x7fELF stub").unwrap();
// Pin both ISAs explicitly so the contract is deterministic and
// independent of whatever GPU (if any) this host has.
// SAFETY: serialised by `test_env_lock`; cleared before returning.
unsafe {
std::env::set_var("ROCJITSU_HOOKS_LIB", &fake_hook);
std::env::set_var("RJ_DBT_TARGET_ISA", "gfx1201");
std::env::remove_var("RJ_DBT_SOURCE_ISA");
std::env::set_var("RJ_DBT_LOG", "2");
}
let session = write_session(def_for_guest(90500)); // gfx950 guest
let backend = get_emulator_backend(dbt::NAME).unwrap();
let injection = backend
.injection_def(&session)
.expect("injection should succeed with a discoverable hook");
// The hook is loaded via HSA_TOOLS_LIB, never LD_PRELOAD.
assert_eq!(injection.ld_preload, None);
assert_eq!(
injection.env.get("HSA_TOOLS_LIB").map(String::as_str),
Some(fake_hook.to_str().unwrap())
);
assert_eq!(
injection.env.get("RJ_DBT_TARGET_ISA").map(String::as_str),
Some("gfx1201")
);
// Source ISA is derived from the gfx950 guest agent.
assert_eq!(
injection.env.get("RJ_DBT_SOURCE_ISA").map(String::as_str),
Some("gfx950")
);
// The hook log level is forwarded from the environment.
assert_eq!(
injection.env.get("RJ_DBT_LOG").map(String::as_str),
Some("2")
);
// DBT runs translated code on the host's real GPU.
assert!(injection.host_gpus);
unsafe {
std::env::remove_var("ROCJITSU_HOOKS_LIB");
std::env::remove_var("RJ_DBT_TARGET_ISA");
std::env::remove_var("RJ_DBT_LOG");
}
}
#[test]
fn injection_fails_without_a_target_isa() {
let _g = mirage_core::paths::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
mirage_core::paths::set_test_root(tmp.path());
let fake_hook = tmp.path().join("librocjitsu_hooks.so");
std::fs::write(&fake_hook, b"\x7fELF stub").unwrap();
// SAFETY: serialised by `test_env_lock`.
unsafe {
std::env::set_var("ROCJITSU_HOOKS_LIB", &fake_hook);
std::env::remove_var("RJ_DBT_TARGET_ISA");
}
// A gfx950 guest with no target option: on a host with no supported
// GPU there is no target to translate to, so injection must fail
// loudly rather than run unemulated. (On a host that *does* have a
// supported GPU, a target is auto-selected and injection succeeds —
// accept either, but never a silent wrong env.)
let session = write_session(def_for_guest(90500));
let backend = get_emulator_backend(dbt::NAME).unwrap();
match backend.injection_def(&session) {
Err(e) => assert!(
e.to_string().contains("no target ISA"),
"unexpected error: {e}"
),
Ok(injection) => assert!(
injection.env.contains_key("RJ_DBT_TARGET_ISA"),
"a successful injection must carry a target ISA"
),
}
unsafe { std::env::remove_var("ROCJITSU_HOOKS_LIB") };
}
#[test]
fn target_isa_option_drives_injection() {
let _g = mirage_core::paths::test_env_lock();
let tmp = tempfile::tempdir().unwrap();
mirage_core::paths::set_test_root(tmp.path());
let fake_hook = tmp.path().join("librocjitsu_hooks.so");
std::fs::write(&fake_hook, b"\x7fELF stub").unwrap();
// SAFETY: serialised by `test_env_lock`.
unsafe {
std::env::set_var("ROCJITSU_HOOKS_LIB", &fake_hook);
std::env::remove_var("RJ_DBT_TARGET_ISA");
std::env::remove_var("RJ_DBT_SOURCE_ISA");
}
let mut emulator = def_for_guest(90402); // gfx942 guest
emulator.options.insert(
dbt::OPT_TARGET_ISA.to_string(),
SimpleValue::String("gfx950".to_string()),
);
let session = write_session(emulator);
let backend = get_emulator_backend(dbt::NAME).unwrap();
let injection = backend.injection_def(&session).unwrap();
assert_eq!(
injection.env.get("RJ_DBT_TARGET_ISA").map(String::as_str),
Some("gfx950")
);
assert_eq!(
injection.env.get("RJ_DBT_SOURCE_ISA").map(String::as_str),
Some("gfx942")
);
unsafe { std::env::remove_var("ROCJITSU_HOOKS_LIB") };
}
/// Locate the `rj_dbt_translate` CLI: an explicit `RJ_DBT_TRANSLATE`
/// override, then `$PATH`, then the in-tree rocjitsu build output.
fn find_dbt_translate() -> Option<PathBuf> {
if let Some(p) = std::env::var_os("RJ_DBT_TRANSLATE") {
let p = PathBuf::from(p);
if p.is_file() {
return Some(p);
}
}
if let Ok(out) = Command::new("sh")
.arg("-c")
.arg("command -v rj_dbt_translate")
.output()
&& out.status.success()
{
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !path.is_empty() {
return Some(PathBuf::from(path));
}
}
// In-tree build, relative to this crate. The rocjitsu CMake build
// links the CLI into `build/tools/`.
let candidate = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../rocjitsu/build/tools/rj_dbt_translate");
candidate.is_file().then_some(candidate)
}
/// Find a sample gfx950 HSA code object to translate. Probes the
/// ROCm-SDK wheel assets a developer venv ships.
fn find_gfx950_code_object() -> Option<PathBuf> {
if let Ok(out) = Command::new("sh")
.arg("-c")
.arg(
"find $HOME/rocm-systems/emulation -path '*hsa-amd-aqlprofile*' \
-name 'gfx950_*.hsaco' 2>/dev/null | head -1",
)
.output()
&& out.status.success()
{
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !path.is_empty() {
let p = PathBuf::from(path);
if p.is_file() {
return Some(p);
}
}
}
// Fall back to a raw gfx950 code object emitted by the rocjitsu
// `device_kernels` build (a standalone AMDGPU code object, not a HIP
// fatbin), so the test is self-sufficient from just a rocjitsu build.
let in_tree = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../rocjitsu/build/kernels/triton_cdna4_matmul_dynamic_32x32x64.hsaco");
in_tree.is_file().then_some(in_tree)
}
/// Real end-to-end translation: drive the actual `rj_dbt_translate`
/// pipeline to retarget a real gfx950 code object to gfx1201. Skipped
/// unless both the tool and a sample code object are available on this
/// host (the rocjitsu CMake `rj_dbt_translate` target builds the CLI).
///
/// This validates what is in mirage's scope — that the integration
/// invokes the translator correctly on a real input and the pipeline
/// runs to a deterministic, structured result. The rocjitsu DBT
/// translator is an MVP whose per-instruction coverage is still
/// growing, so a translatable-source kernel may legitimately stop at a
/// structured "expansion rule not implemented" diagnostic; that means
/// the pipeline ran, which is the contract under test. The test fails
/// only on outcomes that would indicate a wiring bug: the tool failing
/// to run, a usage error, or a failure to *parse* the input.
#[test]
fn real_dbt_translation_when_tool_available() {
let Some(tool) = find_dbt_translate() else {
eprintln!("rj_dbt_translate not found; skipping real translation test");
return;
};
let Some(code_object) = find_gfx950_code_object() else {
eprintln!("no gfx950 sample code object found; skipping real translation test");
return;
};
eprintln!(
"translating {} (gfx950 -> gfx1201) with {}",
code_object.display(),
tool.display()
);
let output = Command::new(&tool)
.arg(&code_object)
.arg("--input-target")
.arg("gfx950")
.arg("--output-target")
.arg("gfx1201")
.arg("--output-mode")
.arg("code-object")
.output()
.expect("rj_dbt_translate should be executable");
let stderr = String::from_utf8_lossy(&output.stderr);
// A parse failure means the input was not a real AMDGPU code object
// — i.e. the test fed the tool the wrong thing. That is a wiring bug.
assert!(
!stderr.contains("failed to parse input"),
"the sample was not a valid code object: {stderr}"
);
if output.status.success() {
// Happy path: a fully translated code object was emitted.
assert!(
!output.stdout.is_empty(),
"a successful translation must emit a code object on stdout"
);
return;
}
// Otherwise the pipeline must have run far enough to decode the
// input and reach a *structured* translation diagnostic (a known
// translator-coverage gap), not crashed or printed a usage error.
let ran_pipeline = stderr.contains("translation failed")
|| stderr.contains("legalization")
|| stderr.contains("expand")
|| stderr.contains("resource");
assert!(
ran_pipeline,
"rj_dbt_translate did not run the translation pipeline; \
exit={:?} stderr={stderr}",
output.status.code()
);
eprintln!(
"translation pipeline ran; stopped at a translator-coverage gap (expected for the MVP):\n{}",
stderr.lines().next().unwrap_or_default()
);
}