Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
85cf9a3
fix(guard): check_pr_review_counts.sh was RED on main and ran nowhere…
noahgift Sep 21, 2026
f4fcf33
Merge remote-tracking branch 'origin/main' into PMAT-3646-pr-review-c…
noahgift Sep 21, 2026
e3ceb27
PMAT-3076: F16/BF16 CPU matvec reads the row with AVX2+F16C+FMA inste…
noahgift Sep 21, 2026
eebd642
feat(release): batching is the default merge path — scripts/batch_fol…
noahgift Sep 21, 2026
62ae1b6
PMAT-3673: AD-04 quorum receipt for #3674 — 3/3 PASS, all three lanes…
noahgift Sep 21, 2026
135bcfb
PMAT-3076: the greedy-parity gate against pinned llama.cpp, and the m…
noahgift Sep 21, 2026
4645aea
PMAT-3076: the parity gate is a host-side gate, not a tree guard; it …
noahgift Sep 21, 2026
e5b2e75
fix(tokenizer): the unknown token is the model's, not BPE's — Qwen3.5…
noahgift Sep 21, 2026
e80df17
PMAT-3076: wire the parity gate. The case table runs on every PR, the…
noahgift Sep 21, 2026
97093d2
PMAT-3076: say in the declaration who runs it; round 4's two FAILs re…
noahgift Sep 21, 2026
7acdb8f
PMAT-3076: AD-04 quorum receipt, 3/3 PASS on 97093d2a5
noahgift Sep 21, 2026
9cc89e3
roadmap(PMAT-3609): the three repo-layout facts round 2 lane 1 misrea…
noahgift Sep 21, 2026
8964ed3
WIP PMAT-3672: single chat-template application (parked for P0 #3685)
noahgift Sep 21, 2026
e2d12bd
ci(diet): coverage on tags only, perf-gate table path-scoped, model s…
noahgift Sep 21, 2026
3e7a88c
audit(PMAT-3609): quorum round 3 receipt, judged at 9cc89e3a9
noahgift Sep 21, 2026
f7b25a6
ci(diet): quorum round 1 findings — book nightly + path-filter RULE 3…
noahgift Sep 21, 2026
64a153c
fix(guard): a pinned pv that panics was UNMEASURED fleet state — only…
noahgift Sep 21, 2026
0517e75
fix(cli): a model file that fails to parse is named by its magic byte…
noahgift Sep 21, 2026
6fd2416
fix(release): the publish universe's size is derived, never 74 — ORDE…
noahgift Sep 21, 2026
ea0861e
Merge remote-tracking branch 'origin/main' into PMAT-3646-pr-review-c…
noahgift Sep 21, 2026
62ae8ea
fix(guard): check_pr_review_counts.sh leaves the unwired-guards ledge…
noahgift Sep 21, 2026
b6282d1
PMAT-3672: apr run templates the prompt ONCE, in realizar; --chat tra…
noahgift Sep 21, 2026
1d58252
Merge commit '62ae1b628' into release/0.70-batch-1
noahgift Sep 21, 2026
7ef78c0
Merge commit '64a153c00' into release/0.70-batch-1
noahgift Sep 21, 2026
a9c4c9d
Merge commit '62ae8ea3c' into release/0.70-batch-1
noahgift Sep 21, 2026
e54f68c
Merge commit '3e7a88cd0' into release/0.70-batch-1
noahgift Sep 21, 2026
56aa766
Merge commit '6fd241645' into release/0.70-batch-1
noahgift Sep 21, 2026
4abf137
Merge commit '7acdb8f0b' into release/0.70-batch-1
noahgift Sep 21, 2026
6dccdc4
batch: regenerate the generated set once (batch_fold.sh --regen)
noahgift Sep 21, 2026
9a0c3cb
Merge commit 'f7b25a6c6' into release/0.70-batch-1
noahgift Sep 21, 2026
b97b84b
Merge commit '0517e75a7' into release/0.70-batch-1
noahgift Sep 21, 2026
5b72bb6
batch: regenerate the generated set once (batch_fold.sh --regen)
noahgift Sep 21, 2026
c995801
fix(release): a blank line in publish-order.txt is refused by its lin…
noahgift Sep 21, 2026
59f7fe7
Merge commit 'c995801d5' into release/0.70-batch-1
noahgift Sep 21, 2026
fe8fbf7
Merge commit 'b6282d187' into release/0.70-batch-1
noahgift Sep 21, 2026
f31b127
batch: regenerate the generated set once (batch_fold.sh --regen)
noahgift Sep 21, 2026
44ff6ea
Merge remote-tracking branch 'origin/main' into release/0.70-batch-1
noahgift Sep 21, 2026
0a8fc20
batch: regenerate the generated set once (batch_fold.sh --regen)
noahgift Sep 21, 2026
32dd7b6
Merge origin/main (225b2a9ab, the v0.69.0 bump) into release/0.70-bat…
noahgift Sep 21, 2026
a48bb55
fix(batch-1): float16_greedy_parity.sh's two messages named `apr` as …
noahgift Sep 21, 2026
eb454d4
Revert "Merge commit 'f7b25a6c6' into release/0.70-batch-1"
noahgift Sep 21, 2026
262dfb2
fix(claims): three tok/s literals in AppState docs moved to docs/BEAT…
noahgift Sep 21, 2026
9ac424e
merge origin/main (#3706) into release/0.70-batch-1 — roadmap resolve…
noahgift Sep 21, 2026
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
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -624,6 +624,14 @@ gates = [
# silence. RED when evidence/perf041/<host>/marker.json is absent, is not
# PASS, or is older than perf-matrix.yaml `witness.max_age_days`.
"scripts/check_perf041_marker.sh",
# #3076: apr's F16/BF16 CPU matvec against the pinned llama.cpp, greedy, on the
# template-free models its committed receipts name (evidence/pmat-3076-f16-bf16-matvec/).
# A model absent from the release host is a FAIL. This list is
# [package.metadata.dogfood].gates: only `scripts/dogfood.sh --phase pre-publish`
# executes it, at release time, on the host that has the models. Nothing on the PR path
# runs this entry. The PR-time half, the comparison helpers' case table, is
# scripts/check_float16_greedy_parity.sh, which guard_tree.sh dispatches.
"scripts/float16_greedy_parity.sh",
]

# The interfaces this release SHIPS, each paired with the e2e target that proves
Expand Down
13 changes: 13 additions & 0 deletions crates/apr-cli/src/commands/hex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,19 @@ fn format_display_name(fmt: FileFormat) -> &'static str {
}
}

