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
17 changes: 15 additions & 2 deletions crates/apr-cli/src/commands/inference_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ struct InferenceOutput {
/// Populated only when `--stream` asked for it (see
/// [`decode_token_pieces`]) — every other mode renders the whole `text`.
token_texts: Option<Vec<String>>,
/// Prompt and completion counts plus the finish reason (#3718).
usage: RunUsage,
}

/// Execute inference on model
Expand Down Expand Up @@ -231,6 +233,7 @@ fn execute_inference(
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: RunUsage::default(),
})
}
}
Expand Down Expand Up @@ -299,7 +302,8 @@ fn execute_with_realizar(
options: &RunOptions,
_use_mmap: bool,
) -> Result<InferenceOutput> {
use realizar::{run_inference, InferenceConfig};
use realizar::infer::run_inference_report;
use realizar::InferenceConfig;

// Get prompt from options or input file
let prompt = if let Some(ref p) = options.prompt {
Expand Down Expand Up @@ -343,7 +347,10 @@ fn execute_with_realizar(
}

// Run inference via realizar
let result = run_inference(&config).map_err(inference_error)?;
// #3718: the report carries what only the decode path knows (finish reason,
// context window); the prompt count is `input_token_count`, taken after the
// chat template, so it is the number the model actually read.
let (result, report) = run_inference_report(&config).map_err(inference_error)?;

// Report performance if benchmarking
if options.benchmark {
Expand Down Expand Up @@ -381,6 +388,12 @@ fn execute_with_realizar(
used_gpu: Some(result.used_gpu),
generated_tokens,
token_texts,
usage: RunUsage {
prompt_tokens: Some(result.input_token_count),
completion_tokens: Some(result.generated_token_count),
finish_reason: report.finish_reason.map(|r| r.as_str()),
context_length: report.context_length,
},
})
}

Expand Down
22 changes: 22 additions & 0 deletions crates/apr-cli/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,25 @@ impl Default for RunOptions {
}
}

/// What `apr run --json` reports about the prompt and how generation ended (#3718).
///
/// RAH (#3716) rebuilt Qwen3.5's tokenizer from the GGUF just to learn how many
/// prompt tokens the model saw, and could not tell a reply cut at `--max-tokens`
/// from one that finished. Every field is `None` when the inference path did not
/// report it, which is `null` in the JSON and never a zero that reads as measured.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct RunUsage {
/// Tokens actually fed to the model: after the chat template, including BOS.
pub prompt_tokens: Option<usize>,
/// Tokens the engine generated. Unlike `RunResult::tokens_generated`, never a
/// word-count stand-in.
pub completion_tokens: Option<usize>,
/// `"stop"` (a stop token ended it) or `"length"` (the budget did).
pub finish_reason: Option<&'static str>,
/// The model's context window, from its metadata.
pub context_length: Option<usize>,
}

/// Run result
#[derive(Debug, Clone)]
pub(crate) struct RunResult {
Expand All @@ -230,6 +249,8 @@ pub(crate) struct RunResult {
/// could be resolved. `--stream` used to emit `"text":""` for every token
/// because nothing ever decoded the ids one at a time.
pub token_texts: Option<Vec<String>>,
/// Prompt and completion counts plus the finish reason (#3718).
pub usage: RunUsage,
}

/// Resolve a user-supplied model argument into a [`ModelSource`].
Expand Down Expand Up @@ -316,6 +337,7 @@ pub(crate) fn run_model(source: &str, options: &RunOptions) -> Result<RunResult>
used_gpu: output.used_gpu,
generated_tokens: output.generated_tokens,
token_texts: output.token_texts,
usage: output.usage,
})
}

Expand Down
1 change: 1 addition & 0 deletions crates/apr-cli/src/commands/run_07.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ include!("run_tests_chrome_trace.rs");
include!("run_tests_layer_trace.rs");
include!("run_tests_stream_output.rs");
include!("run_tests_accel_reconcile.rs");
include!("run_tests_usage_3718.rs");
}
9 changes: 9 additions & 0 deletions crates/apr-cli/src/commands/run_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,15 @@ fn build_final_json(
"inference_time_ms": (result.duration_secs * 1000.0 * 100.0).round() / 100.0,
"used_gpu": result.used_gpu.unwrap_or(false),
"cached": result.cached,
// #3718: what the model read and why it stopped writing. `prompt_tokens` is
// the post-chat-template count (BOS included) fed to the model, so a
// consumer no longer rebuilds the tokenizer to learn it. `finish_reason`
// is "length" when the budget ended the reply, so a cut answer is never
// mistaken for a finished one. Each is null when the path did not report it.
"prompt_tokens": result.usage.prompt_tokens,
"completion_tokens": result.usage.completion_tokens,
"finish_reason": result.usage.finish_reason,
"context_length": result.usage.context_length,
// #3602: `used_gpu: false` alone collapses two different outcomes — "no
// accelerator was asked for" and "one was asked for, attempted, and
// REFUSED at runtime". A consumer cannot tell a CPU run from a rejected
Expand Down
2 changes: 2 additions & 0 deletions crates/apr-cli/src/commands/run_tests_accel_reconcile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ fn a_forced_accelerator_that_ran_on_cpu_is_refused() {
used_gpu: Some(false),
generated_tokens: Some(vec![9707]),
token_texts: None,
usage: Default::default(),
};

let err = reconcile_accelerator(true, &result)
Expand Down Expand Up @@ -148,6 +149,7 @@ fn gpu_result(used_gpu: Option<bool>) -> RunResult {
used_gpu,
generated_tokens: Some(vec![9707]),
token_texts: None,
usage: Default::default(),
}
}

