Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion crates/apr-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,12 @@ cuda = ["inference", "realizar/cuda", "entrenar/cuda", "aprender-train-distill?/
# the binary was built with it (§2.1 INVALID-BUILD reads that entry).
cuda-batch = ["cuda"]
# PMAT-335: WGPU backend for AMD/Intel/Apple GPU inference via Vulkan/Metal/WebGPU
wgpu = ["inference"]
# PMAT-335: the `wgpu` code in commands/finetune.rs uses
# `entrenar::finetune::wgpu_pipeline::WgpuInstructPipeline` and
# `entrenar::autograd::wgpu_training`, both gated on aprender-train's `gpu`
# feature. `wgpu = ["inference"]` enabled neither entrenar nor that gate, so
# `--features wgpu` (and therefore `--all-features`) did not compile.
wgpu = ["inference", "training", "entrenar/gpu"]
visualization = ["renacer", "trueno-viz"]
zram = ["trueno-zram-core"]
training = ["dep:entrenar", "dep:entrenar-lora", "dep:aprender-train-distill", "dep:aprender-train-common", "aprender-train-distill?/shard-batch-source"]
Expand Down
6 changes: 5 additions & 1 deletion crates/apr-cli/src/commands/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,11 @@ fn clean_chat_response(raw: &str) -> String {
}

/// Detect format from magic bytes (more reliable than extension)
#[cfg(feature = "inference")]
///
/// Not gated: this reads four magic bytes and returns the ungated
/// `ModelFormat`. It carried `#[cfg(feature = "inference")]` while its caller
/// `format_from_leading_bytes` did not, so `--no-default-features` could not
/// compile the module at all.
fn detect_format_from_bytes(data: &[u8]) -> ModelFormat {
if data.len() < 8 {
return ModelFormat::Demo;
Expand Down
9 changes: 9 additions & 0 deletions crates/apr-cli/src/commands/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,16 @@ pub(crate) fn run(
// and run an element-wise diff per `apr-cli-trace-save-tensor-v1`
// `apr_diff_values_compat` invariant.
if is_aprt_stage_file(path1) && is_aprt_stage_file(path2) {
// The APRT body is read by realizar's save_tensor reader; without
// it there is nothing to fall back to, so say which build is needed
// rather than mis-diffing the pair as models.
#[cfg(feature = "realizar")]
return run_aprt_stage_diff(path1, path2, limit, json_output);
#[cfg(not(feature = "realizar"))]
return Err(CliError::InvalidFormat(
"APRT stage files need the save-tensor reader; rebuild with --features inference"
.to_string(),
));
}
// Run tensor value comparison
run_tensor_value_diff(path1, path2, filter, limit, transpose_aware, json_output)
Expand Down
1 change: 1 addition & 0 deletions crates/apr-cli/src/commands/diff_05_aprt_stage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ fn compute_aprt_stage_stats(

// serde_json::json!() macro uses infallible unwrap internally
#[allow(clippy::disallowed_methods)]
#[cfg(feature = "realizar")]
fn run_aprt_stage_diff(
path1: &Path,
path2: &Path,
Expand Down
113 changes: 74 additions & 39 deletions crates/apr-cli/src/commands/explain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ use std::path::{Path, PathBuf};
/// The positional argument is auto-detected: if it looks like a file path
/// (exists on disk or has a model file extension), it's treated as `--file`.
/// Otherwise it's treated as an error code or family name.
/// Companion-file lookup for `apr explain`.
///
/// realizar's `safetensors::find_sibling_file` handles hash-prefixed and
/// symlinked model paths (CB-510 / publish-safety), so it is used whenever
/// `inference` links realizar. `apr explain` reports metadata and does not
/// infer, so it must still build without that feature: the fallback is the
/// literal sibling in the parent directory, returned only when it exists so
/// callers see the same `Option` contract either way.
#[cfg(feature = "inference")]
fn find_sibling(path: &Path, name: &str) -> Option<PathBuf> {
realizar::safetensors::find_sibling_file(path, name)
}

#[cfg(not(feature = "inference"))]
fn find_sibling(path: &Path, name: &str) -> Option<PathBuf> {
let candidate = path.parent()?.join(name);
candidate.exists().then_some(candidate)
}

#[allow(clippy::unnecessary_wraps, clippy::fn_params_excessive_bools)]
#[provable_contracts_macros::contract(
"apr-cli-operations-v1",
Expand Down Expand Up @@ -228,7 +247,7 @@ fn resolve_family_from_model_file(
// Companion lookup: find_sibling_file handles hash-prefixed / symlinked
// model paths robustly (CB-510 / publish-safety) and only returns Some
// when the file exists — subsuming the prior with_file_name + .exists().
if let Some(config_path) = realizar::safetensors::find_sibling_file(&real_path, "config.json") {
if let Some(config_path) = find_sibling(&real_path, "config.json") {
resolve_from_config_json(&config_path)
} else {
emit_kernel_error(
Expand Down Expand Up @@ -341,7 +360,7 @@ fn resolve_config_mapping(
// hash-prefixed / symlinked model paths); fall back to the literal
// sibling dir so extract_config_mapping reports not-found as before
// (parent().join avoids with_file_name per publish-safety).
realizar::safetensors::find_sibling_file(p, "config.json").unwrap_or_else(|| {
find_sibling(p, "config.json").unwrap_or_else(|| {
p.parent().map_or_else(
|| std::path::PathBuf::from("config.json"),
|d| d.join("config.json"),
Expand Down Expand Up @@ -467,48 +486,64 @@ fn explain_error_code(code: &str, json: bool) {
/// PMAT-266: Explain tensor — look up in actual model file via RosettaStone
// serde_json::json!() macro uses infallible unwrap internally
#[allow(clippy::disallowed_methods)]
fn explain_tensor(tensor_name: &str, file: Option<&Path>, json: bool) {
// GH-510: Respect --json flag for tensor explanations
if json {
let role = TENSOR_ROLES
/// The tensors in `path` whose name matches `tensor_name`, as JSON objects.
///
/// `None` when there is no readable model at `path`; an empty vector when the
/// model has no matching tensor. The caller distinguishes the two.
fn matching_tensors_json(tensor_name: &str, path: &Path) -> Option<Vec<serde_json::Value>> {
if !path.exists() {
return None;
}
let report = aprender::format::rosetta::RosettaStone::new()
.inspect(path)
.ok()?;
Some(
report
.tensors
.iter()
.find(|(patterns, _)| patterns.iter().any(|p| tensor_name.contains(p)))
.map(|(_, desc)| *desc);
.filter(|t| t.name == tensor_name || t.name.contains(tensor_name))
.map(|t| {
serde_json::json!({
"name": t.name,
"shape": t.shape,
"dtype": format!("{:?}", t.dtype),
})
})
.collect(),
)
}

let mut output = serde_json::json!({
"tensor": tensor_name,
"role": role.unwrap_or("unknown"),
});
/// GH-510: the `--json` half of [`explain_tensor`].
fn explain_tensor_json(tensor_name: &str, file: Option<&Path>) {
let role = TENSOR_ROLES
.iter()
.find(|(patterns, _)| patterns.iter().any(|p| tensor_name.contains(p)))
.map(|(_, desc)| *desc);

// If a file is provided, try to look up the actual tensor
if let Some(path) = file {
if path.exists() {
let rosetta = aprender::format::rosetta::RosettaStone::new();
if let Ok(report) = rosetta.inspect(path) {
let matching: Vec<_> = report
.tensors
.iter()
.filter(|t| t.name == tensor_name || t.name.contains(tensor_name))
.map(|t| {
serde_json::json!({
"name": t.name,
"shape": t.shape,
"dtype": format!("{:?}", t.dtype),
})
})
.collect();
if !matching.is_empty() {
output["matches"] = serde_json::json!(matching);
}
output["file"] = serde_json::json!(path.display().to_string());
}
}
let mut output = serde_json::json!({
"tensor": tensor_name,
"role": role.unwrap_or("unknown"),
});

if let Some((path, matching)) =
file.and_then(|p| Some((p, matching_tensors_json(tensor_name, p)?)))
{
if !matching.is_empty() {
output["matches"] = serde_json::json!(matching);
}
output["file"] = serde_json::json!(path.display().to_string());
}

println!(
"{}",
serde_json::to_string_pretty(&output).unwrap_or_default()
);
println!(
"{}",
serde_json::to_string_pretty(&output).unwrap_or_default()
);
}

fn explain_tensor(tensor_name: &str, file: Option<&Path>, json: bool) {
// GH-510: Respect --json flag for tensor explanations
if json {
explain_tensor_json(tensor_name, file);
return;
}

Expand Down
4 changes: 3 additions & 1 deletion crates/apr-cli/src/commands/profile_ollama.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,9 @@ fn safe_ratio(num: f64, denom: f64) -> f64 {
/// Printed in place of a memory-vs-compute verdict while the arithmetic
/// intensity is not measured. A magnitude may be reported; a verdict decided by
/// a number nothing measured may not.
#[cfg(feature = "inference")]
// Not gated: a sentinel string compared by the ungated
// `print_roofline_section`, which the gate left unresolvable without
// `inference`.
pub(crate) const ROOFLINE_AI_UNMEASURED: &str = "UNMEASURED";

/// Does the arithmetic intensity actually depend on the workload?
Expand Down
10 changes: 10 additions & 0 deletions crates/apr-cli/src/commands/test_llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,7 @@ use crate::LlmSubcommand;
///
/// # Errors
/// Propagates whichever mode ran.
#[cfg(feature = "tokio")]
pub fn dispatch(command: &LlmSubcommand) -> Result<()> {
let rt = tokio::runtime::Runtime::new()
.map_err(|e| CliError::InferenceFailed(format!("tokio runtime: {e}")))?;
Expand All @@ -420,6 +421,15 @@ pub fn dispatch(command: &LlmSubcommand) -> Result<()> {
}
}

/// Both modes are async and need a runtime to drive. Without `tokio` there is
/// none, so the command refuses by name instead of failing to link.
#[cfg(not(feature = "tokio"))]
pub fn dispatch(_command: &LlmSubcommand) -> Result<()> {
Err(CliError::InferenceFailed(
"`apr test llm` needs an async runtime; rebuild with --features inference".to_string(),
))
}

/// TWO MODES, ONE ENTRYPOINT — the §4.4-conformant one.
///
/// `--band` selects PERF-024's `run_band`, which until PERF-025 was called by
Expand Down
6 changes: 6 additions & 0 deletions crates/apr-cli/src/commands/test_llm_band.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1770,7 +1770,13 @@ async fn cool(first_lane: &mut bool, cooldown: std::time::Duration, interleaved:
*first_lane = false;
return;
}
// Unreachable without `tokio` — `dispatch` refuses before any band runs —
// but the path still has to compile, and a silently-skipped cooldown would
// be a §5.1 protocol violation if it ever did run.
#[cfg(feature = "tokio")]
tokio::time::sleep(cooldown).await;
#[cfg(not(feature = "tokio"))]
let _ = cooldown;
}

/// Run the §5.1 protocol over every requested band and write the receipts.
Expand Down
Loading
Loading