/// The format a model file's magic bytes identify, for an error message:
/// "APR", "GGUF", "SafeTensors", or "model" when they identify none or the
/// file cannot be read (#3661). Reads at most 9 bytes.
pub(crate) fn detected_model_format(path: &Path) -> &'static str {
use std::io::Read;
let mut head = Vec::with_capacity(9);
let read = std::fs::File::open(path).and_then(|f| f.take(9).read_to_end(&mut head));
match read {
Ok(_) => detect_format(&head).map_or("model", format_display_name),
Err(_) => "model",
}
}

// ============================================================================
// Main entry point
// ============================================================================
Expand Down
3 changes: 2 additions & 1 deletion crates/apr-cli/src/commands/inference_output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,8 @@ fn execute_with_realizar(
.with_top_p(options.top_p)
.with_seed(options.seed)
.with_repeat_penalty(options.repeat_penalty)
.with_repeat_last_n(options.repeat_last_n);
.with_repeat_last_n(options.repeat_last_n)
.with_force_chat_template(options.chat_template);

if options.no_gpu {
config = config.without_gpu();
Expand Down
2 changes: 1 addition & 1 deletion crates/apr-cli/src/commands/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ fn run_rosetta_inspect(
let rosetta = RosettaStone::new();
let report = rosetta
.inspect(path)
.map_err(|e| CliError::InvalidFormat(format!("Inspection failed: {e}")))?;
.map_err(|e| CliError::invalid_model_file(path, "Inspection failed", &e))?;

if json_output {
output_rosetta_json(path, &report);
Expand Down
3 changes: 3 additions & 0 deletions crates/apr-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ pub(crate) mod mcp;
pub(crate) mod merge;
#[cfg(feature = "training")]
pub(crate) mod model_config;
// #3661: a model parse failure names the format its magic bytes identify.
#[cfg(test)]
mod model_file_error_tests_3661;
#[cfg(feature = "training")]
pub(crate) mod monitor;
#[cfg(feature = "dev")]
Expand Down
143 changes: 143 additions & 0 deletions crates/apr-cli/src/commands/model_file_error_tests_3661.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//! #3661: a model file that fails to parse is named by what its magic bytes say it is.
//!
//! `CliError::InvalidFormat` Displays "Invalid APR format", and `apr inspect` and
//! `apr tensors` wrapped every parse failure in it, so a truncated GGUF was reported
//! as an invalid APR file. Each row drives the real command function and renders the
//! error exactly as `apr` prints it (`error: {e}`, main.rs), then checks three things:
//! the format the message names, exit code 4, and that the core error's
//! "Invalid model format:" prefix never appears under the CLI's own.

use crate::commands::{inspect, tensors};
use crate::error::CliError;
use std::path::{Path, PathBuf};

/// A GGUF written by aprender's own exporter, cut off inside its metadata.
/// That is the issue's input: a real GGUF truncated mid-header.
fn truncated_gguf(dir: &Path) -> PathBuf {
use aprender::format::gguf::{export_tensors_to_gguf, GgmlType, GgufTensor, GgufValue};
let tensors = vec![GgufTensor {
name: "token_embd.weight".to_string(),
shape: vec![4, 8],
dtype: GgmlType::F32,
data: vec![0u8; 4 * 8 * 4],
}];
let metadata = vec![
(
"general.architecture".to_string(),
GgufValue::String("llama".to_string()),
),
(
"general.name".to_string(),
GgufValue::String("truncated-3661".to_string()),
),
];
let mut bytes = Vec::new();
export_tensors_to_gguf(&mut bytes, &tensors, &metadata).expect("write GGUF");
// 24 bytes of fixed header (magic, version, two counts), then the first key's u64
// length: cut 4 bytes into it, so the reader hits the issue's own failure,
// "Unexpected EOF reading u64".
let path = dir.join("t.gguf");
std::fs::write(&path, &bytes[..28]).expect("write truncated GGUF");
path
}

/// 4 KiB of noise named `.safetensors`, opening with the issue's own magic bytes
/// (`8e44f12f`). Its first u64 is far past any real header length, so no format matches.
fn random_safetensors(dir: &Path) -> PathBuf {
let mut state: u32 = 0x3661;
let mut bytes: Vec<u8> = vec![0x8e, 0x44, 0xf1, 0x2f, 0x9d, 0x27, 0x6b, 0xb3];
while bytes.len() < 4096 {
state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
bytes.push((state >> 24) as u8);
}
let path = dir.join("t.safetensors");
std::fs::write(&path, &bytes).expect("write random bytes");
path
}

/// A real APR v2 file cut to 32 bytes, less than its 64-byte header.
fn truncated_apr(dir: &Path) -> PathBuf {
use aprender::format::v2::{AprV2Metadata, AprV2Writer};
let mut writer = AprV2Writer::new(AprV2Metadata::new("truncated-3661"));
writer.add_f32_tensor("w", vec![2, 2], &[0.0, 1.0, 2.0, 3.0]);
let bytes = writer.write().expect("write APR v2");
let path = dir.join("t.apr");
std::fs::write(&path, &bytes[..32]).expect("write truncated APR");
path
}

fn inspect_err(path: &Path) -> CliError {
inspect::run(path, false, false, false, false, false).expect_err("inspect must refuse")
}

fn tensors_err(path: &Path) -> CliError {
tensors::run(path, false, None, false, 0).expect_err("tensors must refuse")
}

#[test]
fn a_parse_failure_names_the_format_the_magic_bytes_identify() {
let dir = tempfile::tempdir().expect("tempdir");
let gguf = truncated_gguf(dir.path());
let st = random_safetensors(dir.path());
let apr = truncated_apr(dir.path());

// (row, the error, the prefix the rendered line must start with)
let rows: Vec<(&str, CliError, &str)> = vec![
(
"inspect truncated GGUF",
inspect_err(&gguf),
"error: Invalid GGUF file: ",
),
(
"tensors truncated GGUF",
tensors_err(&gguf),
"error: Invalid GGUF file: ",
),
(
"inspect random .safetensors",
inspect_err(&st),
"error: Invalid model file: ",
),
(
"tensors random .safetensors",
tensors_err(&st),
"error: Invalid model file: ",
),
// A truncated .apr still says APR, so the fix is not "delete the word".
(
"inspect truncated APR",
inspect_err(&apr),
"error: Invalid APR ",
),
(
"tensors truncated APR",
tensors_err(&apr),
"error: Invalid APR file: ",
),
];

let mut failures = Vec::new();
for (row, err, want_prefix) in &rows {
let rendered = format!("error: {err}");
let is_apr = want_prefix.starts_with("error: Invalid APR");
if !rendered.starts_with(want_prefix) {
failures.push(format!("{row}: wanted `{want_prefix}…`, got `{rendered}`"));
}
if !is_apr && rendered.contains("Invalid APR") {
failures.push(format!("{row}: calls a non-APR file APR: `{rendered}`"));
}
if rendered.contains("Invalid model format") {
failures.push(format!(
"{row}: the core prefix leaked under the CLI's: `{rendered}`"
));
}
if err.exit_code_value() != 4 {
failures.push(format!("{row}: exit {} (wanted 4)", err.exit_code_value()));
}
}
assert!(
failures.is_empty(),
"#3661 rows failed:\n{}",
failures.join("\n")
);
}
2 changes: 1 addition & 1 deletion crates/apr-cli/src/commands/profile_safetensors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ fn profile_safetensors_real(
let rosetta = aprender::format::rosetta::RosettaStone::new();
let report = rosetta
.inspect(path)
.map_err(|e| CliError::InvalidFormat(format!("Inspection failed: {e}")))?;
.map_err(|e| CliError::invalid_model_file(path, "Inspection failed", &e))?;

let num_layers = report
.tensors
Expand Down
4 changes: 4 additions & 0 deletions crates/apr-cli/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ pub(crate) struct RunOptions {
pub repeat_last_n: usize,
/// Process prompt tokens one-by-one (debug prefill)
pub split_prompt: bool,
/// #3672: apply the model's chat template (`--chat`, or an instruct/chat source name)
/// even when its metadata and file name say base model. The prompt itself stays raw.
pub chat_template: bool,
/// `--stream`: emit one NDJSON event per generated token.
///
/// Known here (not only at the print site) because streaming is the one
Expand Down Expand Up @@ -202,6 +205,7 @@ impl Default for RunOptions {
repeat_penalty: 1.0,
repeat_last_n: 64,
split_prompt: false,
chat_template: false,
stream: false,
}
}
Expand Down
3 changes: 3 additions & 0 deletions crates/apr-cli/src/commands/run_entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ pub(crate) fn run(
repeat_penalty: f32,
repeat_last_n: usize,
split_prompt: bool,
// #3672: apply the model's chat template once, in realizar; the prompt is raw text.
chat_template: bool,
) -> Result<()> {
// GH-516: Warn on --language/--task since whisper integration is not yet wired up
if language.is_some() {
Expand Down Expand Up @@ -105,6 +107,7 @@ pub(crate) fn run(
repeat_penalty,
repeat_last_n,
split_prompt,
chat_template,
stream,
};

Expand Down
10 changes: 8 additions & 2 deletions crates/apr-cli/src/commands/serve/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,14 @@ pub(crate) fn load_safetensors_tokenizer(path: &Path) -> Option<SafeTensorsToken
let mut vocab = vocab;
let (bos_token_id, eos_token_id) = merge_special_tokens_into_vocab(added_tokens, &mut vocab);

// Create BPE tokenizer with vocab and merge rules
let tokenizer = realizar::tokenizer::BPETokenizer::new(vocab.clone(), merges, "<unk>").ok()?;
// Create BPE tokenizer with vocab and merge rules. #3609: the unknown token is the one
// tokenizer.json DECLARES (`model.unk_token`, null for byte-level models like Qwen),
// not a literal "<unk>" every model is assumed to have.
let unk_token = json
.get("model")
.and_then(|m| m.get("unk_token"))
.and_then(|v| v.as_str());
let tokenizer = realizar::tokenizer::BPETokenizer::new(vocab.clone(), merges, unk_token).ok()?;

Some(SafeTensorsTokenizerInfo {
tokenizer: std::sync::Arc::new(tokenizer),
Expand Down
43 changes: 22 additions & 21 deletions crates/apr-cli/src/commands/serve/handler_gpu_completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ fn start_gguf_server(model_path: &Path, config: &ServerConfig) -> Result<()> {
.green()
);

let vocab = extract_gguf_vocab(&mapped_model, quantized_model.config().vocab_size);
let vocab = extract_gguf_vocab(&mapped_model)?;

// PERF-021 / N4 / I-2: a request has a RESOLUTION, and it is reported.
//
Expand Down Expand Up @@ -463,26 +463,27 @@ fn start_gguf_server(model_path: &Path, config: &ServerConfig) -> Result<()> {
run_cpu_server(quantized_model, vocab, Some(mapped_model), config, Some(offload))
}

/// Extract vocabulary from GGUF model, falling back to placeholder tokens.
/// Extract the vocabulary from a GGUF model, or refuse to serve it (#3609).
///
/// GH-226: When the GGUF lacks `tokenizer.ggml.tokens` metadata, the placeholder
/// vocabulary must include `<unk>` so that `BPETokenizer::new` can find it.
/// Without this, the serve path fails with "Unknown token '<unk>' not in vocabulary"
/// and the server exits before binding — causing "Server failed to become ready".
fn extract_gguf_vocab(
mapped_model: &realizar::gguf::MappedGGUFModel,
vocab_size: usize,
) -> Vec<String> {
mapped_model.model.vocabulary().unwrap_or_else(|| {
eprintln!("Warning: No vocabulary in GGUF, using placeholder tokens");
let mut vocab: Vec<String> = (0..vocab_size).map(|i| format!("token{i}")).collect();
// GH-226: Ensure <unk> is present for BPETokenizer compatibility.
// Use slot 0 (standard convention for unknown token).
if !vocab.is_empty() {
vocab[0] = "<unk>".to_string();
}
vocab
})
/// GH-226 once filled a missing `tokenizer.ggml.tokens` with `token0..tokenN` and put
/// `<unk>` in slot 0, because `BPETokenizer::new` required one. So a model with NO
/// vocabulary loaded, while a model with a real vocabulary that lacked `<unk>` (Qwen3.5)
/// was refused: the less-specified input was the one accepted. The unknown token is
/// now optional, and a model with no vocabulary refuses by name. Placeholder tokens
/// are not a tokenizer.
fn extract_gguf_vocab(mapped_model: &realizar::gguf::MappedGGUFModel) -> Result<Vec<String>> {
mapped_model
.model
.vocabulary()
.ok_or_else(|| no_vocabulary("the GGUF (no tokenizer.ggml.tokens)"))
}

/// #3609: the refusal every serve path gives for a model with no vocabulary.
fn no_vocabulary(what: &str) -> CliError {
CliError::ModelLoadFailed(format!(
"{what} has no vocabulary to tokenize with, so there is nothing to serve; \
refusing to substitute placeholder tokens (#3609)"
))
}

/// #2762: resolve the KV-cache context length for the GGUF + CUDA serve path.
Expand Down Expand Up @@ -651,7 +652,7 @@ fn start_gguf_server_cuda(
let quantized_model = OwnedQuantizedModel::from_mapped(&mapped_model).map_err(|e| {
CliError::ModelLoadFailed(format!("Failed to rebuild quantized model: {e}"))
})?;
let vocab = extract_gguf_vocab(&mapped_model, quantized_model.config().vocab_size);
let vocab = extract_gguf_vocab(&mapped_model)?;
// CUDA init failed and this process fell back to CPU. The offload
// report travels with it UNCHANGED, so `/v1/effective-config` shows
// `gpu_layers_resolved` beside `backend_loaded: ["cpu"]` — which is
Expand Down
27 changes: 8 additions & 19 deletions crates/apr-cli/src/commands/serve/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -594,16 +594,13 @@ fn wgpu_smoke_test(
}

/// PMAT-340: Extract the real vocab from the GGUF for tokenize/detokenize.
/// #3609: a GGUF with no vocabulary refuses by name (see `extract_gguf_vocab`).
#[cfg(feature = "wgpu")]
fn wgpu_vocabulary(mapped: &realizar::gguf::MappedGGUFModel, vocab_size: usize) -> Vec<String> {
mapped.model.vocabulary().unwrap_or_else(|| {
eprintln!("Warning: No vocabulary in GGUF, using placeholder");
let mut v: Vec<String> = (0..vocab_size).map(|i| format!("token{i}")).collect();
if !v.is_empty() {
v[0] = "<unk>".to_string();
}
v
})
fn wgpu_vocabulary(mapped: &realizar::gguf::MappedGGUFModel) -> Result<Vec<String>> {
mapped
.model
.vocabulary()
.ok_or_else(|| no_vocabulary("the GGUF (no tokenizer.ggml.tokens)"))
}

/// PMAT-341: Build a BPE tokenizer from the GGUF merge rules.
Expand Down Expand Up @@ -822,7 +819,7 @@ fn serve_wgpu_backend(
println!("{}", "Starting WGPU inference server...".cyan());

// PMAT-340: Extract real vocab from GGUF for tokenization/detokenization
let vocab = wgpu_vocabulary(mapped, vocab_size);
let vocab = wgpu_vocabulary(mapped)?;
// PMAT-341: Extract BPE merge rules for proper tokenization
let merges = mapped.model.merge_rules().unwrap_or_default();
println!(
Expand Down Expand Up @@ -1443,15 +1440,7 @@ fn try_apr_quantized_cpu(model_path: &Path, config: &ServerConfig) -> Result<()>
let vocab = mapped
.metadata
.get_embedded_vocabulary()
.unwrap_or_else(|| {
let vocab_size = mapped.metadata.vocab_size.unwrap_or(32000);
eprintln!("Warning: No embedded vocabulary in APR, using placeholder tokens");
let mut v: Vec<String> = (0..vocab_size).map(|i| format!("token{i}")).collect();
if !v.is_empty() {
v[0] = "<unk>".to_string();
}
v
});
.ok_or_else(|| no_vocabulary("the APR file (no embedded vocabulary)"))?;

println!("{}", "Q4K CPU inference ready".green());

Expand Down
Loading
Loading