Expand Down
12 changes: 12 additions & 0 deletions crates/apr-cli/src/commands/run_tests_chrome_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
used_gpu: Some(false),
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};

let json = build_chrome_trace_events(&result, "model.gguf", 16, true);
Expand Down Expand Up @@ -54,6 +55,7 @@
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};

let json = build_chrome_trace_events(&result, "empty.gguf", 0, false);
Expand All @@ -73,6 +75,7 @@
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};

let json = build_chrome_trace_events(&result, "model.gguf", 10, false);
Expand All @@ -92,6 +95,7 @@
used_gpu: Some(true),
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};

let json = build_chrome_trace_events(&result, "my-model.gguf", 64, true);
Expand All @@ -111,6 +115,7 @@
used_gpu: Some(false),
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};

let json = build_chrome_trace_events(&result, "model.gguf", 10, false);
Expand Down Expand Up @@ -140,6 +145,7 @@
used_gpu: Some(false),
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};

let json = build_chrome_trace_events(&result, "model.gguf", 10, false);
Expand All @@ -159,6 +165,7 @@
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
let json = build_chrome_trace_events(&result, "m.gguf", 1, false);
assert_eq!(json["displayTimeUnit"], "ms");
Expand All @@ -178,6 +185,7 @@
used_gpu: Some(false),
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
// Just ensure no panic; file creation is best-effort
print_chrome_trace(&result, "test-model.gguf", 32, false, None);
Expand Down Expand Up @@ -210,6 +218,7 @@
used_gpu: Some(false),
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
print_chrome_trace(&result, "test-model.gguf", 32, false, Some(&target));

Expand Down Expand Up @@ -239,6 +248,7 @@
used_gpu: Some(false),
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
print_benchmark_results(&result, "model.gguf", "text", 100);
}
Expand All @@ -254,6 +264,7 @@
used_gpu: Some(false),
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
print_benchmark_results(&result, "model.gguf", "json", 50);
}
Expand All @@ -269,6 +280,7 @@
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
print_benchmark_results(&result, "model.gguf", "text", 10);
}
Expand Down
2 changes: 2 additions & 0 deletions crates/apr-cli/src/commands/run_tests_inference_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
used_gpu: Some(false),
generated_tokens: Some(vec![1, 2, 3, 4, 5]),
token_texts: None,
usage: Default::default(),
};
assert_eq!(output.text, "hello");
assert_eq!(output.tokens_generated, Some(5));
Expand All @@ -32,6 +33,7 @@
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
assert!(output.tokens_generated.is_none());
assert!(output.inference_ms.is_none());
Expand Down
1 change: 1 addition & 0 deletions crates/apr-cli/src/commands/run_tests_layer_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
used_gpu: Some(false),
generated_tokens: None,
token_texts: None,
usage: Default::default(),
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/apr-cli/src/commands/run_tests_model_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
let _debug = format!("{:?}", result);
assert_eq!(result.tokens_generated, Some(5));
Expand Down Expand Up @@ -191,6 +192,7 @@
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
let cloned = result.clone();
assert_eq!(result.text, cloned.text);
Expand Down
3 changes: 3 additions & 0 deletions crates/apr-cli/src/commands/run_tests_parse_token_ids.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
let result_zero = RunResult {
text: String::new(),
Expand All @@ -242,6 +243,7 @@
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};
assert_ne!(
result_none.tokens_generated, result_zero.tokens_generated,
Expand All @@ -262,6 +264,7 @@
used_gpu: Some(true),
generated_tokens: Some(vec![10, 20, 30]),
token_texts: None,
usage: Default::default(),
};
assert_eq!(result.text, "output");
assert!((result.duration_secs - 1.234).abs() < f64::EPSILON);
Expand Down
6 changes: 6 additions & 0 deletions crates/apr-cli/src/commands/run_tests_stream_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ fn stream_output_emits_n_plus_one_json_lines() {
" world".to_string(),
"!".to_string(),
]),
usage: Default::default(),
};

let mut buf: Vec<u8> = Vec::new();
Expand Down Expand Up @@ -85,6 +86,7 @@ fn stream_token_events_carry_their_own_decoded_text() {
" here".to_string(),
" to help".to_string(),
]),
usage: Default::default(),
};

let mut buf: Vec<u8> = Vec::new();
Expand Down Expand Up @@ -125,6 +127,7 @@ fn stream_token_events_degrade_to_empty_text_without_a_tokenizer() {
used_gpu: Some(false),
generated_tokens: Some(vec![7, 9]),
token_texts: None,
usage: Default::default(),
};

let mut buf: Vec<u8> = Vec::new();
Expand All @@ -150,6 +153,7 @@ fn stream_output_no_tokens_emits_only_final() {
used_gpu: Some(false),
generated_tokens: Some(Vec::new()),
token_texts: None,
usage: Default::default(),
};

let mut buf: Vec<u8> = Vec::new();
Expand All @@ -175,6 +179,7 @@ fn stream_output_none_tokens_emits_only_final() {
used_gpu: None,
generated_tokens: None,
token_texts: None,
usage: Default::default(),
};

let mut buf: Vec<u8> = Vec::new();
Expand All @@ -199,6 +204,7 @@ fn build_final_json_matches_legacy_json_shape() {
used_gpu: Some(true),
generated_tokens: Some(vec![1, 2, 3]),
token_texts: None,
usage: Default::default(),
};
let v = build_final_json(&result, "src.apr", 100, false);
assert_eq!(v["model"], "src.apr");
Expand Down
Loading
Loading