From ef7c0239f82169b732a806234ec6c543732ea08c Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 19:37:51 -0500 Subject: [PATCH 01/15] feat(decoders): add checked Qwen3 rank profile --- crates/decoders/src/error.rs | 26 ++- crates/decoders/src/lib.rs | 2 + crates/decoders/src/qwen3.rs | 334 +++++++++++++++++++++++++----- crates/decoders/src/qwen3_rank.rs | 130 ++++++++++++ 4 files changed, 428 insertions(+), 64 deletions(-) create mode 100644 crates/decoders/src/qwen3_rank.rs diff --git a/crates/decoders/src/error.rs b/crates/decoders/src/error.rs index ddc0524..e4085dc 100644 --- a/crates/decoders/src/error.rs +++ b/crates/decoders/src/error.rs @@ -402,8 +402,8 @@ pub enum Error { location: snafu::Location, }, - /// A Qwen3 embedding metadata value is missing, mistyped, or unsupported. - #[snafu(display("qwen3 embedding metadata `{key}` violates {rule}"))] + /// A Qwen3 profile metadata value is missing, mistyped, or unsupported. + #[snafu(display("qwen3 profile metadata `{key}` violates {rule}"))] Qwen3Metadata { /// Exact GGUF key responsible for the refusal. key: &'static str, @@ -414,8 +414,8 @@ pub enum Error { location: snafu::Location, }, - /// A Qwen3 embedding tensor inventory entry is missing or malformed. - #[snafu(display("qwen3 embedding tensor `{name}` violates {rule}"))] + /// A Qwen3 profile tensor inventory entry is missing or malformed. + #[snafu(display("qwen3 profile tensor `{name}` violates {rule}"))] Qwen3Tensor { /// Exact GGUF tensor name responsible for the refusal. name: String, @@ -426,8 +426,8 @@ pub enum Error { location: snafu::Location, }, - /// A bounded Qwen3 embedding execution request is invalid. - #[snafu(display("qwen3 embedding execution request {requested} violates {rule}"))] + /// A bounded Qwen3 profile execution request is invalid. + #[snafu(display("qwen3 profile execution request {requested} violates {rule}"))] Qwen3Execution { /// Requested token count or other bounded execution value. requested: usize, @@ -438,8 +438,8 @@ pub enum Error { location: snafu::Location, }, - /// Qwen3 embedding execution could not reserve one named local buffer. - #[snafu(display("qwen3 embedding could not reserve {length} values for {target}: {source}"))] + /// Qwen3 profile execution could not reserve one named local buffer. + #[snafu(display("qwen3 profile could not reserve {length} values for {target}: {source}"))] Qwen3Allocation { /// Named local buffer. target: &'static str, @@ -452,10 +452,8 @@ pub enum Error { location: snafu::Location, }, - /// Qwen3 embedding execution produced a non-finite scalar. - #[snafu(display( - "qwen3 embedding arithmetic became non-finite during {stage} at index {index}" - ))] + /// Qwen3 profile execution produced a non-finite scalar. + #[snafu(display("qwen3 profile arithmetic became non-finite during {stage} at index {index}"))] Qwen3Arithmetic { /// Mathematical stage. stage: &'static str, @@ -466,8 +464,8 @@ pub enum Error { location: snafu::Location, }, - /// A checked shared CPU operation rejected Qwen3 embedding execution. - #[snafu(display("qwen3 embedding CPU operation failed: {source}"))] + /// A checked shared CPU operation rejected Qwen3 profile execution. + #[snafu(display("qwen3 profile CPU operation failed: {source}"))] Qwen3Cpu { /// Checked CPU operation failure. source: kernels::Error, diff --git a/crates/decoders/src/lib.rs b/crates/decoders/src/lib.rs index 32c0baa..1bcac49 100644 --- a/crates/decoders/src/lib.rs +++ b/crates/decoders/src/lib.rs @@ -28,9 +28,11 @@ pub mod qwen35_execution; pub mod qwen35_recurrent; mod qwen35_requirements; pub mod qwen35_weights; +pub mod qwen3_rank; pub use crate::error::{Error, Result}; pub use crate::qwen3::{Qwen3Execution, Qwen3Weights}; +pub use crate::qwen3_rank::{Qwen3RankExecution, Qwen3RankWeights}; pub use crate::qwen35::Qwen35StructuralProfile; pub use crate::qwen35_execution::{Qwen35Execution, Qwen35ExecutionPlan, Qwen35LogitSelection}; pub use crate::qwen35_recurrent::Qwen35RecurrentExecution; diff --git a/crates/decoders/src/qwen3.rs b/crates/decoders/src/qwen3.rs index 5f6fbe8..3b4d21e 100644 --- a/crates/decoders/src/qwen3.rs +++ b/crates/decoders/src/qwen3.rs @@ -36,6 +36,22 @@ const POOLING_TYPE: &str = "qwen3.pooling_type"; const TOKEN_EMBEDDING: &str = "token_embd.weight"; const OUTPUT_NORM: &str = "output_norm.weight"; const LAST_POOLING_TYPE: u64 = 3; +pub(crate) const RANK_HEAD: &str = "cls.output.weight"; + +#[derive(Clone, Copy)] +pub(crate) enum Qwen3Profile { + Embedding, + Rank, +} + +impl Qwen3Profile { + const fn pooling_type(self) -> u64 { + match self { + Self::Embedding => LAST_POOLING_TYPE, + Self::Rank => 4, + } + } +} #[derive(Clone, Copy)] enum Shape { @@ -47,6 +63,7 @@ enum Shape { QueryHidden, HiddenFeedForward, FeedForwardHidden, + HiddenTwo, } #[derive(Clone, Copy)] @@ -56,12 +73,18 @@ enum Storage { } #[derive(Clone, Copy)] -struct Role { +pub(crate) struct Role { name: &'static str, shape: Shape, storage: Storage, } +pub(crate) const RANK_HEAD_ROLE: Role = Role { + name: RANK_HEAD, + shape: Shape::HiddenTwo, + storage: Storage::F32OrQ8Matrix, +}; + const GLOBAL_ROLES: &[Role] = &[ Role { name: TOKEN_EMBEDDING, @@ -133,13 +156,61 @@ const BLOCK_ROLES: &[Role] = &[ }, ]; -/// One verified Qwen3 embedding payload with its checked causal geometry. +/// Shared, verified Qwen3 causal body with its checked geometry. #[derive(Debug)] -pub struct Qwen3Weights<'artifact> { +pub(crate) struct Qwen3BodyWeights<'artifact> { payload: &'artifact VerifiedArtifact, layout: Layout, } +impl<'artifact> Qwen3BodyWeights<'artifact> { + pub(crate) fn try_from_verified( + payload: &'artifact VerifiedArtifact, + profile: Qwen3Profile, + profile_roles: &[Role], + ) -> Result { + let layout = Layout::from_artifact(payload, profile)?; + validate_inventory( + payload.observation().tensor_descriptors(), + layout, + profile_roles, + )?; + Ok(Self { payload, layout }) + } + + pub(crate) const fn hidden_width(&self) -> usize { + self.layout.hidden + } + + pub(crate) const fn max_context(&self) -> usize { + self.layout.context + } + + pub(crate) const fn payload(&self) -> &'artifact VerifiedArtifact { + self.payload + } + + pub(crate) fn execution(&self, max_context: usize) -> Result> { + if max_context == 0 || max_context > self.max_context() { + return Qwen3ExecutionSnafu { + requested: max_context, + rule: "max context must be nonzero and no greater than the artifact context", + } + .fail(); + } + Ok(Qwen3Execution { + body: self, + max_context, + }) + } +} + +/// One verified Qwen3 embedding payload with its checked causal geometry. +#[derive(Debug)] +pub struct Qwen3Weights<'artifact> { + body: Qwen3BodyWeights<'artifact>, +} + impl<'artifact> Qwen3Weights<'artifact> { /// Bind one digest-verified GGUF payload to the bounded Qwen3 embedding profile. /// @@ -148,21 +219,21 @@ impl<'artifact> Qwen3Weights<'artifact> { /// Returns [`crate::Error`] when metadata, tensor roles, shapes, or the /// bounded no-output-head profile are not satisfied. pub fn try_from_verified(payload: &'artifact VerifiedArtifact) -> Result { - let layout = Layout::from_artifact(payload)?; - validate_inventory(payload.observation().tensor_descriptors(), layout)?; - Ok(Self { payload, layout }) + Ok(Self { + body: Qwen3BodyWeights::try_from_verified(payload, Qwen3Profile::Embedding, &[])?, + }) } /// Return the artifact-derived hidden-vector width. #[must_use] pub const fn hidden_width(&self) -> usize { - self.layout.hidden + self.body.hidden_width() } /// Return the artifact-derived maximum context length. #[must_use] pub const fn max_context(&self) -> usize { - self.layout.context + self.body.max_context() } /// Create one stateless bounded CPU embedding executor. @@ -172,24 +243,14 @@ impl<'artifact> Qwen3Weights<'artifact> { /// Returns [`crate::Error`] when `max_context` is zero or exceeds the /// verified artifact's declared context length. pub fn execution(&self, max_context: usize) -> Result> { - if max_context == 0 || max_context > self.layout.context { - return Qwen3ExecutionSnafu { - requested: max_context, - rule: "max context must be nonzero and no greater than the artifact context", - } - .fail(); - } - Ok(Qwen3Execution { - weights: self, - max_context, - }) + self.body.execution(max_context) } } /// Stateless Qwen3 causal embedding execution with an explicit caller context bound. #[derive(Debug)] pub struct Qwen3Execution<'weights, 'artifact> { - weights: &'weights Qwen3Weights<'artifact>, + body: &'weights Qwen3BodyWeights<'artifact>, max_context: usize, } @@ -213,10 +274,10 @@ impl Qwen3Execution<'_, '_> { } .fail(); } - let embedding = CheckedMatrix::from_payload(self.weights.payload, TOKEN_EMBEDDING)?; + let embedding = CheckedMatrix::from_payload(self.body.payload(), TOKEN_EMBEDDING)?; let mut hidden = reserve( "token hidden rows", - product(token_ids.len(), self.weights.layout.hidden)?, + product(token_ids.len(), self.body.layout.hidden)?, )?; for token_id in token_ids { let token = usize::try_from(*token_id).map_err(|_| { @@ -228,23 +289,20 @@ impl Qwen3Execution<'_, '_> { })?; hidden.extend(embedding.decode_row(token)?); } - for block in 0..self.weights.layout.blocks { + for block in 0..self.body.layout.blocks { self.run_block(block, &mut hidden)?; } - let final_norm = read_f32_vector( - self.weights.payload, - OUTPUT_NORM, - self.weights.layout.hidden, - )?; + let final_norm = + read_f32_vector(self.body.payload(), OUTPUT_NORM, self.body.layout.hidden)?; let normalized = kernels::cpu_f32::rms_norm( &hidden, &final_norm, token_ids.len(), - self.weights.layout.hidden, - self.weights.layout.epsilon, + self.body.layout.hidden, + self.body.layout.epsilon, ) .context(Qwen3CpuSnafu)?; - let start = product(token_ids.len() - 1, self.weights.layout.hidden)?; + let start = product(token_ids.len() - 1, self.body.layout.hidden)?; let result = normalized.get(start..).ok_or_else(|| { Qwen3ExecutionSnafu { requested: start, @@ -263,31 +321,31 @@ impl Qwen3Execution<'_, '_> { reason = "the checked causal-attention and FFN order is one source-defined transformer block" )] fn run_block(&self, block: usize, hidden: &mut [f32]) -> Result<()> { - let layout = self.weights.layout; + let layout = self.body.layout; let tokens = hidden.len() / layout.hidden; let attn_norm = read_f32_vector( - self.weights.payload, + self.body.payload(), &block_name(block, "attn_norm.weight"), layout.hidden, )?; let q_norm = read_f32_vector( - self.weights.payload, + self.body.payload(), &block_name(block, "attn_q_norm.weight"), layout.head_dim, )?; let k_norm = read_f32_vector( - self.weights.payload, + self.body.payload(), &block_name(block, "attn_k_norm.weight"), layout.head_dim, )?; let q = - CheckedMatrix::from_payload(self.weights.payload, &block_name(block, "attn_q.weight"))?; + CheckedMatrix::from_payload(self.body.payload(), &block_name(block, "attn_q.weight"))?; let k = - CheckedMatrix::from_payload(self.weights.payload, &block_name(block, "attn_k.weight"))?; + CheckedMatrix::from_payload(self.body.payload(), &block_name(block, "attn_k.weight"))?; let v = - CheckedMatrix::from_payload(self.weights.payload, &block_name(block, "attn_v.weight"))?; + CheckedMatrix::from_payload(self.body.payload(), &block_name(block, "attn_v.weight"))?; let output = CheckedMatrix::from_payload( - self.weights.payload, + self.body.payload(), &block_name(block, "attn_output.weight"), )?; let key_cache_len = product(tokens, layout.kv_width)?; @@ -330,18 +388,18 @@ impl Qwen3Execution<'_, '_> { } add_in_place(hidden, &attention, "attention residual")?; let ffn_norm = read_f32_vector( - self.weights.payload, + self.body.payload(), &block_name(block, "ffn_norm.weight"), layout.hidden, )?; let gate = CheckedMatrix::from_payload( - self.weights.payload, + self.body.payload(), &block_name(block, "ffn_gate.weight"), )?; let up = - CheckedMatrix::from_payload(self.weights.payload, &block_name(block, "ffn_up.weight"))?; + CheckedMatrix::from_payload(self.body.payload(), &block_name(block, "ffn_up.weight"))?; let down = CheckedMatrix::from_payload( - self.weights.payload, + self.body.payload(), &block_name(block, "ffn_down.weight"), )?; let mut ffn = reserve("FFN residual", hidden.len())?; @@ -386,16 +444,16 @@ impl Layout { clippy::too_many_lines, reason = "strict metadata parsing keeps Qwen3 profile relations in one auditable authority" )] - fn from_artifact(payload: &VerifiedArtifact) -> Result { + fn from_artifact(payload: &VerifiedArtifact, profile: Qwen3Profile) -> Result { let metadata = payload.observation().metadata(); require_string(metadata, ARCHITECTURE, "qwen3")?; - require_u32(metadata, POOLING_TYPE, "last-token pooling type")? - .eq(&LAST_POOLING_TYPE) + require_u32(metadata, POOLING_TYPE, "profile pooling type")? + .eq(&profile.pooling_type()) .then_some(()) .ok_or_else(|| { Qwen3MetadataSnafu { key: POOLING_TYPE, - rule: "must select last-token pooling type 3", + rule: "must select the requested bounded profile pooling type", } .build() })?; @@ -480,11 +538,16 @@ impl Layout { clippy::too_many_lines, reason = "one role inventory defines the complete bounded Qwen3 embedding tensor contract" )] -fn validate_inventory(tensors: &[loader::gguf::TensorDescriptor], layout: Layout) -> Result<()> { +fn validate_inventory( + tensors: &[loader::gguf::TensorDescriptor], + layout: Layout, + profile_roles: &[Role], +) -> Result<()> { let expected_count = layout .blocks .checked_mul(BLOCK_ROLES.len()) .and_then(|count| count.checked_add(GLOBAL_ROLES.len())) + .and_then(|count| count.checked_add(profile_roles.len())) .ok_or_else(|| { Qwen3ExecutionSnafu { requested: layout.blocks, @@ -507,7 +570,7 @@ fn validate_inventory(tensors: &[loader::gguf::TensorDescriptor], layout: Layout length: expected_count, })?; for tensor in tensors { - let Some(role) = role_for_name(&tensor.name, layout.blocks) else { + let Some(role) = role_for_name(&tensor.name, layout.blocks, profile_roles) else { return Qwen3TensorSnafu { name: tensor.name.clone(), rule: "is outside the bounded embedding role inventory", @@ -554,6 +617,15 @@ fn validate_inventory(tensors: &[loader::gguf::TensorDescriptor], layout: Layout .fail(); } } + for role in profile_roles { + if !found.contains(role.name) { + return Qwen3TensorSnafu { + name: role.name.to_string(), + rule: "is required by the bounded Qwen3 profile", + } + .fail(); + } + } for block in 0..layout.blocks { for role in BLOCK_ROLES { let name = block_name(block, role.name); @@ -586,14 +658,18 @@ impl Shape { Self::QueryHidden => vec![query, hidden], Self::HiddenFeedForward => vec![hidden, feed_forward], Self::FeedForwardHidden => vec![feed_forward, hidden], + Self::HiddenTwo => vec![hidden, 2], }) } } -fn role_for_name(name: &str, blocks: usize) -> Option { +fn role_for_name(name: &str, blocks: usize, profile_roles: &[Role]) -> Option { if let Some(role) = GLOBAL_ROLES.iter().copied().find(|role| role.name == name) { return Some(role); } + if let Some(role) = profile_roles.iter().copied().find(|role| role.name == name) { + return Some(role); + } let remainder = name.strip_prefix("blk.")?; let (block, role_name) = remainder.split_once('.')?; let block = block.parse::().ok()?; @@ -1188,6 +1264,101 @@ mod tests { Ok(()) } + #[test] + fn executes_the_rank_profile_to_ordered_raw_classifier_logits() + -> std::result::Result<(), String> { + let raw = rank_fixture()?; + let artifact = verify(&raw)?; + let weights = crate::Qwen3RankWeights::try_from_verified(&artifact) + .map_err(|error| error.to_string())?; + if weights.hidden_width() + != usize::try_from(TEST_HIDDEN).map_err(|error| error.to_string())? + || weights.max_context() + != usize::try_from(TEST_CONTEXT).map_err(|error| error.to_string())? + { + return Err("rank profile did not retain its artifact geometry".to_string()); + } + let logits = weights + .execution(usize::try_from(TEST_CONTEXT).map_err(|error| error.to_string())?) + .and_then(|execution| execution.last_logits(&[0, 1])) + .map_err(|error| error.to_string())?; + if !logits.iter().all(|value| value.is_finite()) + || logits[0].to_bits() == logits[1].to_bits() + { + return Err( + "rank classifier did not preserve finite distinct yes/no logits".to_string(), + ); + } + Ok(()) + } + + #[test] + fn rejects_rank_profile_metadata_and_inventory_deviations() -> std::result::Result<(), String> { + let cases: [fn(&mut RawGguf); 8] = [ + rank_pooling_as_embedding, + remove_rank_labels, + reverse_rank_labels, + wrong_rank_labels, + wrong_rank_head_shape, + wrong_rank_head_dtype, + extra_rank_lm_head, + rank_causal_false, + ]; + for mutate in cases { + let mut raw = rank_fixture()?; + mutate(&mut raw); + let artifact = verify(&raw)?; + if crate::Qwen3RankWeights::try_from_verified(&artifact).is_ok() { + return Err( + "rank profile accepted a strict metadata or inventory deviation".to_string(), + ); + } + } + let mut scaled = rank_fixture()?; + nonneutral_current_rope_scaling(&mut scaled); + let artifact = verify(&scaled)?; + if crate::Qwen3RankWeights::try_from_verified(&artifact).is_ok() { + return Err("rank profile accepted nonneutral rope scaling".to_string()); + } + Ok(()) + } + + #[test] + fn rank_head_refuses_nonfinite_and_overflowing_projection_without_poisoning_retry() + -> std::result::Result<(), String> { + for value in [f32::NAN, f32::INFINITY, f32::MAX] { + let mut malformed = rank_fixture()?; + let head = find_tensor_mut(&mut malformed, RANK_HEAD)?; + for lane in head.payload[..12].chunks_exact_mut(4) { + lane.copy_from_slice(&value.to_le_bytes()); + } + let artifact = verify(&malformed)?; + let weights = crate::Qwen3RankWeights::try_from_verified(&artifact) + .map_err(|error| error.to_string())?; + let execution = weights + .execution(usize::try_from(TEST_CONTEXT).map_err(|error| error.to_string())?) + .map_err(|error| error.to_string())?; + match execution.last_logits(&[0]) { + Err(crate::Error::ProjectionRow { .. } | crate::Error::Qwen3Arithmetic { .. }) => {} + other => { + return Err(format!( + "rank head nonfinite or overflowing projection was accepted for {value:?}: {other:?}" + )); + } + } + } + let artifact = verify(&rank_fixture()?)?; + let logits = crate::Qwen3RankWeights::try_from_verified(&artifact) + .map_err(|error| error.to_string())? + .execution(usize::try_from(TEST_CONTEXT).map_err(|error| error.to_string())?) + .and_then(|execution| execution.last_logits(&[0])) + .map_err(|error| error.to_string())?; + if !logits.iter().all(|value| value.is_finite()) { + return Err("pristine rank retry produced nonfinite logits".to_string()); + } + Ok(()) + } + #[derive(Clone, Copy)] enum ExpectedProfileError { Metadata(&'static str), @@ -1256,6 +1427,57 @@ mod tests { raw.metadata.retain(|entry| entry.key != HIDDEN); } + fn rank_pooling_as_embedding(raw: &mut RawGguf) { + replace_metadata(raw, POOLING_TYPE, &RawMetadataValue::U32(3)); + } + + fn remove_rank_labels(raw: &mut RawGguf) { + raw.metadata + .retain(|entry| entry.key != "qwen3.classifier.output_labels"); + } + + fn reverse_rank_labels(raw: &mut RawGguf) { + replace_metadata( + raw, + "qwen3.classifier.output_labels", + &RawMetadataValue::StringArray(vec!["no".to_string(), "yes".to_string()]), + ); + } + + fn wrong_rank_labels(raw: &mut RawGguf) { + replace_metadata( + raw, + "qwen3.classifier.output_labels", + &RawMetadataValue::StringArray(vec!["yes".to_string(), "maybe".to_string()]), + ); + } + + fn wrong_rank_head_shape(raw: &mut RawGguf) { + replace_tensor_dimensions(raw, RANK_HEAD, &[TEST_HIDDEN, 1]); + } + + fn wrong_rank_head_dtype(raw: &mut RawGguf) { + for tensor in &mut raw.tensors { + if tensor.name == RANK_HEAD { + tensor.format = 1; + tensor.payload = vec![0; 12]; + } + } + } + + fn extra_rank_lm_head(raw: &mut RawGguf) { + raw.tensors.push(RawTensor { + name: "output.weight".to_string(), + dims: vec![TEST_HIDDEN, TEST_VOCABULARY], + format: 0, + payload: vec![0; 48], + }); + } + + fn rank_causal_false(raw: &mut RawGguf) { + raw.metadata.push(metadata_bool(CAUSAL, false)); + } + fn mistype_hidden(raw: &mut RawGguf) { replace_metadata(raw, HIDDEN, &RawMetadataValue::F32(3.0)); } @@ -1503,6 +1725,18 @@ mod tests { }) } + fn rank_fixture() -> std::result::Result { + let mut raw = fixture()?; + replace_metadata(&mut raw, POOLING_TYPE, &RawMetadataValue::U32(4)); + raw.metadata.push(RawMetadata { + key: "qwen3.classifier.output_labels".to_string(), + value: RawMetadataValue::StringArray(vec!["yes".to_string(), "no".to_string()]), + }); + raw.tensors + .push(tensor(RANK_HEAD, vec![TEST_HIDDEN, 2], 0.3125)?); + Ok(raw) + } + fn metadata_u32(key: &str, value: u32) -> RawMetadata { RawMetadata { key: key.to_string(), diff --git a/crates/decoders/src/qwen3_rank.rs b/crates/decoders/src/qwen3_rank.rs new file mode 100644 index 0000000..a2bf197 --- /dev/null +++ b/crates/decoders/src/qwen3_rank.rs @@ -0,0 +1,130 @@ +//! Bounded native CPU Qwen3 rank-head execution over a verified GGUF payload. + +use loader::gguf::{MetaValue, MetaValueType, VerifiedArtifact}; + +use crate::Result; +use crate::error::{Qwen3ExecutionSnafu, Qwen3MetadataSnafu}; +use crate::matrix::CheckedMatrix; +use crate::qwen3::{Qwen3BodyWeights, Qwen3Profile, RANK_HEAD, RANK_HEAD_ROLE}; + +const CLASSIFIER_OUTPUT_LABELS: &str = "qwen3.classifier.output_labels"; +const RANK_LABELS: [&str; 2] = ["yes", "no"]; + +/// One verified Qwen3 rank payload with its checked causal body and two-row head. +#[derive(Debug)] +pub struct Qwen3RankWeights<'artifact> { + body: Qwen3BodyWeights<'artifact>, +} + +impl<'artifact> Qwen3RankWeights<'artifact> { + /// Bind a verified GGUF payload to the bounded Qwen3 rank profile. + /// + /// # Errors + /// + /// Returns [`crate::Error`] when the causal body, rank metadata, or the + /// exact two-label classifier head does not satisfy the bounded profile. + pub fn try_from_verified(payload: &'artifact VerifiedArtifact) -> Result { + require_rank_labels(payload)?; + Ok(Self { + body: Qwen3BodyWeights::try_from_verified( + payload, + Qwen3Profile::Rank, + &[RANK_HEAD_ROLE], + )?, + }) + } + + /// Return the artifact-derived hidden-vector width. + #[must_use] + pub const fn hidden_width(&self) -> usize { + self.body.hidden_width() + } + + /// Return the artifact-derived maximum context length. + #[must_use] + pub const fn max_context(&self) -> usize { + self.body.max_context() + } + + /// Create one stateless bounded CPU rank executor. + /// + /// # Errors + /// + /// Returns [`crate::Error`] when `max_context` is zero or exceeds the + /// verified artifact's declared context length. + pub fn execution(&self, max_context: usize) -> Result> { + Ok(Qwen3RankExecution { + weights: self, + body_execution: self.body.execution(max_context)?, + }) + } +} + +/// Stateless Qwen3 causal rank execution with an explicit caller context bound. +#[derive(Debug)] +pub struct Qwen3RankExecution<'weights, 'artifact> { + weights: &'weights Qwen3RankWeights<'artifact>, + body_execution: crate::qwen3::Qwen3Execution<'weights, 'artifact>, +} + +impl Qwen3RankExecution<'_, '_> { + /// Return raw final-token classifier logits in the verified `[yes, no]` order. + /// + /// The signed relevance-score reduction belongs to the reranker adapter; + /// this decoder boundary only executes the admitted two-row head. + /// + /// # Errors + /// + /// Returns [`crate::Error`] without exposing partial hidden state or a + /// partial classifier result when body execution or projection fails. + pub fn last_logits(&self, token_ids: &[u32]) -> Result<[f32; 2]> { + let hidden = self.body_execution.last_hidden(token_ids)?; + let head = CheckedMatrix::from_payload(self.weights.body.payload(), RANK_HEAD)?; + let logits: [f32; 2] = head + .project(&hidden)? + .try_into() + .map_err(|values: Vec| { + Qwen3ExecutionSnafu { + requested: values.len(), + rule: "the admitted rank classifier head must project exactly two logits", + } + .build() + })?; + for (index, logit) in logits.iter().enumerate() { + if !logit.is_finite() { + return crate::error::Qwen3ArithmeticSnafu { + stage: "rank classifier projection", + index, + } + .fail(); + } + } + Ok(logits) + } +} + +fn require_rank_labels(payload: &VerifiedArtifact) -> Result<()> { + let metadata = payload.observation().metadata(); + let Some(MetaValue::Array(labels)) = metadata.get(CLASSIFIER_OUTPUT_LABELS) else { + return Qwen3MetadataSnafu { + key: CLASSIFIER_OUTPUT_LABELS, + rule: "must be the exact string array [yes, no]", + } + .fail(); + }; + if labels.element_type() != MetaValueType::String + || labels.values().len() != RANK_LABELS.len() + || !labels + .values() + .iter() + .zip(RANK_LABELS) + .all(|(label, expected)| matches!(label, MetaValue::String(value) if value == expected)) + { + return Qwen3MetadataSnafu { + key: CLASSIFIER_OUTPUT_LABELS, + rule: "must be the exact string array [yes, no] in source label order", + } + .fail(); + } + Ok(()) +} From a6760db5942837d110babc35e959c1dcf29a98d9 Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 19:42:06 -0500 Subject: [PATCH 02/15] fix(decoders): bind Qwen3 rank roles to profile --- crates/decoders/src/qwen3.rs | 187 +++++++++++++++++++++++------- crates/decoders/src/qwen3_rank.rs | 17 +-- 2 files changed, 151 insertions(+), 53 deletions(-) diff --git a/crates/decoders/src/qwen3.rs b/crates/decoders/src/qwen3.rs index 3b4d21e..5a07fc0 100644 --- a/crates/decoders/src/qwen3.rs +++ b/crates/decoders/src/qwen3.rs @@ -1,4 +1,4 @@ -//! Bounded native CPU Qwen3 embedding execution over one verified GGUF payload. +//! Bounded native CPU Qwen3 causal execution and embedding-profile admission. use std::collections::{HashMap, HashSet}; use std::num::NonZeroUsize; @@ -37,6 +37,8 @@ const TOKEN_EMBEDDING: &str = "token_embd.weight"; const OUTPUT_NORM: &str = "output_norm.weight"; const LAST_POOLING_TYPE: u64 = 3; pub(crate) const RANK_HEAD: &str = "cls.output.weight"; +pub(crate) const RANK_LABELS_KEY: &str = "qwen3.classifier.output_labels"; +pub(crate) const RANK_LABELS: [&str; 2] = ["yes", "no"]; #[derive(Clone, Copy)] pub(crate) enum Qwen3Profile { @@ -51,6 +53,13 @@ impl Qwen3Profile { Self::Rank => 4, } } + + const fn profile_roles(self) -> &'static [Role] { + match self { + Self::Embedding => &[], + Self::Rank => &[RANK_HEAD_ROLE], + } + } } #[derive(Clone, Copy)] @@ -63,7 +72,7 @@ enum Shape { QueryHidden, HiddenFeedForward, FeedForwardHidden, - HiddenTwo, + HiddenRankLabels, } #[derive(Clone, Copy)] @@ -81,7 +90,7 @@ pub(crate) struct Role { pub(crate) const RANK_HEAD_ROLE: Role = Role { name: RANK_HEAD, - shape: Shape::HiddenTwo, + shape: Shape::HiddenRankLabels, storage: Storage::F32OrQ8Matrix, }; @@ -167,13 +176,12 @@ impl<'artifact> Qwen3BodyWeights<'artifact> { pub(crate) fn try_from_verified( payload: &'artifact VerifiedArtifact, profile: Qwen3Profile, - profile_roles: &[Role], ) -> Result { let layout = Layout::from_artifact(payload, profile)?; validate_inventory( payload.observation().tensor_descriptors(), layout, - profile_roles, + profile.profile_roles(), )?; Ok(Self { payload, layout }) } @@ -220,7 +228,7 @@ impl<'artifact> Qwen3Weights<'artifact> { /// bounded no-output-head profile are not satisfied. pub fn try_from_verified(payload: &'artifact VerifiedArtifact) -> Result { Ok(Self { - body: Qwen3BodyWeights::try_from_verified(payload, Qwen3Profile::Embedding, &[])?, + body: Qwen3BodyWeights::try_from_verified(payload, Qwen3Profile::Embedding)?, }) } @@ -247,7 +255,7 @@ impl<'artifact> Qwen3Weights<'artifact> { } } -/// Stateless Qwen3 causal embedding execution with an explicit caller context bound. +/// Stateless Qwen3 causal execution with an explicit caller context bound. #[derive(Debug)] pub struct Qwen3Execution<'weights, 'artifact> { body: &'weights Qwen3BodyWeights<'artifact>, @@ -460,7 +468,7 @@ impl Layout { if matches!(metadata.get(CAUSAL), Some(MetaValue::Bool(false))) { return Qwen3MetadataSnafu { key: CAUSAL, - rule: "explicit false is outside the causal embedding profile", + rule: "explicit false is outside the bounded causal profile", } .fail(); } @@ -573,7 +581,7 @@ fn validate_inventory( let Some(role) = role_for_name(&tensor.name, layout.blocks, profile_roles) else { return Qwen3TensorSnafu { name: tensor.name.clone(), - rule: "is outside the bounded embedding role inventory", + rule: "is outside the bounded profile role inventory", } .fail(); }; @@ -612,7 +620,7 @@ fn validate_inventory( if !found.contains(role.name) { return Qwen3TensorSnafu { name: role.name.to_string(), - rule: "is required by the bounded embedding profile", + rule: "is required by the bounded profile", } .fail(); } @@ -632,7 +640,7 @@ fn validate_inventory( if !found.contains(&name) { return Qwen3TensorSnafu { name, - rule: "is required by the bounded embedding profile", + rule: "is required by the bounded profile", } .fail(); } @@ -658,7 +666,7 @@ impl Shape { Self::QueryHidden => vec![query, hidden], Self::HiddenFeedForward => vec![hidden, feed_forward], Self::FeedForwardHidden => vec![feed_forward, hidden], - Self::HiddenTwo => vec![hidden, 2], + Self::HiddenRankLabels => vec![hidden, u64_from(RANK_LABELS.len())?], }) } } @@ -1294,31 +1302,62 @@ mod tests { #[test] fn rejects_rank_profile_metadata_and_inventory_deviations() -> std::result::Result<(), String> { - let cases: [fn(&mut RawGguf); 8] = [ - rank_pooling_as_embedding, - remove_rank_labels, - reverse_rank_labels, - wrong_rank_labels, - wrong_rank_head_shape, - wrong_rank_head_dtype, - extra_rank_lm_head, - rank_causal_false, + let cases = [ + ProfileCase::metadata("embedding pool", rank_pooling_as_embedding, POOLING_TYPE), + ProfileCase::metadata("missing labels", remove_rank_labels, RANK_LABELS_KEY), + ProfileCase::metadata("typed labels", mistype_rank_labels, RANK_LABELS_KEY), + ProfileCase::metadata("reversed labels", reverse_rank_labels, RANK_LABELS_KEY), + ProfileCase::metadata("wrong labels", wrong_rank_labels, RANK_LABELS_KEY), + ProfileCase::metadata("extra label", extra_rank_label, RANK_LABELS_KEY), + ProfileCase::tensor("missing head", remove_rank_head, "inventory"), + ProfileCase::tensor("wrong head shape", wrong_rank_head_shape, RANK_HEAD), + ProfileCase::tensor("wrong head dtype", wrong_rank_head_dtype, RANK_HEAD), + ProfileCase::tensor("extra classifier bias", extra_rank_bias, "inventory"), + ProfileCase::tensor("extra language head", extra_rank_lm_head, "inventory"), + ProfileCase::tensor("missing trunk tensor", remove_tensor, "inventory"), + ProfileCase::metadata("noncausal", rank_causal_false, CAUSAL), + ProfileCase::metadata( + "nonneutral rope scaling", + nonneutral_current_rope_scaling, + ROPE_SCALING_FACTOR, + ), ]; - for mutate in cases { - let mut raw = rank_fixture()?; - mutate(&mut raw); - let artifact = verify(&raw)?; - if crate::Qwen3RankWeights::try_from_verified(&artifact).is_ok() { - return Err( - "rank profile accepted a strict metadata or inventory deviation".to_string(), - ); - } + for case in cases { + assert_rank_profile_case(case)?; } - let mut scaled = rank_fixture()?; - nonneutral_current_rope_scaling(&mut scaled); - let artifact = verify(&scaled)?; - if crate::Qwen3RankWeights::try_from_verified(&artifact).is_ok() { - return Err("rank profile accepted nonneutral rope scaling".to_string()); + Ok(()) + } + + #[test] + fn rejects_a_duplicate_rank_head_during_gguf_verification() -> std::result::Result<(), String> { + let mut raw = rank_fixture()?; + let Some(head) = raw + .tensors + .iter() + .find(|tensor| tensor.name == RANK_HEAD) + .cloned() + else { + return Err("rank fixture was missing its classifier head".to_string()); + }; + raw.tensors.push(head); + if verify(&raw).is_ok() { + return Err("GGUF verification accepted a duplicate rank classifier head".to_string()); + } + Ok(()) + } + + #[test] + fn embedding_profile_refuses_an_otherwise_valid_rank_artifact() + -> std::result::Result<(), String> { + let artifact = verify(&rank_fixture()?)?; + if !matches!( + Qwen3Weights::try_from_verified(&artifact), + Err(crate::Error::Qwen3Metadata { + key: POOLING_TYPE, + .. + }) + ) { + return Err("embedding profile accepted a valid rank artifact".to_string()); } Ok(()) } @@ -1416,6 +1455,28 @@ mod tests { } } + fn assert_rank_profile_case(case: ProfileCase) -> std::result::Result<(), String> { + let mut raw = rank_fixture()?; + (case.mutate)(&mut raw); + let error = rank_profile_error(&raw)?; + let accepted = match case.expected { + ExpectedProfileError::Metadata(key) => { + matches!(error, crate::Error::Qwen3Metadata { key: actual, .. } if actual == key) + } + ExpectedProfileError::Tensor(name) => { + matches!(error, crate::Error::Qwen3Tensor { name: ref actual, .. } if actual == name) + } + }; + if accepted { + Ok(()) + } else { + Err(format!( + "rank {} returned the wrong typed profile error: {error}", + case.name + )) + } + } + fn profile_error(raw: &RawGguf) -> std::result::Result { let artifact = verify(raw)?; Qwen3Weights::try_from_verified(&artifact) @@ -1423,6 +1484,13 @@ mod tests { .ok_or_else(|| "invalid profile fixture was accepted".to_string()) } + fn rank_profile_error(raw: &RawGguf) -> std::result::Result { + let artifact = verify(raw)?; + crate::Qwen3RankWeights::try_from_verified(&artifact) + .err() + .ok_or_else(|| "invalid rank profile fixture was accepted".to_string()) + } + fn remove_hidden(raw: &mut RawGguf) { raw.metadata.retain(|entry| entry.key != HIDDEN); } @@ -1432,14 +1500,21 @@ mod tests { } fn remove_rank_labels(raw: &mut RawGguf) { - raw.metadata - .retain(|entry| entry.key != "qwen3.classifier.output_labels"); + raw.metadata.retain(|entry| entry.key != RANK_LABELS_KEY); + } + + fn mistype_rank_labels(raw: &mut RawGguf) { + replace_metadata( + raw, + RANK_LABELS_KEY, + &RawMetadataValue::I32Array(vec![1, 2]), + ); } fn reverse_rank_labels(raw: &mut RawGguf) { replace_metadata( raw, - "qwen3.classifier.output_labels", + RANK_LABELS_KEY, &RawMetadataValue::StringArray(vec!["no".to_string(), "yes".to_string()]), ); } @@ -1447,7 +1522,7 @@ mod tests { fn wrong_rank_labels(raw: &mut RawGguf) { replace_metadata( raw, - "qwen3.classifier.output_labels", + RANK_LABELS_KEY, &RawMetadataValue::StringArray(vec!["yes".to_string(), "maybe".to_string()]), ); } @@ -1456,6 +1531,22 @@ mod tests { replace_tensor_dimensions(raw, RANK_HEAD, &[TEST_HIDDEN, 1]); } + fn extra_rank_label(raw: &mut RawGguf) { + replace_metadata( + raw, + RANK_LABELS_KEY, + &RawMetadataValue::StringArray(vec![ + "yes".to_string(), + "no".to_string(), + "maybe".to_string(), + ]), + ); + } + + fn remove_rank_head(raw: &mut RawGguf) { + raw.tensors.retain(|tensor| tensor.name != RANK_HEAD); + } + fn wrong_rank_head_dtype(raw: &mut RawGguf) { for tensor in &mut raw.tensors { if tensor.name == RANK_HEAD { @@ -1474,6 +1565,15 @@ mod tests { }); } + fn extra_rank_bias(raw: &mut RawGguf) { + raw.tensors.push(RawTensor { + name: "cls.output.bias".to_string(), + dims: vec![2], + format: 0, + payload: vec![0; 8], + }); + } + fn rank_causal_false(raw: &mut RawGguf) { raw.metadata.push(metadata_bool(CAUSAL, false)); } @@ -1729,8 +1829,13 @@ mod tests { let mut raw = fixture()?; replace_metadata(&mut raw, POOLING_TYPE, &RawMetadataValue::U32(4)); raw.metadata.push(RawMetadata { - key: "qwen3.classifier.output_labels".to_string(), - value: RawMetadataValue::StringArray(vec!["yes".to_string(), "no".to_string()]), + key: RANK_LABELS_KEY.to_string(), + value: RawMetadataValue::StringArray( + RANK_LABELS + .iter() + .map(|label| (*label).to_string()) + .collect(), + ), }); raw.tensors .push(tensor(RANK_HEAD, vec![TEST_HIDDEN, 2], 0.3125)?); diff --git a/crates/decoders/src/qwen3_rank.rs b/crates/decoders/src/qwen3_rank.rs index a2bf197..2ba570f 100644 --- a/crates/decoders/src/qwen3_rank.rs +++ b/crates/decoders/src/qwen3_rank.rs @@ -5,10 +5,7 @@ use loader::gguf::{MetaValue, MetaValueType, VerifiedArtifact}; use crate::Result; use crate::error::{Qwen3ExecutionSnafu, Qwen3MetadataSnafu}; use crate::matrix::CheckedMatrix; -use crate::qwen3::{Qwen3BodyWeights, Qwen3Profile, RANK_HEAD, RANK_HEAD_ROLE}; - -const CLASSIFIER_OUTPUT_LABELS: &str = "qwen3.classifier.output_labels"; -const RANK_LABELS: [&str; 2] = ["yes", "no"]; +use crate::qwen3::{Qwen3BodyWeights, Qwen3Profile, RANK_HEAD, RANK_LABELS, RANK_LABELS_KEY}; /// One verified Qwen3 rank payload with its checked causal body and two-row head. #[derive(Debug)] @@ -26,11 +23,7 @@ impl<'artifact> Qwen3RankWeights<'artifact> { pub fn try_from_verified(payload: &'artifact VerifiedArtifact) -> Result { require_rank_labels(payload)?; Ok(Self { - body: Qwen3BodyWeights::try_from_verified( - payload, - Qwen3Profile::Rank, - &[RANK_HEAD_ROLE], - )?, + body: Qwen3BodyWeights::try_from_verified(payload, Qwen3Profile::Rank)?, }) } @@ -105,9 +98,9 @@ impl Qwen3RankExecution<'_, '_> { fn require_rank_labels(payload: &VerifiedArtifact) -> Result<()> { let metadata = payload.observation().metadata(); - let Some(MetaValue::Array(labels)) = metadata.get(CLASSIFIER_OUTPUT_LABELS) else { + let Some(MetaValue::Array(labels)) = metadata.get(RANK_LABELS_KEY) else { return Qwen3MetadataSnafu { - key: CLASSIFIER_OUTPUT_LABELS, + key: RANK_LABELS_KEY, rule: "must be the exact string array [yes, no]", } .fail(); @@ -121,7 +114,7 @@ fn require_rank_labels(payload: &VerifiedArtifact) -> Result<()> { .all(|(label, expected)| matches!(label, MetaValue::String(value) if value == expected)) { return Qwen3MetadataSnafu { - key: CLASSIFIER_OUTPUT_LABELS, + key: RANK_LABELS_KEY, rule: "must be the exact string array [yes, no] in source label order", } .fail(); From 135b7cd5b8164f6b010a156bfe3bc9ac0d267f07 Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 19:43:05 -0500 Subject: [PATCH 03/15] test(decoders): add independent Qwen3 rank oracle --- crates/decoders/src/qwen3_oracle_tests.rs | 167 +++++++++++++++++++++- 1 file changed, 166 insertions(+), 1 deletion(-) diff --git a/crates/decoders/src/qwen3_oracle_tests.rs b/crates/decoders/src/qwen3_oracle_tests.rs index 1278eaa..1d07715 100644 --- a/crates/decoders/src/qwen3_oracle_tests.rs +++ b/crates/decoders/src/qwen3_oracle_tests.rs @@ -10,7 +10,7 @@ use std::num::NonZeroU64; use loader::gguf::{ArtifactByteLimit, Sha256Digest, VerifiedArtifact}; use test_fixtures::{RawGguf, RawMetadata, RawMetadataValue, RawTensor, serialize_raw_gguf}; -use crate::Qwen3Weights; +use crate::{Qwen3RankWeights, Qwen3Weights}; type TestResult = std::result::Result; @@ -62,6 +62,9 @@ enum Fault { MissingFfnResidual, MissingFinalNorm, FirstTokenPool, + SwappedRankRows, + NegatedRankHead, + SoftmaxRankHead, } impl Fault { @@ -104,6 +107,18 @@ impl Fault { const fn pools_first_token(self) -> bool { matches!(self, Self::FirstTokenPool) } + + const fn swaps_rank_rows(self) -> bool { + matches!(self, Self::SwappedRankRows) + } + + const fn negates_rank_head(self) -> bool { + matches!(self, Self::NegatedRankHead) + } + + const fn softmaxes_rank_head(self) -> bool { + matches!(self, Self::SoftmaxRankHead) + } } #[derive(Debug)] @@ -184,6 +199,86 @@ fn late_invalid_token_refusal_leaves_stateless_execution_retryable() -> TestResu Ok(()) } +#[test] +fn rank_head_matches_independent_f64_raw_logits_for_f32_and_q8_storage() -> TestResult<()> { + for storage in [Storage::F32, Storage::Q8] { + let fixture = qwen3_rank_fixture(storage)?; + let artifact = verify_fixture(&fixture)?; + let weights = + Qwen3RankWeights::try_from_verified(&artifact).map_err(|error| error.to_string())?; + let actual = weights + .execution(CONTEXT) + .and_then(|execution| execution.last_logits(&TOKEN_IDS)) + .map_err(|error| error.to_string())?; + let expected = oracle_rank_logits(&fixture, &TOKEN_IDS, Fault::None)?; + assert_f32_matches_f64(&actual, &expected, "raw rank logits")?; + + let score = expected[0] - expected[1]; + if !score.is_finite() || score.abs() <= 2.0 * tolerance(expected[0], expected[1]) { + return Err( + "rank fixture did not produce an asymmetric signed yes-minus-no score".to_string(), + ); + } + + for (name, fault) in [ + ("swapped yes/no rank rows", Fault::SwappedRankRows), + ("negated rank head", Fault::NegatedRankHead), + ("first-token rank pooling", Fault::FirstTokenPool), + ("missing final rank normalization", Fault::MissingFinalNorm), + ( + "softmax rank probabilities instead of raw logits", + Fault::SoftmaxRankHead, + ), + ] { + let incorrect = oracle_rank_logits(&fixture, &TOKEN_IDS, fault)?; + assert_discriminated(&expected, &incorrect, name)?; + } + } + Ok(()) +} + +#[test] +fn rank_head_late_nonfinite_refusal_preserves_pristine_retry() -> TestResult<()> { + for value in [f32::NAN, f32::INFINITY] { + let mut malformed = qwen3_rank_fixture(Storage::F32)?; + let head = raw_tensor_mut(&mut malformed, "cls.output.weight")?; + head.payload[..4].copy_from_slice(&value.to_le_bytes()); + let artifact = verify_fixture(&malformed)?; + let weights = + Qwen3RankWeights::try_from_verified(&artifact).map_err(|error| error.to_string())?; + let execution = weights + .execution(CONTEXT) + .map_err(|error| error.to_string())?; + if execution.last_logits(&TOKEN_IDS).is_ok() { + return Err(format!( + "nonfinite rank head value {value:?} unexpectedly executed" + )); + } + } + + let fixture = qwen3_rank_fixture(Storage::F32)?; + let artifact = verify_fixture(&fixture)?; + let weights = + Qwen3RankWeights::try_from_verified(&artifact).map_err(|error| error.to_string())?; + let retry = weights + .execution(CONTEXT) + .and_then(|execution| execution.last_logits(&TOKEN_IDS)) + .map_err(|error| error.to_string())?; + let fresh = weights + .execution(CONTEXT) + .and_then(|execution| execution.last_logits(&TOKEN_IDS)) + .map_err(|error| error.to_string())?; + if retry + .iter() + .zip(fresh) + .any(|(retry, fresh)| retry.to_bits() != fresh.to_bits()) + || !retry.iter().all(|value| value.is_finite()) + { + return Err("a refused rank head changed a subsequent pristine rank retry".to_string()); + } + Ok(()) +} + #[expect( clippy::too_many_lines, reason = "the raw fixture names every role and deliberately alternates F32/Q8 storage" @@ -286,6 +381,33 @@ fn qwen3_fixture() -> TestResult { }) } +fn qwen3_rank_fixture(head_storage: Storage) -> TestResult { + let mut fixture = qwen3_fixture()?; + replace_metadata(&mut fixture, "qwen3.pooling_type", RawMetadataValue::U32(4))?; + fixture.metadata.push(RawMetadata { + key: "qwen3.classifier.output_labels".to_string(), + value: RawMetadataValue::StringArray(vec!["yes".to_string(), "no".to_string()]), + }); + fixture.tensors.push(matrix_tensor( + "cls.output.weight", + HIDDEN, + 2, + head_storage, + 131, + )?); + Ok(fixture) +} + +fn replace_metadata(fixture: &mut RawGguf, key: &str, value: RawMetadataValue) -> TestResult<()> { + let metadata = fixture + .metadata + .iter_mut() + .find(|metadata| metadata.key == key) + .ok_or_else(|| format!("rank fixture is missing metadata `{key}`"))?; + metadata.value = value; + Ok(()) +} + fn block_name(block: usize, role: &str) -> String { format!("blk.{block}.{role}") } @@ -457,6 +579,35 @@ fn oracle_last_hidden(raw: &RawGguf, token_ids: &[u32], fault: Fault) -> TestRes .ok_or_else(|| "oracle pooled row is outside the token sequence".to_string()) } +fn oracle_rank_logits(raw: &RawGguf, token_ids: &[u32], fault: Fault) -> TestResult<[f64; 2]> { + let hidden = oracle_last_hidden(raw, token_ids, fault)?; + let head = decode_matrix(raw, "cls.output.weight")?; + let values = project(&head, &hidden, Fault::None)?; + let mut logits: [f64; 2] = values.try_into().map_err(|values: Vec| { + format!("rank oracle expected two head logits, got {}", values.len()) + })?; + if fault.swaps_rank_rows() { + logits.swap(0, 1); + } + if fault.negates_rank_head() { + logits = [-logits[0], -logits[1]]; + } + if fault.softmaxes_rank_head() { + let maximum = logits[0].max(logits[1]); + let yes = (logits[0] - maximum).exp(); + let no = (logits[1] - maximum).exp(); + let total = yes + no; + if !total.is_finite() || total <= 0.0 { + return Err("rank oracle softmax normalization is not finite positive".to_string()); + } + logits = [yes / total, no / total]; + } + if !logits.iter().all(|value| value.is_finite()) { + return Err("rank oracle logits are non-finite".to_string()); + } + Ok(logits) +} + fn oracle_block( raw: &RawGguf, block: usize, @@ -809,6 +960,20 @@ fn raw_tensor<'fixture>(raw: &'fixture RawGguf, name: &str) -> TestResult<&'fixt Ok(tensor) } +fn raw_tensor_mut<'fixture>( + raw: &'fixture mut RawGguf, + name: &str, +) -> TestResult<&'fixture mut RawTensor> { + let mut matching = raw.tensors.iter_mut().filter(|tensor| tensor.name == name); + let tensor = matching + .next() + .ok_or_else(|| format!("rank fixture is missing tensor `{name}`"))?; + if matching.next().is_some() { + return Err(format!("rank fixture duplicates tensor `{name}`")); + } + Ok(tensor) +} + fn decode_f32_values(payload: &[u8]) -> TestResult> { if !payload.len().is_multiple_of(4) { return Err("oracle F32 payload has trailing bytes".to_string()); From dea250e259dccfb2cd062cfbef051c7df4e4e137 Mon Sep 17 00:00:00 2001 From: CodyKickertz Date: Sun, 6 Sep 2026 19:49:47 -0500 Subject: [PATCH 04/15] feat: extract bounded template renderer --- Cargo.toml | 1 + crates/templates/Cargo.toml | 17 ++ crates/templates/src/error.rs | 68 ++++++ crates/templates/src/lib.rs | 400 ++++++++++++++++++++++++++++++++++ crates/text/Cargo.toml | 2 +- crates/text/src/error.rs | 10 + crates/text/src/lib.rs | 287 ++++++------------------ 7 files changed, 568 insertions(+), 217 deletions(-) create mode 100644 crates/templates/Cargo.toml create mode 100644 crates/templates/src/error.rs create mode 100644 crates/templates/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 0c13287..e709fea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ tokenize = { path = "crates/tokenize" } cache = { path = "crates/cache" } decode = { path = "crates/decode" } text = { path = "crates/text" } +templates = { path = "crates/templates" } core = { path = "crates/core" } transformers = { path = "crates/transformers" } encoders = { path = "crates/encoders" } diff --git a/crates/templates/Cargo.toml b/crates/templates/Cargo.toml new file mode 100644 index 0000000..1f8dab9 --- /dev/null +++ b/crates/templates/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "templates" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +description = "Bounded format-neutral MiniJinja rendering." +publish = false + +[dependencies] +minijinja = { workspace = true } +minijinja-contrib = { workspace = true } +serde = { workspace = true, features = ["derive"] } +snafu = { workspace = true } + +[lints] +workspace = true diff --git a/crates/templates/src/error.rs b/crates/templates/src/error.rs new file mode 100644 index 0000000..c769b23 --- /dev/null +++ b/crates/templates/src/error.rs @@ -0,0 +1,68 @@ +//! Typed failures for bounded template rendering. + +use snafu::Snafu; + +/// Result alias used throughout `templates`. +pub type Result = std::result::Result; + +/// Failures from compiling or rendering one bounded template. +#[derive(Debug, Snafu)] +#[snafu(visibility(pub))] +#[non_exhaustive] +pub enum Error { + /// A bounded render buffer allocation could not be reserved. + #[snafu(display("template renderer could not reserve bounded {target} storage"))] + Allocation { + /// Allocation purpose. + target: &'static str, + /// Allocation failure returned by the standard library. + source: std::collections::TryReserveError, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + + /// Template compilation or rendering failed. + #[snafu(display("template renderer failed: {source}"))] + Template { + /// Template engine failure retaining its error chain. + source: minijinja::Error, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + + /// Rendered bytes violated the UTF-8 invariant required by template output. + #[snafu(display("template renderer emitted invalid UTF-8: {source}"))] + RenderedUtf8 { + /// UTF-8 conversion failure retaining its error chain. + source: std::string::FromUtf8Error, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + + /// A template source or rendered result exceeded a declared bound. + #[snafu(display("template renderer {field} {actual} exceeds limit {limit}"))] + LimitExceeded { + /// Bounded dimension. + field: &'static str, + /// Observed value. + actual: usize, + /// Maximum accepted value. + limit: usize, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + + /// Static renderer configuration was internally inconsistent. + #[snafu(display("template renderer configuration violates {rule}"))] + InvalidConfiguration { + /// Exact invariant that failed. + rule: &'static str, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, +} diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs new file mode 100644 index 0000000..cd54e6f --- /dev/null +++ b/crates/templates/src/lib.rs @@ -0,0 +1,400 @@ +//! Format-neutral immutable rendering of one bounded artifact template. + +#![deny(missing_docs)] +#![deny(unsafe_op_in_unsafe_fn)] + +pub mod error; + +use std::io::{self, Write}; + +use minijinja::{Environment, UndefinedBehavior}; +use minijinja_contrib::pycompat::unknown_method_callback; +use serde::Serialize; +use snafu::ResultExt; + +use crate::error::{ + AllocationSnafu, InvalidConfigurationSnafu, LimitExceededSnafu, RenderedUtf8Snafu, + TemplateSnafu, +}; + +pub use crate::error::{Error, Result}; + +/// Static bounds for one template source and each independent render. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[non_exhaustive] +pub struct TemplateLimits { + /// Maximum UTF-8 bytes accepted for the template source. + template_bytes: usize, + /// Maximum UTF-8 bytes retained for one rendered result. + rendered_bytes: usize, + /// `MiniJinja` instruction budget for each render. + fuel: u64, + /// `MiniJinja` recursion limit for each render. + recursion: usize, +} + +impl TemplateLimits { + /// Construct limits after validating their independent renderer invariants. + /// + /// # Errors + /// + /// Returns [`Error::InvalidConfiguration`] when a limit is zero or the + /// `fuel` value cannot be represented by `MiniJinja`. + pub fn new( + template_bytes: usize, + rendered_bytes: usize, + fuel: u64, + recursion: usize, + ) -> Result { + let limits = Self { + template_bytes, + rendered_bytes, + fuel, + recursion, + }; + validate_limits(limits)?; + Ok(limits) + } +} + +/// An immutable borrowed template whose environment has no resolution authority. +pub struct BoundedTemplate<'source> { + source: &'source str, + environment: Environment<'source>, + limits: TemplateLimits, +} + +impl<'source> BoundedTemplate<'source> { + /// Admit and compile one source under immutable, bounded rendering policy. + /// + /// # Errors + /// + /// Returns a typed limit, configuration, or `MiniJinja` compilation error. + pub fn new(source: &'source str, limits: TemplateLimits) -> Result { + check_limit("template bytes", source.len(), limits.template_bytes)?; + let environment = configured_environment(limits)?; + environment + .template_from_str(source) + .context(TemplateSnafu)?; + Ok(Self { + source, + environment, + limits, + }) + } + + /// Render one serializable context with fresh fuel and bounded output storage. + /// + /// # Errors + /// + /// Returns a typed allocation, limit, UTF-8, or `MiniJinja` rendering error. + pub fn render(&self, context: impl Serialize) -> Result { + let template = self + .environment + .template_from_str(self.source) + .context(TemplateSnafu)?; + let mut output = ByteCappedWriter::new(self.limits.rendered_bytes)?; + let result = template.render_captured_to(context, &mut output); + if output.exceeded { + return LimitExceededSnafu { + field: "rendered template bytes", + actual: output.attempted, + limit: self.limits.rendered_bytes, + } + .fail(); + } + result.context(TemplateSnafu)?; + output.into_string() + } +} + +fn validate_limits(limits: TemplateLimits) -> Result<()> { + if [ + limits.template_bytes, + limits.rendered_bytes, + limits.recursion, + ] + .contains(&0) + || limits.fuel == 0 + || isize::try_from(limits.fuel).is_err() + { + return InvalidConfigurationSnafu { + rule: "template limits must be non-zero and fuel must fit isize", + } + .fail(); + } + Ok(()) +} + +fn configured_environment<'source>(limits: TemplateLimits) -> Result> { + let mut environment = Environment::new(); + environment.set_undefined_behavior(UndefinedBehavior::Strict); + environment.set_unknown_method_callback(unknown_method_callback); + environment.set_fuel(Some(limits.fuel)); + environment.set_recursion_limit(limits.recursion); + if environment.recursion_limit() != limits.recursion { + return InvalidConfigurationSnafu { + rule: "requested template recursion limit is not supported by this runtime", + } + .fail(); + } + Ok(environment) +} + +fn check_limit(field: &'static str, actual: usize, limit: usize) -> Result<()> { + if actual > limit { + return LimitExceededSnafu { + field, + actual, + limit, + } + .fail(); + } + Ok(()) +} + +struct ByteCappedWriter { + output: Vec, + maximum: usize, + attempted: usize, + exceeded: bool, +} + +impl ByteCappedWriter { + fn new(maximum: usize) -> Result { + let mut output = Vec::new(); + output.try_reserve_exact(maximum).context(AllocationSnafu { + target: "rendered template bytes", + })?; + Ok(Self { + output, + maximum, + attempted: 0, + exceeded: false, + }) + } + + fn into_string(self) -> Result { + String::from_utf8(self.output).context(RenderedUtf8Snafu) + } +} + +impl Write for ByteCappedWriter { + fn write(&mut self, buffer: &[u8]) -> io::Result { + let attempted = self.output.len().checked_add(buffer.len()); + self.attempted = match attempted { + Some(value) => value, + None => usize::MAX, + }; + if attempted.is_none_or(|value| value > self.maximum) { + self.exceeded = true; + return Err(io::Error::new( + io::ErrorKind::WriteZero, + "output limit reached", + )); + } + self.output.extend_from_slice(buffer); + Ok(buffer.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::error::Error as StdError; + + use minijinja::ErrorKind; + use serde::Serialize; + + use super::{BoundedTemplate, ByteCappedWriter, Error, TemplateLimits}; + + type TestResult = std::result::Result>; + + #[derive(Serialize)] + struct Message<'content> { + content: &'content str, + } + + #[derive(Serialize)] + struct Context<'content> { + message: Message<'content>, + } + + fn limits() -> TemplateLimits { + TemplateLimits { + template_bytes: 4_096, + rendered_bytes: 256, + fuel: 10_000, + recursion: 16, + } + } + + #[test] + fn compile_refuses_invalid_static_limits() { + assert!(matches!( + TemplateLimits::new(4_096, 256, 0, 16), + Err(Error::InvalidConfiguration { .. }) + )); + + assert!(matches!( + TemplateLimits::new(4_096, 256, u64::MAX, 16), + Err(Error::InvalidConfiguration { .. }) + )); + } + + #[test] + fn resolution_has_no_registered_or_host_loader_authority() -> TestResult<()> { + for source in [ + "{% include 'missing' %}", + "{% set target = 'missing' %}{% include target %}", + "{% import 'missing' as imported %}", + "{% set target = 'missing' %}{% import target as imported %}", + "{% extends 'missing' %}{% block body %}x{% endblock %}", + "{% set target = 'missing' %}{% extends target %}{% block body %}x{% endblock %}", + "{% include '' %}", + "{% import '' as imported %}", + "{% extends '' %}{% block body %}x{% endblock %}", + "{% include 'artifact-chat-template' %}", + "{% import 'artifact-chat-template' as imported %}", + "{% extends 'artifact-chat-template' %}{% block body %}x{% endblock %}", + ] { + let template = BoundedTemplate::new(source, limits())?; + assert!( + matches!(template.render(()), Err(Error::Template { source, .. }) if source.kind() == ErrorKind::TemplateNotFound), + "source unexpectedly resolved: {source}" + ); + } + assert_eq!( + BoundedTemplate::new("a{% include 'missing' ignore missing %}b", limits())? + .render(())?, + "ab" + ); + assert!(matches!( + BoundedTemplate::new("{{ missing }}", limits())?.render(()), + Err(Error::Template { source, .. }) if source.kind() == ErrorKind::UndefinedError + )); + Ok(()) + } + + #[test] + fn supports_macros_json_and_python_compatibility() -> TestResult<()> { + let python = BoundedTemplate::new("{{ message.content.startswith('a') }}", limits())? + .render(Context { + message: Message { content: "alice" }, + })?; + assert_eq!(python, "True"); + + let json = + BoundedTemplate::new("{{ message.content|tojson }}", limits())?.render(Context { + message: Message { content: "alice" }, + })?; + assert_eq!(json, "\"alice\""); + + let macro_template = BoundedTemplate::new( + "{% macro emit(value) %}{{ value }}{% endmacro %}{{ emit(message.content) }}", + limits(), + )?; + assert_eq!( + macro_template.render(Context { + message: Message { content: "alice" }, + })?, + "alice" + ); + Ok(()) + } + + #[test] + fn each_render_receives_fresh_fuel() -> TestResult<()> { + let mut constrained = limits(); + constrained.fuel = 100; + let template = + BoundedTemplate::new("{% for value in range(2) %}x{% endfor %}", constrained)?; + assert_eq!(template.render(())?, "xx"); + assert_eq!(template.render(())?, "xx"); + Ok(()) + } + + #[test] + fn caps_and_recursion_refuse_exact_boundaries() -> TestResult<()> { + let mut capped = limits(); + capped.template_bytes = 4; + assert!(matches!( + BoundedTemplate::new("hello", capped), + Err(Error::LimitExceeded { + field: "template bytes", + actual: 5, + limit: 4, + .. + }) + )); + + capped = limits(); + capped.rendered_bytes = 4; + let template = BoundedTemplate::new("12345", capped)?; + assert!(matches!( + template.render(()), + Err(Error::LimitExceeded { + field: "rendered template bytes", + actual: 5, + limit: 4, + .. + }) + )); + + capped = limits(); + capped.recursion = usize::MAX; + assert!(matches!( + BoundedTemplate::new("hello", capped), + Err(Error::InvalidConfiguration { .. }) + )); + Ok(()) + } + + #[test] + fn fuel_exhaustion_retains_engine_error_kind() -> TestResult<()> { + let mut constrained = limits(); + constrained.fuel = 1; + let template = BoundedTemplate::new( + "{% for value in range(100) %}hello{% endfor %}", + constrained, + )?; + assert!(matches!( + template.render(()), + Err(Error::Template { source, .. }) if source.kind() == ErrorKind::OutOfFuel + )); + Ok(()) + } + + #[test] + fn allocation_and_utf8_failures_retain_typed_sources() -> TestResult<()> { + let allocation_error = BoundedTemplate::new( + "x", + TemplateLimits { + rendered_bytes: usize::MAX, + ..limits() + }, + )? + .render(()) + .expect_err("impossibly large bounded output must not allocate"); + assert!( + StdError::source(&allocation_error) + .is_some_and(|source| source.is::()), + "allocation wrapper must retain TryReserveError" + ); + + let mut invalid_utf8 = ByteCappedWriter::new(1)?; + invalid_utf8.output.push(0xff); + let utf8_error = invalid_utf8 + .into_string() + .expect_err("invalid byte must fail UTF-8 conversion"); + assert!( + StdError::source(&utf8_error) + .is_some_and(|source| source.is::()), + "UTF-8 wrapper must retain FromUtf8Error" + ); + Ok(()) + } +} diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 395d597..62e71f3 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -12,9 +12,9 @@ decoders = { path = "../decoders" } decode = { path = "../decode" } loader = { path = "../loader", default-features = false } minijinja = { workspace = true } -minijinja-contrib = { workspace = true } serde = { workspace = true, features = ["derive"] } snafu = { workspace = true } +templates = { path = "../templates" } tokenize = { path = "../tokenize" } [lints] diff --git a/crates/text/src/error.rs b/crates/text/src/error.rs index cead512..8ca9ad6 100644 --- a/crates/text/src/error.rs +++ b/crates/text/src/error.rs @@ -62,6 +62,16 @@ pub enum Error { location: snafu::Location, }, + /// A newer shared renderer failure has no text-specific legacy equivalent. + #[snafu(display("text template renderer failed: {source}"))] + TemplateRenderer { + /// Shared renderer failure retaining its error chain. + source: templates::Error, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// Tokenizer parsing, encoding, or decoding failed. #[snafu(display("text tokenizer failed: {source}"))] Tokenizer { diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index 28a59f0..ab73604 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -23,21 +23,18 @@ pub mod error; -use std::io::{self, Write}; - use decoders::{Qwen35LogitSelection, Qwen35Weights}; use loader::gguf::{MetaValue, VerifiedArtifact}; -use minijinja::{Environment, UndefinedBehavior, context}; -use minijinja_contrib::pycompat::unknown_method_callback; use serde::Serialize; -use snafu::ResultExt; +use snafu::{IntoError, ResultExt}; +use templates::{BoundedTemplate, TemplateLimits}; use tokenize::{TokenizerByteLimit, TokenizerIdentity, VerifiedTokenizer}; use crate::error::{ AllocationSnafu, CancelledSnafu, DecodeSnafu, DecoderSnafu, EmptyPromptSnafu, InvalidConfigurationSnafu, LimitExceededSnafu, LogitShapeSnafu, MetadataSnafu, - RenderedUtf8Snafu, SpecialTokenPolicySnafu, TemplateSnafu, TokenizerSnafu, - VocabularyMismatchSnafu, + RenderedUtf8Snafu, SpecialTokenPolicySnafu, TemplateRendererSnafu, TemplateSnafu, + TokenizerSnafu, VocabularyMismatchSnafu, }; pub use crate::error::{Error, Result}; @@ -104,28 +101,28 @@ pub struct PipelineLimits { } impl PipelineLimits { - fn validate(self) -> Result<()> { + fn validate(self) -> Result { if [ - self.template_bytes, self.messages, self.message_bytes, self.prompt_bytes, - self.rendered_bytes, self.context_tokens, self.output_tokens, self.output_bytes, - self.template_recursion, ] .contains(&0) - || self.template_fuel == 0 - || isize::try_from(self.template_fuel).is_err() { return InvalidConfigurationSnafu { rule: "static limits must be non-zero and template fuel must fit isize", } .fail(); } - Ok(()) + template_limits(self).map_err(|_| { + InvalidConfigurationSnafu { + rule: "static limits must be non-zero and template fuel must fit isize", + } + .build() + }) } } @@ -268,8 +265,7 @@ struct SpecialTokenPolicy { pub struct TextPipeline<'artifact> { weights: Qwen35Weights<'artifact>, tokenizer: VerifiedTokenizer, - environment: Environment<'artifact>, - template: &'artifact str, + template: BoundedTemplate<'artifact>, special_tokens: SpecialTokenPolicy, limits: PipelineLimits, } @@ -284,7 +280,7 @@ impl<'artifact> TextPipeline<'artifact> { companion: TokenizerCompanion<'_>, limits: PipelineLimits, ) -> Result { - limits.validate()?; + let template_limits = limits.validate()?; let tokenizer = VerifiedTokenizer::from_bytes( companion.bytes, companion.identity, @@ -295,12 +291,12 @@ impl<'artifact> TextPipeline<'artifact> { let template = metadata_string(metadata, CHAT_TEMPLATE_KEY)?; check_limit("template bytes", template.len(), limits.template_bytes)?; let special_tokens = verify_vocabulary(metadata, &tokenizer)?; - let environment = compile_template(template, limits)?; + let template = + BoundedTemplate::new(template, template_limits).map_err(map_template_error)?; let weights = Qwen35Weights::try_from_verified(artifact).context(DecoderSnafu)?; Ok(Self { weights, tokenizer, - environment, template, special_tokens, limits, @@ -446,25 +442,14 @@ impl<'artifact> TextPipeline<'artifact> { } fn render(&self, request: &GenerationRequest<'_>) -> Result { - let template = self - .environment - .template_from_str(self.template) - .context(TemplateSnafu)?; - let mut output = ByteCappedWriter::new(self.limits.rendered_bytes)?; - let result = template.render_captured_to( - context!(messages => request.messages, add_generation_prompt => true, enable_thinking => request.enable_thinking, tools => Vec::<()>::new()), - &mut output, - ); - if output.exceeded { - return LimitExceededSnafu { - field: "rendered template bytes", - actual: output.attempted, - limit: self.limits.rendered_bytes, - } - .fail(); - } - result.context(TemplateSnafu)?; - output.into_string() + self.template + .render(RenderContext { + messages: request.messages, + add_generation_prompt: true, + enable_thinking: request.enable_thinking, + tools: Vec::<()>::new(), + }) + .map_err(map_template_error) } fn encode_prompt(&self, rendered: &str) -> Result> { @@ -492,22 +477,46 @@ impl<'artifact> TextPipeline<'artifact> { } } -fn compile_template(template: &str, limits: PipelineLimits) -> Result> { - let mut environment = Environment::new(); - environment.set_undefined_behavior(UndefinedBehavior::Strict); - environment.set_unknown_method_callback(unknown_method_callback); - environment.set_fuel(Some(limits.template_fuel)); - environment.set_recursion_limit(limits.template_recursion); - if environment.recursion_limit() != limits.template_recursion { - return InvalidConfigurationSnafu { - rule: "requested template recursion limit is not supported by this runtime", +#[derive(Serialize)] +struct RenderContext<'messages> { + messages: &'messages [TextMessage], + add_generation_prompt: bool, + enable_thinking: bool, + tools: Vec<()>, +} + +fn template_limits(limits: PipelineLimits) -> templates::Result { + TemplateLimits::new( + limits.template_bytes, + limits.rendered_bytes, + limits.template_fuel, + limits.template_recursion, + ) +} + +fn map_template_error(error: templates::Error) -> Error { + match error { + templates::Error::Allocation { target, source, .. } => { + AllocationSnafu { target }.into_error(source) } - .fail(); + templates::Error::Template { source, .. } => TemplateSnafu.into_error(source), + templates::Error::RenderedUtf8 { source, .. } => RenderedUtf8Snafu.into_error(source), + templates::Error::LimitExceeded { + field, + actual, + limit, + .. + } => LimitExceededSnafu { + field, + actual, + limit, + } + .build(), + templates::Error::InvalidConfiguration { rule, .. } => { + InvalidConfigurationSnafu { rule }.build() + } + _ => TemplateRendererSnafu.into_error(error), } - environment - .template_from_str(template) - .context(TemplateSnafu)?; - Ok(environment) } fn metadata_string<'metadata>( @@ -729,50 +738,6 @@ fn check_cancelled(cancellation: &dyn Cancellation, boundary: &'static str) -> R Ok(()) } -struct ByteCappedWriter { - output: Vec, - maximum: usize, - attempted: usize, - exceeded: bool, -} - -impl ByteCappedWriter { - fn new(maximum: usize) -> Result { - let mut output = Vec::new(); - output.try_reserve_exact(maximum).context(AllocationSnafu { - target: "rendered template bytes", - })?; - Ok(Self { - output, - maximum, - attempted: 0, - exceeded: false, - }) - } - fn into_string(self) -> Result { - String::from_utf8(self.output).context(RenderedUtf8Snafu) - } -} - -impl Write for ByteCappedWriter { - fn write(&mut self, buffer: &[u8]) -> io::Result { - let attempted = self.output.len().checked_add(buffer.len()); - self.attempted = attempted.unwrap_or(usize::MAX); - if attempted.is_none_or(|value| value > self.maximum) { - self.exceeded = true; - return Err(io::Error::new( - io::ErrorKind::WriteZero, - "output limit reached", - )); - } - self.output.extend_from_slice(buffer); - Ok(buffer.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - #[cfg(test)] const CRATE_NAME: &str = "text"; @@ -783,7 +748,6 @@ mod tests { use std::num::{NonZeroU64, NonZeroUsize}; use loader::gguf::{ArtifactByteLimit, Sha256Digest}; - use minijinja::ErrorKind; use sha2::{Digest, Sha256}; use test_fixtures::{ Qwen35FixtureConfig, RawGguf, RawMetadata, RawMetadataValue, SyntheticGguf, @@ -1126,72 +1090,6 @@ mod tests { fn crate_identity_matches_role() { assert_eq!(env!("CARGO_PKG_NAME"), CRATE_NAME); } - #[test] - fn capped_writer_refuses_overflow_without_partial_chunk() -> TestResult<()> { - let mut writer = ByteCappedWriter::new(3)?; - assert_eq!(writer.write(b"ok").ok(), Some(2)); - assert!(writer.write(b"no").is_err()); - assert_eq!(writer.output, b"ok"); - Ok(()) - } - - #[test] - fn template_limits_refuse_fuel_overflow_and_silent_recursion_cap() -> TestResult<()> { - let mut limits = test_limits(1)?; - limits.template_fuel = u64::MAX; - assert!(matches!( - limits.validate(), - Err(Error::InvalidConfiguration { .. }) - )); - limits.template_fuel = 1; - limits.template_recursion = usize::MAX; - assert!(matches!( - compile_template("hello", limits), - Err(Error::InvalidConfiguration { .. }) - )); - Ok(()) - } - - #[test] - fn template_resolution_has_no_registered_or_host_loader_authority() -> TestResult<()> { - let environment = compile_template("hello", test_limits(1)?)?; - for source in [ - "{% include 'missing' %}", - "{% set target = 'missing' %}{% include target %}", - "{% import 'missing' as imported %}", - "{% set target = 'missing' %}{% import target as imported %}", - "{% extends 'missing' %}{% block body %}x{% endblock %}", - "{% set target = 'missing' %}{% extends target %}{% block body %}x{% endblock %}", - "{% include '' %}", - "{% import '' as imported %}", - "{% extends '' %}{% block body %}x{% endblock %}", - "{% include 'artifact-chat-template' %}", - "{% import 'artifact-chat-template' as imported %}", - "{% extends 'artifact-chat-template' %}{% block body %}x{% endblock %}", - ] { - let template = environment.template_from_str(source)?; - assert!( - matches!( - template.render(()), - Err(error) if error.kind() == ErrorKind::TemplateNotFound - ), - "source unexpectedly resolved: {source}" - ); - } - let ignored = environment - .template_from_str("a{% include 'missing' ignore missing %}b")? - .render(())?; - assert_eq!(ignored, "ab"); - assert!( - matches!( - environment.template_from_str("{{ missing }}")?.render(()), - Err(error) if error.kind() == ErrorKind::UndefinedError - ), - "the closed environment must also reject undefined values strictly" - ); - Ok(()) - } - #[test] fn bos_and_eos_flags_define_exact_prompt_without_silent_deduplication() -> TestResult<()> { let tokenizer_json = tokenizer_json(); @@ -1392,44 +1290,6 @@ mod tests { Ok(()) } - #[test] - fn template_fuel_and_rendered_byte_caps_fail_at_the_configured_numbers() -> TestResult<()> { - let tokenizer_json = tokenizer_json(); - let messages = [TextMessage::new(TextRole::User, "hello")]; - let fuel_template = "{% for value in range(100) %}hello{% endfor %}"; - let fuel_config = fixture_config(&TOKENS, 3, false, false, fuel_template); - let fixture = build_qwen35_fixture(&fuel_config)?; - let (_directory, artifact) = load_fixture(&fixture)?; - let mut limits = test_limits(tokenizer_json.len())?; - limits.rendered_bytes = 4_096; - limits.template_fuel = 1; - let pipeline = pipeline_result(&artifact, &tokenizer_json, limits)?; - let request = GenerationRequest::new(&messages, 1, false); - let error = text_error(pipeline.render(&request))?; - match error { - Error::Template { source, .. } => assert_eq!( - source.kind(), - ErrorKind::OutOfFuel, - "fuel exhaustion must retain MiniJinja's exact error kind" - ), - error => { - return Err(std::io::Error::other(format!( - "expected template fuel exhaustion, received `{error}`" - )) - .into()); - } - } - - let cap_config = fixture_config(&TOKENS, 3, false, false, "12345"); - let fixture = build_qwen35_fixture(&cap_config)?; - let (_directory, artifact) = load_fixture(&fixture)?; - let mut limits = test_limits(tokenizer_json.len())?; - limits.rendered_bytes = 4; - let pipeline = pipeline_result(&artifact, &tokenizer_json, limits)?; - expect_limit(pipeline.render(&request), "rendered template bytes", 5, 4)?; - Ok(()) - } - #[test] fn tokenizer_template_and_message_input_caps_report_exact_dimensions() -> TestResult<()> { let tokenizer_json = tokenizer_json(); @@ -1654,14 +1514,16 @@ mod tests { } #[test] - fn allocation_template_decode_and_utf8_failures_retain_typed_sources() -> TestResult<()> { - let allocation_error = text_error(ByteCappedWriter::new(usize::MAX))?; - assert!( - require_source(&allocation_error)?.is::(), - "allocation wrapper must retain TryReserveError" - ); - - let template_error = text_error(compile_template("{% if", test_limits(1)?))?; + fn template_and_decode_failures_retain_typed_sources() -> TestResult<()> { + let tokenizer_json = tokenizer_json(); + let config = fixture_config(&TOKENS, 3, false, false, "{% if"); + let fixture = build_qwen35_fixture(&config)?; + let (_directory, artifact) = load_fixture(&fixture)?; + let template_error = text_error(pipeline_result( + &artifact, + &tokenizer_json, + test_limits(tokenizer_json.len())?, + ))?; assert!( require_source(&template_error)?.is::(), "template wrapper must retain minijinja::Error" @@ -1673,13 +1535,6 @@ mod tests { "greedy wrapper must retain decode::Error" ); - let mut invalid_utf8 = ByteCappedWriter::new(1)?; - invalid_utf8.output.push(0xff); - let utf8_error = text_error(invalid_utf8.into_string())?; - assert!( - require_source(&utf8_error)?.is::(), - "UTF-8 wrapper must retain FromUtf8Error" - ); Ok(()) } From 054085892b6d8dd0d3ff27c1125507da57ef2b01 Mon Sep 17 00:00:00 2001 From: CodyKickertz Date: Sun, 6 Sep 2026 19:50:46 -0500 Subject: [PATCH 05/15] feat(rerank): add bounded Qwen3 CPU adapter --- crates/rerank/Cargo.toml | 18 ++- crates/rerank/src/error.rs | 109 +++++++++++++ crates/rerank/src/lib.rs | 8 +- crates/rerank/src/qwen3.rs | 305 +++++++++++++++++++++++++++++++++++++ 4 files changed, 437 insertions(+), 3 deletions(-) create mode 100644 crates/rerank/src/qwen3.rs diff --git a/crates/rerank/Cargo.toml b/crates/rerank/Cargo.toml index cfdb48b..ae891c6 100644 --- a/crates/rerank/Cargo.toml +++ b/crates/rerank/Cargo.toml @@ -7,14 +7,28 @@ license.workspace = true description = "Cross-encoder reranker contract and preflight surface. Phase 5 Option A." publish = false +[features] +# WHY: Preserve the established ModernBERT CPU reference as the ordinary +# rerank surface while allowing native Qwen3 CPU consumers to avoid its graph. +default = ["modernbert"] +modernbert = ["dep:encoders", "dep:kernels"] + [dependencies] -encoders = { workspace = true } -kernels = { workspace = true } +decoders = { path = "../decoders" } +encoders = { workspace = true, optional = true } +kernels = { workspace = true, default-features = false, optional = true } +loader = { path = "../loader", default-features = false } +num-traits = { workspace = true } serde = { workspace = true, features = ["derive"] } snafu = { workspace = true } +templates = { path = "../templates" } +tokenize = { workspace = true } [dev-dependencies] serde_json = { workspace = true } +sha2 = { workspace = true } +tempfile = { workspace = true } +test-fixtures = { path = "../test-fixtures" } transformers = { workspace = true } [lints] diff --git a/crates/rerank/src/error.rs b/crates/rerank/src/error.rs index a231c91..609ba3d 100644 --- a/crates/rerank/src/error.rs +++ b/crates/rerank/src/error.rs @@ -129,6 +129,115 @@ pub enum Error { #[snafu(implicit)] location: snafu::Location, }, + /// Native Qwen3 rank decoder failure. + #[snafu(display("native Qwen3 rank decoder: {source}"))] + Qwen3Decoder { + /// Source decoder failure. + source: decoders::Error, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// Native Qwen3 tokenizer or artifact-token compatibility failure. + #[snafu(display("native Qwen3 rerank tokenize: {source}"))] + Qwen3Tokenizer { + /// Source tokenizer failure. + source: tokenize::Error, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// Artifact-owned Qwen3 rerank template failure. + #[snafu(display("native Qwen3 rerank template: {source}"))] + Qwen3Template { + /// Source bounded-template failure. + source: templates::Error, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// Required Qwen3 rerank metadata was absent or had an incompatible type. + #[snafu(display("invalid Qwen3 rerank metadata `{key}`"))] + Qwen3Metadata { + /// Exact GGUF metadata key. + key: &'static str, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// Qwen3 rerank setup limits were internally inconsistent. + #[snafu(display("invalid Qwen3 rerank limits: {rule}"))] + Qwen3Limits { + /// Violated setup-limit invariant. + rule: &'static str, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// One framed Qwen3 rerank item exceeds its explicit byte bound. + #[snafu(display("Qwen3 rerank item {index} has {actual} input bytes, limit {limit}"))] + Qwen3InputBytesTooLong { + /// Batch item index. + index: usize, + /// Checked instruction, query, and document byte total. + actual: usize, + /// Trusted setup limit. + limit: usize, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// One framed Qwen3 rerank item exceeds its explicit token bound. + #[snafu(display("Qwen3 rerank item {index} has {actual} tokens, limit {limit}"))] + Qwen3InputTokensTooLong { + /// Batch item index. + index: usize, + /// Encoded token count. + actual: usize, + /// Trusted setup limit. + limit: usize, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// Qwen3 rerank input lengths could not be represented together. + #[snafu(display("Qwen3 rerank input byte length overflowed usize"))] + Qwen3InputByteLengthOverflow { + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// A Qwen3 rerank batch exceeds its explicit item bound. + #[snafu(display("Qwen3 rerank batch has {actual} items, limit {limit}"))] + Qwen3BatchTooLarge { + /// Number of submitted pairs. + actual: usize, + /// Trusted setup limit. + limit: usize, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// A bounded Qwen3 rerank allocation could not be reserved. + #[snafu(display("Qwen3 rerank could not reserve bounded {target} storage"))] + Qwen3Allocation { + /// Allocation purpose. + target: &'static str, + /// Allocation failure returned by the standard library. + source: std::collections::TryReserveError, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// Qwen3 rerank raw logits could not produce one finite signed score. + #[snafu(display("Qwen3 rerank item {index} produced a non-finite relevance score"))] + Qwen3NonFiniteScore { + /// Batch item index. + index: usize, + /// Source code location where the error was reported. + #[snafu(implicit)] + location: snafu::Location, + }, } /// Crate-local result alias. diff --git a/crates/rerank/src/lib.rs b/crates/rerank/src/lib.rs index aaa3e23..4e866c3 100644 --- a/crates/rerank/src/lib.rs +++ b/crates/rerank/src/lib.rs @@ -3,15 +3,17 @@ //! Cross-encoder rerank wrappers. Score (query, document) pairs //! directly for hybrid-retrieval post-ranking. //! -//! Phase 5 Option A: contract and preflight surface only. +//! Native CPU Qwen3 and CPU ModernBERT reranking surfaces. //! - [`ModernBertConfig`] - serde-deserializable config shape. //! - [`Reranker`] - trait contract matching TEI `Backend::predict`. //! - [`GteReranker`] - named preflight surface; fails loudly. +//! - [`Qwen3Reranker`] - artifact-bound CPU causal cross-encoder adapter. //! //! ## Responsibility //! //! - `Reranker` impls backed by cross-encoder transformers //! - GTE-reranker-modernbert-base (aletheia Phase 06 target, 149 M) +//! - Qwen3 rank GGUF payloads with artifact-owned chat framing //! - bge-reranker family //! //! Lands in Phase 5. Consumers: kanon/mnemosyne Phase 04f hybrid @@ -25,16 +27,20 @@ pub mod batch; pub mod config; +#[cfg(feature = "modernbert")] pub mod cpu_reranker; pub mod error; pub mod gte; +pub mod qwen3; pub mod reranker; pub use crate::batch::{Predictions, RerankBatch, RerankItem, RerankScores}; pub use crate::config::{ModernBertConfig, ModernBertPreflight}; +#[cfg(feature = "modernbert")] pub use crate::cpu_reranker::{ClassifierHead, ModernBertCpuReranker}; pub use crate::error::{Error, Result}; pub use crate::gte::GteReranker; +pub use crate::qwen3::{Qwen3Reranker, Qwen3RerankerLimits}; pub use crate::reranker::Reranker; #[cfg(test)] diff --git a/crates/rerank/src/qwen3.rs b/crates/rerank/src/qwen3.rs new file mode 100644 index 0000000..d34bb8e --- /dev/null +++ b/crates/rerank/src/qwen3.rs @@ -0,0 +1,305 @@ +//! Bounded native CPU Qwen3 cross-encoder reranking. + +use loader::gguf::{MetaValue, MetaValueType, VerifiedArtifact}; +use num_traits::ToPrimitive; +use serde::Serialize; +use snafu::ResultExt; +use templates::{BoundedTemplate, TemplateLimits}; +use tokenize::VerifiedTokenizer; + +use decoders::Qwen3RankWeights; + +use crate::batch::{Predictions, RerankBatch}; +use crate::error::{ + EmptyBatchSnafu, EmptyDocumentSnafu, EmptyQuerySnafu, Qwen3BatchTooLargeSnafu, + Qwen3DecoderSnafu, Qwen3InputByteLengthOverflowSnafu, Qwen3InputBytesTooLongSnafu, + Qwen3InputTokensTooLongSnafu, Qwen3LimitsSnafu, Qwen3MetadataSnafu, Qwen3NonFiniteScoreSnafu, + Qwen3TemplateSnafu, Qwen3TokenizerSnafu, Result, +}; +use crate::reranker::Reranker; + +const TOKENS: &str = "tokenizer.ggml.tokens"; +const CHAT_TEMPLATE: &str = "tokenizer.chat_template"; +const ADD_BOS: &str = "tokenizer.ggml.add_bos_token"; +const ADD_EOS: &str = "tokenizer.ggml.add_eos_token"; +const IM_START: &str = "<|im_start|>"; +const IM_END: &str = "<|im_end|>"; + +/// Explicit CPU work limits for one Qwen3 reranker. +#[derive(Clone, Copy, Debug)] +pub struct Qwen3RerankerLimits { + /// Maximum checked UTF-8 bytes across instruction, query, and document. + pub max_pair_bytes: usize, + /// Maximum rendered token IDs per pair. + pub max_tokens: usize, + /// Maximum pairs accepted by one prediction request. + pub max_batch_items: usize, + /// Validated independent limits for artifact-owned template rendering. + pub template: TemplateLimits, +} + +/// Artifact-bound native CPU Qwen3 reranker with one signed relevance score per pair. +pub struct Qwen3Reranker<'artifact> { + weights: Qwen3RankWeights<'artifact>, + tokenizer: VerifiedTokenizer, + template: BoundedTemplate, + instruction: String, + max_pair_bytes: usize, + max_tokens: usize, + max_batch_items: usize, +} + +impl<'artifact> Qwen3Reranker<'artifact> { + /// Construct one bounded CPU-only Qwen3 reranker from verified artifacts and trusted setup. + /// + /// # Errors + /// + /// Returns a typed error when setup limits, artifact metadata, tokenizer + /// vocabulary/special tokens, template compilation, or rank weights fail + /// their bounded Qwen3 contract. + pub fn from_verified_cpu( + artifact: &'artifact VerifiedArtifact, + tokenizer: VerifiedTokenizer, + limits: Qwen3RerankerLimits, + instruction: String, + ) -> Result { + validate_limits(limits)?; + if instruction.trim().is_empty() { + return Qwen3LimitsSnafu { + rule: "trusted rerank instruction must not be blank", + } + .fail(); + } + let metadata = artifact.observation().metadata(); + let vocabulary = vocabulary(metadata)?; + tokenizer + .verify_exact_vocabulary( + vocabulary.len(), + vocabulary.iter().filter_map(|value| match value { + MetaValue::String(spelling) => Some(spelling.as_str()), + _ => None, + }), + ) + .context(Qwen3TokenizerSnafu)?; + require_no_automatic_specials(metadata, ADD_BOS)?; + require_no_automatic_specials(metadata, ADD_EOS)?; + verify_special(&tokenizer, vocabulary.len(), IM_START)?; + verify_special(&tokenizer, vocabulary.len(), IM_END)?; + let template_source = match metadata.get(CHAT_TEMPLATE) { + Some(MetaValue::String(source)) => source, + _ => return Qwen3MetadataSnafu { key: CHAT_TEMPLATE }.fail(), + }; + let template = + BoundedTemplate::new(template_source, limits.template).context(Qwen3TemplateSnafu)?; + let weights = Qwen3RankWeights::try_from_verified(artifact).context(Qwen3DecoderSnafu)?; + if limits.max_tokens > weights.max_context() { + return Qwen3LimitsSnafu { + rule: "setup token limit exceeds artifact context", + } + .fail(); + } + Ok(Self { + weights, + tokenizer, + template, + instruction, + max_pair_bytes: limits.max_pair_bytes, + max_tokens: limits.max_tokens, + max_batch_items: limits.max_batch_items, + }) + } + + fn score_item(&self, index: usize, query: &str, document: &str) -> Result { + let input_bytes = checked_input_bytes(&self.instruction, query, document)?; + if input_bytes > self.max_pair_bytes { + return Qwen3InputBytesTooLongSnafu { + index, + actual: input_bytes, + limit: self.max_pair_bytes, + } + .fail(); + } + let rendered = self + .template + .render(RenderContext::new(&self.instruction, query, document)) + .context(Qwen3TemplateSnafu)?; + let token_ids = self + .tokenizer + .tokenizer() + .encode(&rendered, false) + .context(Qwen3TokenizerSnafu)?; + if token_ids.is_empty() || token_ids.len() > self.max_tokens { + return Qwen3InputTokensTooLongSnafu { + index, + actual: token_ids.len(), + limit: self.max_tokens, + } + .fail(); + } + let logits = self + .weights + .execution(self.max_tokens) + .context(Qwen3DecoderSnafu)? + .last_logits(&token_ids) + .context(Qwen3DecoderSnafu)?; + signed_relevance_score(index, logits) + } +} + +impl Reranker for Qwen3Reranker<'_> { + fn predict(&self, batch: RerankBatch) -> Result { + validate_batch(&batch, self.max_batch_items)?; + let mut predictions = Predictions::new(); + for (index, item) in batch.items.iter().enumerate() { + let score = self.score_item(index, &item.query, &item.document)?; + predictions.insert(index, vec![score]); + } + Ok(predictions) + } +} + +#[derive(Serialize)] +struct RenderContext<'text> { + messages: [RenderMessage<'text>; 3], +} + +impl<'text> RenderContext<'text> { + const fn new(instruction: &'text str, query: &'text str, document: &'text str) -> Self { + Self { + messages: [ + RenderMessage { + role: "system", + content: instruction, + }, + RenderMessage { + role: "query", + content: query, + }, + RenderMessage { + role: "document", + content: document, + }, + ], + } + } +} + +#[derive(Serialize)] +struct RenderMessage<'text> { + role: &'static str, + content: &'text str, +} + +fn validate_limits(limits: Qwen3RerankerLimits) -> Result<()> { + if [ + limits.max_pair_bytes, + limits.max_tokens, + limits.max_batch_items, + ] + .contains(&0) + { + return Qwen3LimitsSnafu { + rule: "pair, token, batch, and template limits must be nonzero", + } + .fail(); + } + Ok(()) +} + +fn vocabulary(metadata: &std::collections::HashMap) -> Result<&[MetaValue]> { + let Some(MetaValue::Array(values)) = metadata.get(TOKENS) else { + return Qwen3MetadataSnafu { key: TOKENS }.fail(); + }; + if values.element_type() != MetaValueType::String { + return Qwen3MetadataSnafu { key: TOKENS }.fail(); + } + Ok(values.values()) +} + +fn require_no_automatic_specials( + metadata: &std::collections::HashMap, + key: &'static str, +) -> Result<()> { + match metadata.get(key) { + None | Some(MetaValue::Bool(false)) => Ok(()), + _ => Qwen3MetadataSnafu { key }.fail(), + } +} + +fn verify_special( + tokenizer: &VerifiedTokenizer, + vocabulary_size: usize, + spelling: &str, +) -> Result<()> { + let id = tokenizer + .tokenizer() + .token_to_id(spelling) + .ok_or_else(|| Qwen3MetadataSnafu { key: TOKENS }.build())?; + if tokenizer.tokenizer().id_to_token(id).as_deref() != Some(spelling) { + return Qwen3MetadataSnafu { key: TOKENS }.fail(); + } + tokenizer + .verify_declared_special_id(vocabulary_size, id) + .context(Qwen3TokenizerSnafu) +} + +fn checked_input_bytes(instruction: &str, query: &str, document: &str) -> Result { + instruction + .len() + .checked_add(query.len()) + .and_then(|total| total.checked_add(document.len())) + .ok_or_else(|| Qwen3InputByteLengthOverflowSnafu.build()) +} + +fn signed_relevance_score(index: usize, logits: [f32; 2]) -> Result { + (f64::from(logits[0]) - f64::from(logits[1])) + .to_f32() + .filter(|score| score.is_finite()) + .ok_or_else(|| Qwen3NonFiniteScoreSnafu { index }.build()) +} + +fn validate_batch(batch: &RerankBatch, limit: usize) -> Result<()> { + if batch.items.is_empty() { + return EmptyBatchSnafu.fail(); + } + for (index, item) in batch.items.iter().enumerate() { + if item.query.trim().is_empty() { + return EmptyQuerySnafu { index }.fail(); + } + if item.document.trim().is_empty() { + return EmptyDocumentSnafu { index }.fail(); + } + } + if batch.items.len() > limit { + return Qwen3BatchTooLargeSnafu { + actual: batch.items.len(), + limit, + } + .fail(); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::signed_relevance_score; + + #[test] + fn signed_score_preserves_yes_minus_no_direction_and_refuses_overflow() + -> std::result::Result<(), String> { + let positive = + signed_relevance_score(0, [3.5, -1.25]).map_err(|error| error.to_string())?; + let negative = + signed_relevance_score(1, [-1.25, 3.5]).map_err(|error| error.to_string())?; + if positive != 4.75 || negative != -4.75 { + return Err("signed rerank score must remain raw yes minus no logits".to_string()); + } + if !matches!( + signed_relevance_score(2, [f32::MAX, -f32::MAX]), + Err(crate::Error::Qwen3NonFiniteScore { index: 2, .. }) + ) { + return Err("non-finite signed subtraction must be refused".to_string()); + } + Ok(()) + } +} From d02a88d431d7e2ce57b69dd2673a17c10d19f97a Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 19:56:23 -0500 Subject: [PATCH 06/15] fix(tokenize): refuse configured padding and truncation --- crates/tokenize/src/error.rs | 16 +++++ crates/tokenize/src/lib.rs | 118 +++++++++++++++++++++++++++++++++-- 2 files changed, 129 insertions(+), 5 deletions(-) diff --git a/crates/tokenize/src/error.rs b/crates/tokenize/src/error.rs index af28eb1..c109ee2 100644 --- a/crates/tokenize/src/error.rs +++ b/crates/tokenize/src/error.rs @@ -67,6 +67,22 @@ pub enum Error { location: snafu::Location, }, + /// A verified tokenizer retains upstream padding configuration. + #[snafu(display("tokenizer retains configured upstream padding"))] + ConfiguredPadding { + /// Source code location where the refusal was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + + /// A verified tokenizer retains upstream truncation configuration. + #[snafu(display("tokenizer retains configured upstream truncation"))] + ConfiguredTruncation { + /// Source code location where the refusal was reported. + #[snafu(implicit)] + location: snafu::Location, + }, + /// The expected vocabulary count differs from the tokenizer vocabulary count. #[snafu(display("tokenizer vocabulary has {actual} entries, expected {expected} entries"))] VocabularyLengthMismatch { diff --git a/crates/tokenize/src/lib.rs b/crates/tokenize/src/lib.rs index 4fb88de..da036c5 100644 --- a/crates/tokenize/src/lib.rs +++ b/crates/tokenize/src/lib.rs @@ -36,11 +36,11 @@ use std::path::Path; use sha2::{Digest, Sha256}; use crate::error::{ - ByteLengthMismatchSnafu, ByteLimitExceededSnafu, DigestMismatchSnafu, - ExpectedVocabularyLengthMismatchSnafu, InvalidByteLimitSnafu, - SpecialTokenEncodingMismatchSnafu, SpecialTokenIdOutOfRangeSnafu, SpecialTokenMissingSnafu, - SpecialTokenNotMarkedSnafu, UpstreamSnafu, VocabularyIdOutOfRangeSnafu, - VocabularyLengthMismatchSnafu, VocabularyMismatchSnafu, + ByteLengthMismatchSnafu, ByteLimitExceededSnafu, ConfiguredPaddingSnafu, + ConfiguredTruncationSnafu, DigestMismatchSnafu, ExpectedVocabularyLengthMismatchSnafu, + InvalidByteLimitSnafu, SpecialTokenEncodingMismatchSnafu, SpecialTokenIdOutOfRangeSnafu, + SpecialTokenMissingSnafu, SpecialTokenNotMarkedSnafu, UpstreamSnafu, + VocabularyIdOutOfRangeSnafu, VocabularyLengthMismatchSnafu, VocabularyMismatchSnafu, }; pub use crate::error::{Error, Result}; @@ -195,6 +195,27 @@ impl VerifiedTokenizer { &self.tokenizer } + /// Refuse a tokenizer that can silently pad or truncate native input. + /// + /// Native text, embedding, and reranking boundaries own their explicit + /// request limits and special-token policies. This check leaves upstream + /// settings intact so ordinary tokenizer consumers retain their configured + /// behavior. + /// + /// # Errors + /// + /// Returns [`Error::ConfiguredPadding`] or [`Error::ConfiguredTruncation`] + /// when the verified upstream tokenizer retains either setting. + pub fn verify_unpadded_untruncated(&self) -> Result<()> { + if self.tokenizer.inner.get_padding().is_some() { + return ConfiguredPaddingSnafu.fail(); + } + if self.tokenizer.inner.get_truncation().is_some() { + return ConfiguredTruncationSnafu.fail(); + } + Ok(()) + } + /// Verify an expected ordered vocabulary against this exact tokenizer. /// /// `expected_count` is supplied separately so callers can stream borrowed @@ -501,6 +522,38 @@ mod tests { } }"#; + const CONFIGURED_TRUNCATION: &str = r#"{ + "version": "1.0", + "truncation": {"direction":"Right","max_length":1,"strategy":"LongestFirst","stride":0}, + "padding": null, + "added_tokens": [], + "normalizer": null, + "pre_tokenizer": { "type": "Whitespace" }, + "post_processor": null, + "decoder": null, + "model": { + "type": "WordLevel", + "vocab": {"[UNK]":0,"hello":1,"world":2}, + "unk_token": "[UNK]" + } + }"#; + + const CONFIGURED_PADDING: &str = r#"{ + "version": "1.0", + "truncation": null, + "padding": {"strategy":{"Fixed":4},"direction":"Right","pad_to_multiple_of":null,"pad_id":0,"pad_type_id":0,"pad_token":"[UNK]"}, + "added_tokens": [], + "normalizer": null, + "pre_tokenizer": { "type": "Whitespace" }, + "post_processor": null, + "decoder": null, + "model": { + "type": "WordLevel", + "vocab": {"[UNK]":0,"hello":1,"world":2}, + "unk_token": "[UNK]" + } + }"#; + fn verified_tokenizer(bytes: &[u8]) -> Result { let digest = TokenizerDigest::from_bytes(Sha256::digest(bytes).into()); let identity = TokenizerIdentity::new(bytes.len(), digest); @@ -508,6 +561,61 @@ mod tests { VerifiedTokenizer::from_bytes(bytes, identity, limit) } + #[test] + fn verified_tokenizer_accepts_null_padding_and_truncation() -> Result<()> { + verified_tokenizer(TRIVIAL_TOKENIZER.as_bytes())?.verify_unpadded_untruncated() + } + + #[test] + fn verified_tokenizer_refuses_configured_truncation_without_mutating_it() -> Result<()> { + let ordinary = Tokenizer::from_bytes(CONFIGURED_TRUNCATION.as_bytes())?; + let ordinary_ids = ordinary.encode("hello world", false)?; + assert_eq!( + ordinary_ids, + vec![1], + "configured truncation must affect ordinary encode" + ); + let verified = verified_tokenizer(CONFIGURED_TRUNCATION.as_bytes())?; + assert!( + matches!( + verified.verify_unpadded_untruncated(), + Err(Error::ConfiguredTruncation { .. }) + ), + "verified tokenizer must refuse configured truncation" + ); + assert_eq!( + verified.tokenizer().encode("hello world", false)?, + ordinary_ids, + "verification must not mutate ordinary tokenizer truncation" + ); + Ok(()) + } + + #[test] + fn verified_tokenizer_refuses_configured_padding_without_mutating_it() -> Result<()> { + let ordinary = Tokenizer::from_bytes(CONFIGURED_PADDING.as_bytes())?; + let ordinary_ids = ordinary.encode("hello", false)?; + assert_eq!( + ordinary_ids.len(), + 4, + "configured padding must affect ordinary encode" + ); + let verified = verified_tokenizer(CONFIGURED_PADDING.as_bytes())?; + assert!( + matches!( + verified.verify_unpadded_untruncated(), + Err(Error::ConfiguredPadding { .. }) + ), + "verified tokenizer must refuse configured padding" + ); + assert_eq!( + verified.tokenizer().encode("hello", false)?, + ordinary_ids, + "verification must not mutate ordinary tokenizer padding" + ); + Ok(()) + } + /// Build a tiny WordLevel `tokenizer.json` on disk so the /// round-trip test runs without pulling any real model file. fn write_trivial_tokenizer(path: &Path) -> std::io::Result<()> { From 4703e81f71a569d03a5ba7485562607b68dc9fea Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 19:57:17 -0500 Subject: [PATCH 07/15] fix(rerank): harden Qwen3 setup bounds --- crates/rerank/src/qwen3.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/rerank/src/qwen3.rs b/crates/rerank/src/qwen3.rs index d34bb8e..15697f0 100644 --- a/crates/rerank/src/qwen3.rs +++ b/crates/rerank/src/qwen3.rs @@ -26,6 +26,10 @@ const IM_START: &str = "<|im_start|>"; const IM_END: &str = "<|im_end|>"; /// Explicit CPU work limits for one Qwen3 reranker. +/// +/// These limits bound accepted input and retained renderer output, not every +/// tokenizer or template temporary allocation in the process. Requests that +/// exceed them are refused rather than truncated. #[derive(Clone, Copy, Debug)] pub struct Qwen3RerankerLimits { /// Maximum checked UTF-8 bytes across instruction, query, and document. @@ -42,7 +46,7 @@ pub struct Qwen3RerankerLimits { pub struct Qwen3Reranker<'artifact> { weights: Qwen3RankWeights<'artifact>, tokenizer: VerifiedTokenizer, - template: BoundedTemplate, + template: BoundedTemplate<'artifact>, instruction: String, max_pair_bytes: usize, max_tokens: usize, @@ -70,6 +74,9 @@ impl<'artifact> Qwen3Reranker<'artifact> { } .fail(); } + tokenizer + .verify_unpadded_untruncated() + .context(Qwen3TokenizerSnafu)?; let metadata = artifact.observation().metadata(); let vocabulary = vocabulary(metadata)?; tokenizer @@ -262,6 +269,13 @@ fn validate_batch(batch: &RerankBatch, limit: usize) -> Result<()> { if batch.items.is_empty() { return EmptyBatchSnafu.fail(); } + if batch.items.len() > limit { + return Qwen3BatchTooLargeSnafu { + actual: batch.items.len(), + limit, + } + .fail(); + } for (index, item) in batch.items.iter().enumerate() { if item.query.trim().is_empty() { return EmptyQuerySnafu { index }.fail(); @@ -270,13 +284,6 @@ fn validate_batch(batch: &RerankBatch, limit: usize) -> Result<()> { return EmptyDocumentSnafu { index }.fail(); } } - if batch.items.len() > limit { - return Qwen3BatchTooLargeSnafu { - actual: batch.items.len(), - limit, - } - .fail(); - } Ok(()) } From 4419cce5cd86b236c4e17505bec187135b3d572c Mon Sep 17 00:00:00 2001 From: CodyKickertz Date: Sun, 6 Sep 2026 19:58:21 -0500 Subject: [PATCH 08/15] fix(text): refuse configured tokenizer shaping --- crates/text/src/lib.rs | 57 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index ab73604..a7707f0 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -287,6 +287,9 @@ impl<'artifact> TextPipeline<'artifact> { limits.tokenizer_bytes, ) .context(TokenizerSnafu)?; + tokenizer + .verify_unpadded_untruncated() + .context(TokenizerSnafu)?; let metadata = artifact.observation().metadata(); let template = metadata_string(metadata, CHAT_TEMPLATE_KEY)?; check_limit("template bytes", template.len(), limits.template_bytes)?; @@ -1379,6 +1382,60 @@ mod tests { Ok(()) } + #[test] + fn configured_tokenizer_padding_and_truncation_are_refused_in_native_setup() -> TestResult<()> { + let config = fixture_config(&TOKENS, 3, false, false, "hello"); + let fixture = build_qwen35_fixture(&config)?; + let (_directory, artifact) = load_fixture(&fixture)?; + let ordinary = tokenizer_json(); + let cases = [ + ( + ordinary.replace( + "\"truncation\":null", + "\"truncation\":{\"direction\":\"Right\",\"max_length\":1,\"strategy\":\"LongestFirst\",\"stride\":0}", + ), + "truncation", + ), + ( + ordinary.replace( + "\"padding\":null", + "\"padding\":{\"strategy\":{\"Fixed\":4},\"direction\":\"Right\",\"pad_to_multiple_of\":null,\"pad_id\":0,\"pad_type_id\":0,\"pad_token\":\"[UNK]\"}", + ), + "padding", + ), + ]; + for (configured, setting) in cases { + let error = text_error(pipeline_result( + &artifact, + &configured, + test_limits(configured.len())?, + ))?; + match (setting, error) { + ( + "truncation", + Error::Tokenizer { + source: tokenize::Error::ConfiguredTruncation { .. }, + .. + }, + ) + | ( + "padding", + Error::Tokenizer { + source: tokenize::Error::ConfiguredPadding { .. }, + .. + }, + ) => {} + (_, error) => { + return Err(std::io::Error::other(format!( + "native text setup accepted or misreported configured tokenizer {setting}: {error}" + )) + .into()); + } + } + } + Ok(()) + } + #[test] fn context_output_token_and_decoded_byte_caps_report_exact_dimensions() -> TestResult<()> { let tokenizer_json = tokenizer_json(); From e1ef64fdccc8c4384337833368544ad598e8eed7 Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 19:58:32 -0500 Subject: [PATCH 09/15] fix(embed): refuse configured tokenizer settings --- crates/embed/src/qwen3.rs | 72 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/crates/embed/src/qwen3.rs b/crates/embed/src/qwen3.rs index aaaff7d..2d9f4d1 100644 --- a/crates/embed/src/qwen3.rs +++ b/crates/embed/src/qwen3.rs @@ -95,6 +95,9 @@ impl<'artifact> Qwen3EmbeddingModel<'artifact> { }), ) .context(Qwen3TokenizerSnafu)?; + tokenizer + .verify_unpadded_untruncated() + .context(Qwen3TokenizerSnafu)?; let insert_bos = flag(metadata, ADD_BOS)?; let append_eos = flag(metadata, ADD_EOS)?; let bos = id(metadata, BOS)?; @@ -638,14 +641,70 @@ mod tests { Ok(()) } + #[test] + fn qwen3_refuses_configured_tokenizer_padding_and_truncation() + -> std::result::Result<(), Box> { + let artifact = artifact(&fixture()?)?; + for (settings, expected) in [ + (TokenizerSettings::ConfiguredPadding, "padding"), + (TokenizerSettings::ConfiguredTruncation, "truncation"), + ] { + let Err(error) = Qwen3EmbeddingModel::from_verified_cpu( + &artifact, + verified_tokenizer_with_settings(TokenizerModel::WordLevel, settings)?, + Qwen3EmbeddingLimits { + max_text_bytes: 32, + max_tokens: 4, + max_batch_items: 4, + }, + Qwen3RolePrefixes::default(), + ) else { + return Err(format!("configured tokenizer {expected} must be rejected").into()); + }; + let crate::error::Error::Qwen3Tokenizer { source, .. } = error else { + return Err( + format!("configured tokenizer {expected} must retain its source").into(), + ); + }; + assert!( + matches!( + (settings, source), + ( + TokenizerSettings::ConfiguredPadding, + tokenize::Error::ConfiguredPadding { .. } + ) | ( + TokenizerSettings::ConfiguredTruncation, + tokenize::Error::ConfiguredTruncation { .. } + ) + ), + "native Qwen3 setup must retain the typed {expected} refusal" + ); + } + Ok(()) + } + #[derive(Clone, Copy)] enum TokenizerModel { WordLevel, WordPiece, } + #[derive(Clone, Copy)] + enum TokenizerSettings { + Null, + ConfiguredPadding, + ConfiguredTruncation, + } + fn verified_tokenizer( kind: TokenizerModel, + ) -> std::result::Result> { + verified_tokenizer_with_settings(kind, TokenizerSettings::Null) + } + + fn verified_tokenizer_with_settings( + kind: TokenizerModel, + settings: TokenizerSettings, ) -> std::result::Result> { let model = match kind { TokenizerModel::WordLevel => { @@ -655,8 +714,19 @@ mod tests { r###"{"type":"WordPiece","unk_token":"[UNK]","continuing_subword_prefix":"##","max_input_chars_per_word":100,"vocab":{"[BOS]":0,"[EOS]":1,"alice":2,"bob":3}}"### } }; + let (truncation, padding) = match settings { + TokenizerSettings::Null => ("null", "null"), + TokenizerSettings::ConfiguredPadding => ( + "null", + r#"{"strategy":{"Fixed":4},"direction":"Right","pad_to_multiple_of":null,"pad_id":0,"pad_type_id":0,"pad_token":"[BOS]"}"#, + ), + TokenizerSettings::ConfiguredTruncation => ( + r#"{"direction":"Right","max_length":1,"strategy":"LongestFirst","stride":0}"#, + "null", + ), + }; let json = format!( - r#"{{"version":"1.0","truncation":null,"padding":null,"added_tokens":[{{"id":0,"content":"[BOS]","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true}},{{"id":1,"content":"[EOS]","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true}}],"normalizer":null,"pre_tokenizer":{{"type":"Whitespace"}},"post_processor":{{"type":"TemplateProcessing","single":[{{"Sequence":{{"id":"A","type_id":0}}}},{{"SpecialToken":{{"id":"[EOS]","type_id":0}}}}],"pair":[{{"Sequence":{{"id":"A","type_id":0}}}},{{"Sequence":{{"id":"B","type_id":1}}}},{{"SpecialToken":{{"id":"[EOS]","type_id":0}}}}],"special_tokens":{{"[EOS]":{{"id":"[EOS]","ids":[1],"tokens":["[EOS]"]}}}}}},"decoder":null,"model":{model}}}"# + r#"{{"version":"1.0","truncation":{truncation},"padding":{padding},"added_tokens":[{{"id":0,"content":"[BOS]","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true}},{{"id":1,"content":"[EOS]","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true}}],"normalizer":null,"pre_tokenizer":{{"type":"Whitespace"}},"post_processor":{{"type":"TemplateProcessing","single":[{{"Sequence":{{"id":"A","type_id":0}}}},{{"SpecialToken":{{"id":"[EOS]","type_id":0}}}}],"pair":[{{"Sequence":{{"id":"A","type_id":0}}}},{{"Sequence":{{"id":"B","type_id":1}}}},{{"SpecialToken":{{"id":"[EOS]","type_id":0}}}}],"special_tokens":{{"[EOS]":{{"id":"[EOS]","ids":[1],"tokens":["[EOS]"]}}}}}},"decoder":null,"model":{model}}}"# ); let bytes = json.as_bytes(); let length = NonZeroUsize::new(bytes.len()).ok_or("empty synthetic tokenizer")?; From 676c13d02eb97c1dc2fa9128f14fb5e787ed1e91 Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 19:59:36 -0500 Subject: [PATCH 10/15] feat: integrate native reranking ownership and CPU witnesses --- AGENTS.md | 3 +++ ARCHITECTURE.md | 32 ++++++++++++++++++++++++++++++++ Cargo.lock | 20 +++++++++++++++++++- README.md | 17 ++++++++++++++++- crates/decoders/src/qwen3.rs | 2 +- crates/rerank/Cargo.toml | 2 +- crates/rerank/src/lib.rs | 7 +++---- crates/rerank/src/qwen3.rs | 18 +++++++++++++----- crates/rerank/src/reranker.rs | 5 +++-- docs/gpu-denied-runner.md | 5 +++++ llms.txt | 5 +++-- scripts/check-hip-build-modes.sh | 2 +- 12 files changed, 100 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 402f42c..23aca55 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,8 @@ share `/data/target` across agent lanes. See the precise threat model. Ordinary compilation is not a hardware-test permit. Every Cargo invocation, including lockfile generation and formatting, uses the runner; an ambient compiler wrapper must not escape the isolated lane. +Host CPU compile admission is separate: where required, the approved admission +entrypoint wraps this runner rather than relying on ambient PATH shims. ## Key patterns @@ -62,6 +64,7 @@ runner; an ambient compiler wrapper must not escape the isolated lane. | STT pipeline | `crates/ekphrasis/` | | Tokenizer | `crates/tokenize/` | | Native text request pipeline | `crates/text/` | +| Shared bounded template rendering | `crates/templates/` | | Shared synthetic GGUF test data | `crates/test-fixtures/` (dev-only) | The private planning corpus governs future scope and sequencing. This repository's diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index afe1aa2..7382f93 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -61,6 +61,18 @@ semantically respects that boundary. Stella API and tensor/encoder graph; direct consumers disable default features to exclude that accelerator-capable graph. Consumers still use the unchanged `core::EmbeddingModel` contract. +- `rerank` consumes the same CPU decoder/tokenizer graph and `templates` for + native Qwen3 pair scoring. Its default `modernbert` feature preserves the + existing encoder implementation; disabling default features removes that + accelerator-capable graph without changing the `Reranker` contract. +- `templates` owns bounded, capability-free artifact-template rendering for + `text` and `rerank`. It has no model, GGUF, tokenizer or device dependency; + pipelines retain artifact binding, typed message roles and token policy. +- `tokenize` owns exact vocabulary/special-ID verification and refusal of + configured tokenizer padding or truncation. Native text, embedding and + reranking setup invoke that guard; ordinary tokenizer consumers retain their + configured behavior. Disabling automatic special tokens alone does not + disable padding or truncation. - `taxis` depends locally on `hipcore`. - `kernels/gpu` enables the local `hipcore` and `taxis` dependencies and GPU launcher modules, including their nested parity references. Standalone @@ -186,6 +198,26 @@ instruction, and advertised dimensions do not imply Matryoshka qualification. Synthetic family and pipeline tests do not establish deployed-artifact parity, retrieval quality, reindex authority, serving or GPU qualification. +## Native reranking ownership + +`decoders::Qwen3RankWeights` admits a distinct rank profile over the shared +private Qwen3 body. Rank pooling, exact `[yes, no]` labels and a two-row +`cls.output.weight` are required; embedding admission still refuses extra +heads. The decoder returns raw terminal-token classifier logits after final +RMS normalization. `rerank` owns their signed `yes - no` reduction and returns +one relevance logit per input index, not probabilities or sorted results. + +The pipeline renders its verified artifact's embedded template using typed +system/query/document messages and an explicit setup instruction. It encodes +the whole render with automatic special tokens disabled. Separate byte, +token and batch bounds reject oversized inputs without truncation. The shared +`templates` owner retains strict undefined values, fuel, recursion and output +bounds, without external or named template resolution. Public batch structs +are revalidated at the prediction boundary; failure publishes no partial map. +These controls do not bound all tokenizer/template intermediates or total +process memory. Synthetic execution does not resolve exact-artifact conversion +provenance, template parity, retrieval quality, deployment or hardware gates. + ## cfg flags - `logismos_no_gpu_kernels` - build path without compiled HIP kernels. Implemented GPU operations diff --git a/Cargo.lock b/Cargo.lock index f985dd1..61376c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -992,11 +992,19 @@ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" name = "rerank" version = "1.0.11" dependencies = [ + "decoders", "encoders", "kernels", + "loader", + "num-traits", "serde", "serde_json", + "sha2", "snafu", + "tempfile", + "templates", + "test-fixtures", + "tokenize", "transformers", ] @@ -1214,6 +1222,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "templates" +version = "1.0.11" +dependencies = [ + "minijinja", + "minijinja-contrib", + "serde", + "snafu", +] + [[package]] name = "test-fixtures" version = "1.0.11" @@ -1230,11 +1248,11 @@ dependencies = [ "decoders", "loader", "minijinja", - "minijinja-contrib", "serde", "sha2", "snafu", "tempfile", + "templates", "test-fixtures", "tokenize", ] diff --git a/README.md b/README.md index 5c47cf5..2a6901c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ targeting AMD gfx1100, with owned HIP/WMMA kernels and progressively owned execu **Status:** HIP primitives, Stella CPU golden-fixture parity, action-free placement, process-local admission/residency coordination, and bounded instruction emulation exist. GGUF inspection, digest-bound mixed-weight CPU projections, bounded hybrid CPU -text generation and native Qwen3 CPU embeddings are foundations, not serving or +text generation and native Qwen3 CPU embeddings/reranking are foundations, not serving or hardware qualification. The W7900 is available; the RX 7900 XTX is a planned second device and requires its own qualification. The experimental below-HIP @@ -101,6 +101,21 @@ establish exact deployed-artifact parity, retrieval quality or reindex authority Direct `embed` consumers disable default features for the HIP-free native path; the default `stella` feature preserves the existing Stella API and dependencies. +[`rerank`](crates/rerank/src/lib.rs) implements native Qwen3 CPU pair scoring +through the existing `Reranker` contract. Its checked rank profile shares the +causal decoder body without weakening embedding admission. The verified +artifact supplies its template; setup supplies the instruction and independent +byte/token/batch limits. Each input index receives one raw `yes - no` relevance +logit, not a probability. Oversized requests fail without truncation. +Direct native consumers disable default features; the default `modernbert` +feature preserves the existing encoder implementation. Exact converted-model +provenance, template/tokenizer parity and retrieval quality remain unqualified. + +[`templates`](crates/templates/src/lib.rs) owns bounded template rendering for +text generation and reranking. It permits no host callbacks, loader or named +template registration; output, recursion and fuel limits are operational +controls, not a total-memory sandbox. + [`contracts/runtime-scope.toml`](contracts/runtime-scope.toml) records this product boundary. Bounded adaptation remains absent unless a named consumer contract supplies an output owner, retention and revocation policy, and rollback. The repository guard validates those declared diff --git a/crates/decoders/src/qwen3.rs b/crates/decoders/src/qwen3.rs index 5a07fc0..77c77f4 100644 --- a/crates/decoders/src/qwen3.rs +++ b/crates/decoders/src/qwen3.rs @@ -1,4 +1,4 @@ -//! Bounded native CPU Qwen3 causal execution and embedding-profile admission. +//! Bounded native CPU Qwen3 causal execution with strict embedding and rank profiles. use std::collections::{HashMap, HashSet}; use std::num::NonZeroUsize; diff --git a/crates/rerank/Cargo.toml b/crates/rerank/Cargo.toml index ae891c6..29c31f0 100644 --- a/crates/rerank/Cargo.toml +++ b/crates/rerank/Cargo.toml @@ -4,7 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true -description = "Cross-encoder reranker contract and preflight surface. Phase 5 Option A." +description = "Cross-encoder reranking with native Qwen3 and ModernBERT CPU implementations." publish = false [features] diff --git a/crates/rerank/src/lib.rs b/crates/rerank/src/lib.rs index 4e866c3..43a1699 100644 --- a/crates/rerank/src/lib.rs +++ b/crates/rerank/src/lib.rs @@ -12,12 +12,11 @@ //! ## Responsibility //! //! - `Reranker` impls backed by cross-encoder transformers -//! - GTE-reranker-modernbert-base (aletheia Phase 06 target, 149 M) +//! - Existing ModernBERT CPU execution behind the default `modernbert` feature //! - Qwen3 rank GGUF payloads with artifact-owned chat framing -//! - bge-reranker family //! -//! Lands in Phase 5. Consumers: kanon/mnemosyne Phase 04f hybrid -//! rerank, aletheia's memory recall. +//! Native Qwen3 consumers may disable default features for a GPU-free graph. +//! CPU execution does not establish model-quality or hardware qualification. #![deny(missing_docs)] #![deny(unsafe_op_in_unsafe_fn)] #![expect( diff --git a/crates/rerank/src/qwen3.rs b/crates/rerank/src/qwen3.rs index 15697f0..a467af8 100644 --- a/crates/rerank/src/qwen3.rs +++ b/crates/rerank/src/qwen3.rs @@ -175,15 +175,15 @@ impl<'text> RenderContext<'text> { Self { messages: [ RenderMessage { - role: "system", + role: RenderRole::System, content: instruction, }, RenderMessage { - role: "query", + role: RenderRole::Query, content: query, }, RenderMessage { - role: "document", + role: RenderRole::Document, content: document, }, ], @@ -191,9 +191,17 @@ impl<'text> RenderContext<'text> { } } +#[derive(Serialize)] +#[serde(rename_all = "lowercase")] +enum RenderRole { + System, + Query, + Document, +} + #[derive(Serialize)] struct RenderMessage<'text> { - role: &'static str, + role: RenderRole, content: &'text str, } @@ -206,7 +214,7 @@ fn validate_limits(limits: Qwen3RerankerLimits) -> Result<()> { .contains(&0) { return Qwen3LimitsSnafu { - rule: "pair, token, batch, and template limits must be nonzero", + rule: "pair, token, and batch limits must be nonzero", } .fail(); } diff --git a/crates/rerank/src/reranker.rs b/crates/rerank/src/reranker.rs index 87da361..c7a119a 100644 --- a/crates/rerank/src/reranker.rs +++ b/crates/rerank/src/reranker.rs @@ -1,7 +1,8 @@ //! The reranker trait contract. //! -//! Implementations live beside their backend: [`crate::cpu_reranker`] for -//! the CPU cross-encoder, [`crate::gte`] for the preflight surface. +//! Implementations live beside their backend: [`crate::qwen3`] for native +//! Qwen3, `cpu_reranker` for feature-gated ModernBERT, and [`crate::gte`] +//! for the preflight surface. use crate::batch::{Predictions, RerankBatch}; use crate::error::Result; diff --git a/docs/gpu-denied-runner.md b/docs/gpu-denied-runner.md index cff24df..f383ecb 100644 --- a/docs/gpu-denied-runner.md +++ b/docs/gpu-denied-runner.md @@ -13,6 +13,11 @@ The runner is intentionally non-interactive. Its standard descriptors must be pipes, `/dev/null`, or safe regular files as described below; redirect through a pipe when launching it from a terminal. +GPU denial is separate from host CPU build admission. When the host requires +compile admission, its approved entrypoint must wrap this runner. The runner's +sanitized environment intentionally does not inherit ambient PATH shims, so a +bare runner invocation cannot establish that separate admission contract. + ## Lockfile maintenance Cargo runs inside the boundary even when resolving dependencies or checking diff --git a/llms.txt b/llms.txt index b9f965e..bfac8d7 100644 --- a/llms.txt +++ b/llms.txt @@ -40,14 +40,15 @@ records implemented crate ownership; Kanon standards govern engineering practice - `crates/transformers/` - transformer building blocks (attention, norms) - `crates/encoders/` - encoder model impls (Stella, ModernBERT) - `crates/embed/` - native CPU Qwen3 embedding adapter; default Stella pipeline -- `crates/rerank/` - `Reranker` trait and cross-encoder implementations (GteReranker, ModernBERT CPU) +- `crates/rerank/` - native CPU Qwen3 relevance logits; `Reranker` contract and default ModernBERT implementation - `crates/quant/` - original CPU block/row reconstruction for admitted quantized formats - `crates/loader/` - safetensors, bounded GGUF v3 observation, and digest-bound immutable payload ownership -- `crates/decoders/` - verified Qwen3 causal embedding and Qwen3.5 hybrid CPU execution +- `crates/decoders/` - verified Qwen3 causal embedding/rank profiles and Qwen3.5 hybrid CPU execution - `crates/decode/` - checked CPU logit processing and token selection - `crates/bin/` - CPU-only `plan` and typed GGUF `inspect` commands - `crates/tokenize/` - tokenizer facade - `crates/text/` - artifact-bound text-only native CPU generation with checked greedy decoding +- `crates/templates/` - shared bounded artifact-template rendering without host capabilities - `crates/test-fixtures/` - dev-only original synthetic GGUF serialization and fixtures - `crates/logismos/` - top-level integration facade diff --git a/scripts/check-hip-build-modes.sh b/scripts/check-hip-build-modes.sh index 05638a7..760cc19 100755 --- a/scripts/check-hip-build-modes.sh +++ b/scripts/check-hip-build-modes.sh @@ -127,7 +127,7 @@ OUT="$ROOT/target/hip-build-mode-witness" exit 1 fi # WHY: One package selection owns both graph and compiler witnesses. - set -- -p kernels -p transformers -p decoders -p text -p decode -p embed + set -- -p kernels -p transformers -p decoders -p text -p decode -p embed -p rerank -p templates cargo tree --offline --locked --no-default-features "$@" \ --edges normal,build --prefix none --format "{p}" >"$out/cpu-dependencies.log" if grep -Eq "^(hipcore|taxis) " "$out/cpu-dependencies.log"; then From 4d3b2a1e6ff370f20ee2d2aa31cc79bf3cad6331 Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 20:02:51 -0500 Subject: [PATCH 11/15] test(templates): retain typed sources without panic shortcuts --- crates/templates/src/lib.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index cd54e6f..6db9523 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -378,10 +378,11 @@ mod tests { }, )? .render(()) - .expect_err("impossibly large bounded output must not allocate"); + .err() + .ok_or("impossibly large bounded output must not allocate")?; assert!( StdError::source(&allocation_error) - .is_some_and(|source| source.is::()), + .is_some_and(::is::), "allocation wrapper must retain TryReserveError" ); @@ -389,10 +390,11 @@ mod tests { invalid_utf8.output.push(0xff); let utf8_error = invalid_utf8 .into_string() - .expect_err("invalid byte must fail UTF-8 conversion"); + .err() + .ok_or("invalid byte must fail UTF-8 conversion")?; assert!( StdError::source(&utf8_error) - .is_some_and(|source| source.is::()), + .is_some_and(::is::), "UTF-8 wrapper must retain FromUtf8Error" ); Ok(()) From 9f6d0bdd67af61324f0f321c9bfc4b490e269431 Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 20:06:26 -0500 Subject: [PATCH 12/15] test(rerank): add Qwen3 native adapter witnesses --- crates/rerank/src/qwen3.rs | 24 +- crates/rerank/src/qwen3/tests.rs | 706 +++++++++++++++++++++++++++++++ 2 files changed, 708 insertions(+), 22 deletions(-) create mode 100644 crates/rerank/src/qwen3/tests.rs diff --git a/crates/rerank/src/qwen3.rs b/crates/rerank/src/qwen3.rs index a467af8..5b64948 100644 --- a/crates/rerank/src/qwen3.rs +++ b/crates/rerank/src/qwen3.rs @@ -296,25 +296,5 @@ fn validate_batch(batch: &RerankBatch, limit: usize) -> Result<()> { } #[cfg(test)] -mod tests { - use super::signed_relevance_score; - - #[test] - fn signed_score_preserves_yes_minus_no_direction_and_refuses_overflow() - -> std::result::Result<(), String> { - let positive = - signed_relevance_score(0, [3.5, -1.25]).map_err(|error| error.to_string())?; - let negative = - signed_relevance_score(1, [-1.25, 3.5]).map_err(|error| error.to_string())?; - if positive != 4.75 || negative != -4.75 { - return Err("signed rerank score must remain raw yes minus no logits".to_string()); - } - if !matches!( - signed_relevance_score(2, [f32::MAX, -f32::MAX]), - Err(crate::Error::Qwen3NonFiniteScore { index: 2, .. }) - ) { - return Err("non-finite signed subtraction must be refused".to_string()); - } - Ok(()) - } -} +#[path = "qwen3/tests.rs"] +mod tests; diff --git a/crates/rerank/src/qwen3/tests.rs b/crates/rerank/src/qwen3/tests.rs new file mode 100644 index 0000000..3673a75 --- /dev/null +++ b/crates/rerank/src/qwen3/tests.rs @@ -0,0 +1,706 @@ +//! Synthetic end-to-end witnesses for the bounded Qwen3 rerank adapter. + +use std::error::Error as StdError; +use std::num::NonZeroU64; + +use loader::gguf::{ArtifactByteLimit, Sha256Digest, VerifiedArtifact}; +use sha2::{Digest, Sha256}; +use templates::TemplateLimits; +use test_fixtures::{RawGguf, RawMetadata, RawMetadataValue, RawTensor, serialize_raw_gguf}; +use tokenize::{ + Error as TokenizerError, TokenizerByteLimit, TokenizerDigest, TokenizerIdentity, + VerifiedTokenizer, +}; + +use super::{Qwen3Reranker, Qwen3RerankerLimits, signed_relevance_score}; +use crate::{Error, RerankBatch, RerankItem, Reranker}; + +type TestResult = std::result::Result>; + +const HIDDEN: usize = 2; +const FEED_FORWARD: usize = 4; +const CONTEXT: usize = 8; +const EPSILON: f32 = 0.001; +const F32: u32 = 0; +const POSITIVE_DOCUMENT: &str = "docpos"; +const NEGATIVE_DOCUMENT: &str = "docneg"; +const INSTRUCTION: &str = "instruct"; +const POSITIVE_QUERY: &str = "positive"; +const NEGATIVE_QUERY: &str = "negative"; +const TEMPLATE: &str = "{% if messages[0].role == \"system\" and messages[1].role == \"query\" and messages[2].role == \"document\" %}system {{ messages[0].content }} query {{ messages[1].content }} document {% if messages[0].content == \"different\" and messages[1].content == \"positive\" and messages[2].content == \"docneg\" %}docpos{% else %}{{ messages[2].content }}{% endif %}{% else %}docpos{% endif %}"; + +#[test] +fn signed_score_is_raw_yes_minus_no_and_refuses_f32_overflow() -> TestResult<()> { + let positive = signed_relevance_score(0, [3.5, -1.25])?; + let negative = signed_relevance_score(1, [-1.25, 3.5])?; + if positive != 4.75 || negative != -4.75 { + return Err("signed rerank score must remain raw yes minus no logits".into()); + } + assert!(matches!( + signed_relevance_score(2, [f32::MAX, -f32::MAX]), + Err(Error::Qwen3NonFiniteScore { index: 2, .. }) + )); + Ok(()) +} + +#[test] +fn dyn_reranker_returns_indexed_signed_scores_from_artifact_template() -> TestResult<()> { + let raw = raw_rank_fixture()?; + let (_directory, artifact) = verified_artifact(&raw)?; + let reranker = reranker(&artifact, limits(128, 6, 2)?)?; + let interface: &dyn Reranker = &reranker; + let batch = batch(vec![ + (POSITIVE_QUERY, POSITIVE_DOCUMENT), + (NEGATIVE_QUERY, NEGATIVE_DOCUMENT), + ]); + + let predictions = interface.predict(batch)?; + if predictions.len() != 2 || predictions.keys().copied().collect::>() != [0, 1] { + return Err("reranker did not preserve both input indices".into()); + } + assert_score( + predictions + .get(&0) + .ok_or("positive prediction is missing")?, + hand_score([1.0, 0.0])?, + "positive artifact-framed pair", + )?; + assert_score( + predictions + .get(&1) + .ok_or("negative prediction is missing")?, + hand_score([0.0, 1.0])?, + "negative artifact-framed pair", + )?; + Ok(()) +} + +#[test] +fn explicit_instruction_and_typed_roles_control_the_final_document_token() -> TestResult<()> { + let raw = raw_rank_fixture()?; + let (_directory, artifact) = verified_artifact(&raw)?; + let default = reranker(&artifact, limits(128, 6, 1)?)?; + let custom = reranker_with_instruction(&artifact, limits(128, 6, 1)?, "different")?; + let input = batch(vec![(POSITIVE_QUERY, NEGATIVE_DOCUMENT)]); + let default_result = default.predict(input.clone())?; + let custom_result = custom.predict(input)?; + assert_score( + default_result + .get(&0) + .ok_or("default framed prediction is missing")?, + hand_score([0.0, 1.0])?, + "default template document token", + )?; + assert_score( + custom_result + .get(&0) + .ok_or("custom framed prediction is missing")?, + hand_score([1.0, 0.0])?, + "custom system/query/document template branch", + )?; + if default_result == custom_result { + return Err("custom setup and typed role template branch did not alter the score".into()); + } + Ok(()) +} + +#[test] +fn independent_byte_token_and_batch_bounds_refuse_without_truncation() -> TestResult<()> { + let raw = raw_rank_fixture()?; + let (_directory, artifact) = verified_artifact(&raw)?; + let bytes = INSTRUCTION.len() + POSITIVE_QUERY.len() + POSITIVE_DOCUMENT.len(); + let byte_limited = reranker(&artifact, limits(bytes - 1, 6, 2)?)?; + assert!(matches!( + byte_limited.predict(batch(vec![(POSITIVE_QUERY, POSITIVE_DOCUMENT)])), + Err(Error::Qwen3InputBytesTooLong { actual, limit, .. }) if actual == bytes && limit == bytes - 1 + )); + let multibyte_limit = INSTRUCTION.len() + "é".chars().count() + POSITIVE_DOCUMENT.len(); + assert!(matches!( + reranker(&artifact, limits(multibyte_limit, 6, 2)?)?.predict(batch(vec![("é", POSITIVE_DOCUMENT)])), + Err(Error::Qwen3InputBytesTooLong { actual, limit, .. }) + if actual == multibyte_limit + 1 && limit == multibyte_limit + )); + + let token_limited = reranker(&artifact, limits(128, 5, 2)?)?; + assert!(matches!( + token_limited.predict(batch(vec![(POSITIVE_QUERY, POSITIVE_DOCUMENT)])), + Err(Error::Qwen3InputTokensTooLong { + actual: 6, + limit: 5, + .. + }) + )); + + let batch_limited = reranker(&artifact, limits(128, 6, 1)?)?; + let oversized_blank = RerankBatch { + items: vec![item(" ", " "), item(" ", " ")], + }; + assert!(matches!( + batch_limited.predict(oversized_blank), + Err(Error::Qwen3BatchTooLarge { + actual: 2, + limit: 1, + .. + }) + )); + assert!(matches!( + reranker(&artifact, limits(128, 6, 1)?)?.predict(batch(vec![(" ", POSITIVE_DOCUMENT)])), + Err(Error::EmptyQuery { index: 0, .. }) + )); + Ok(()) +} + +#[test] +fn malformed_artifact_metadata_and_tokenizer_cannot_bypass_admission() -> TestResult<()> { + assert_metadata_refusal(enable_automatic_bos, "tokenizer.ggml.add_bos_token")?; + assert_metadata_refusal(remove_chat_template, "tokenizer.chat_template")?; + + let mut raw = raw_rank_fixture()?; + reverse_artifact_vocabulary(&mut raw)?; + let (_directory, artifact) = verified_artifact(&raw)?; + let error = reranker(&artifact, limits(128, 6, 1)?) + .err() + .ok_or("reversed artifact vocabulary unexpectedly passed admission")?; + if !matches!( + &error, + Error::Qwen3Tokenizer { + source: TokenizerError::VocabularyMismatch { id: 0, .. }, + .. + } + ) || StdError::source(&error).is_none() + { + return Err("artifact vocabulary refusal lost its typed source chain".into()); + } + + let mut raw = raw_rank_fixture()?; + invalid_chat_template(&mut raw)?; + let (_directory, artifact) = verified_artifact(&raw)?; + let error = reranker(&artifact, limits(128, 6, 1)?) + .err() + .ok_or("invalid artifact template unexpectedly passed admission")?; + if !matches!(&error, Error::Qwen3Template { .. }) || StdError::source(&error).is_none() { + return Err("template admission failure lost its typed source chain".into()); + } + + let raw = raw_rank_fixture()?; + let (_directory, artifact) = verified_artifact(&raw)?; + let tokenizer = tokenizer_with_swapped_vocabulary()?; + let error = Qwen3Reranker::from_verified_cpu( + &artifact, + tokenizer, + limits(128, 6, 1)?, + INSTRUCTION.to_string(), + ) + .err() + .ok_or("mismatched tokenizer unexpectedly passed admission")?; + if !matches!( + &error, + Error::Qwen3Tokenizer { + source: TokenizerError::VocabularyMismatch { id: 9, .. }, + .. + } + ) || StdError::source(&error).is_none() + { + return Err("tokenizer admission failure lost its typed source chain".into()); + } + Ok(()) +} + +#[test] +fn configured_tokenizer_padding_or_truncation_is_refused_before_prediction() -> TestResult<()> { + let raw = raw_rank_fixture()?; + let (_directory, artifact) = verified_artifact(&raw)?; + let truncation = tokenizer_json().replace( + "\"truncation\":null", + "\"truncation\":{\"direction\":\"Right\",\"max_length\":5,\"strategy\":\"LongestFirst\",\"stride\":0}", + ); + let truncation_error = Qwen3Reranker::from_verified_cpu( + &artifact, + verified_tokenizer(&truncation)?, + limits(128, 6, 1)?, + INSTRUCTION.to_string(), + ) + .err() + .ok_or("configured truncation unexpectedly passed rerank setup")?; + if !matches!( + &truncation_error, + Error::Qwen3Tokenizer { + source: TokenizerError::ConfiguredTruncation { .. }, + .. + } + ) || StdError::source(&truncation_error).is_none() + { + return Err("configured truncation refusal lost its typed source chain".into()); + } + + let padding = tokenizer_json().replace( + "\"padding\":null", + "\"padding\":{\"strategy\":{\"Fixed\":4},\"direction\":\"Right\",\"pad_to_multiple_of\":null,\"pad_id\":0,\"pad_type_id\":0,\"pad_token\":\"[UNK]\"}", + ); + let padding_error = Qwen3Reranker::from_verified_cpu( + &artifact, + verified_tokenizer(&padding)?, + limits(128, 6, 1)?, + INSTRUCTION.to_string(), + ) + .err() + .ok_or("configured padding unexpectedly passed rerank setup")?; + if !matches!( + &padding_error, + Error::Qwen3Tokenizer { + source: TokenizerError::ConfiguredPadding { .. }, + .. + } + ) || StdError::source(&padding_error).is_none() + { + return Err("configured padding refusal lost its typed source chain".into()); + } + Ok(()) +} + +#[test] +fn late_head_refusal_returns_an_error_and_pristine_retry_remains_usable() -> TestResult<()> { + let mut malformed = raw_rank_fixture()?; + set_f32_prefix(&mut malformed, "cls.output.weight", f32::NAN)?; + let (_bad_directory, bad_artifact) = verified_artifact(&malformed)?; + let bad = reranker(&bad_artifact, limits(128, 6, 1)?)?; + let error = bad + .predict(batch(vec![(POSITIVE_QUERY, POSITIVE_DOCUMENT)])) + .err() + .ok_or("late nonfinite rank head unexpectedly returned predictions")?; + if !matches!(&error, Error::Qwen3Decoder { .. }) || StdError::source(&error).is_none() { + return Err("late decoder refusal lost its typed source chain".into()); + } + + let raw = raw_rank_fixture()?; + let (_directory, artifact) = verified_artifact(&raw)?; + let pristine = reranker(&artifact, limits(128, 6, 1)?)?; + let retry = pristine.predict(batch(vec![(POSITIVE_QUERY, POSITIVE_DOCUMENT)]))?; + assert_score( + retry + .get(&0) + .ok_or("pristine retry prediction is missing")?, + hand_score([1.0, 0.0])?, + "pristine retry", + ) +} + +#[test] +fn later_item_refusal_discards_partial_batch_and_allows_same_instance_retry() -> TestResult<()> { + let raw = raw_rank_fixture()?; + let (_directory, artifact) = verified_artifact(&raw)?; + let reranker = reranker(&artifact, limits(128, 6, 2)?)?; + let error = reranker + .predict(batch(vec![ + (POSITIVE_QUERY, POSITIVE_DOCUMENT), + (POSITIVE_QUERY, "docpos docpos"), + ])) + .err() + .ok_or("late oversized item unexpectedly returned a partial prediction map")?; + if !matches!( + error, + Error::Qwen3InputTokensTooLong { + index: 1, + actual: 7, + limit: 6, + .. + } + ) { + return Err("late oversized item did not retain its precise indexed refusal".into()); + } + + let retry = reranker.predict(batch(vec![(POSITIVE_QUERY, POSITIVE_DOCUMENT)]))?; + if retry.keys().copied().collect::>() != [0] { + return Err("same reranker instance did not return a pristine retry map".into()); + } + assert_score( + retry + .get(&0) + .ok_or("same-instance retry prediction is missing")?, + hand_score([1.0, 0.0])?, + "same-instance retry", + ) +} + +fn reranker<'artifact>( + artifact: &'artifact VerifiedArtifact, + limits: Qwen3RerankerLimits, +) -> TestResult> { + reranker_with_instruction(artifact, limits, INSTRUCTION) +} + +fn reranker_with_instruction<'artifact>( + artifact: &'artifact VerifiedArtifact, + limits: Qwen3RerankerLimits, + instruction: &str, +) -> TestResult> { + Ok(Qwen3Reranker::from_verified_cpu( + artifact, + tokenizer()?, + limits, + instruction.to_string(), + )?) +} + +fn limits( + max_pair_bytes: usize, + max_tokens: usize, + max_batch_items: usize, +) -> TestResult { + Ok(Qwen3RerankerLimits { + max_pair_bytes, + max_tokens, + max_batch_items, + template: TemplateLimits::new(1_024, 1_024, 10_000, 16)?, + }) +} + +fn batch(pairs: Vec<(&str, &str)>) -> RerankBatch { + RerankBatch { + items: pairs + .into_iter() + .map(|(query, document)| item(query, document)) + .collect(), + } +} + +fn item(query: &str, document: &str) -> RerankItem { + RerankItem { + query: query.to_string(), + document: document.to_string(), + } +} + +fn assert_score(actual: &[f32], expected: f64, label: &str) -> TestResult<()> { + let [actual] = actual else { + return Err(format!("{label} did not return exactly one score").into()); + }; + let actual = f64::from(*actual); + let tolerance = 1.0e-5 + 1.0e-5 * actual.abs().max(expected.abs()); + if !actual.is_finite() || (actual - expected).abs() > tolerance { + return Err(format!( + "{label} score {actual} did not match independent expected {expected}" + ) + .into()); + } + Ok(()) +} + +fn hand_score(final_embedding: [f64; HIDDEN]) -> TestResult { + let mean_square = final_embedding + .iter() + .map(|value| value * value) + .sum::() + / f64::from(u32::try_from(HIDDEN)?); + let scale = (mean_square + f64::from(EPSILON)).sqrt().recip(); + let normalized = [final_embedding[0] * scale, final_embedding[1] * scale]; + let yes = 2.0 * normalized[0] - normalized[1]; + let no = -normalized[0] + 2.0 * normalized[1]; + Ok(yes - no) +} + +fn tokenizer() -> TestResult { + verified_tokenizer(&tokenizer_json()) +} + +fn tokenizer_with_swapped_vocabulary() -> TestResult { + verified_tokenizer( + &tokenizer_json().replace("\"docpos\":9,\"docneg\":10", "\"docpos\":10,\"docneg\":9"), + ) +} + +fn verified_tokenizer(source: &str) -> TestResult { + let bytes = source.as_bytes(); + let digest = TokenizerDigest::from_bytes(Sha256::digest(bytes).into()); + Ok(VerifiedTokenizer::from_bytes( + bytes, + TokenizerIdentity::new(bytes.len(), digest), + TokenizerByteLimit::try_new(bytes.len())?, + )?) +} + +fn tokenizer_json() -> String { + r#"{ + "version":"1.0", "truncation":null, "padding":null, + "added_tokens":[ + {"id":1,"content":"<|im_start|>","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true}, + {"id":2,"content":"<|im_end|>","single_word":false,"lstrip":false,"rstrip":false,"normalized":false,"special":true} + ], + "normalizer":null, "pre_tokenizer":{"type":"Whitespace"}, "post_processor":null, "decoder":null, + "model":{"type":"WordLevel","vocab":{"[UNK]":0,"<|im_start|>":1,"<|im_end|>":2,"system":3,"query":4,"document":5,"instruct":6,"positive":7,"negative":8,"docpos":9,"docneg":10,"different":11},"unk_token":"[UNK]"} + }"# + .to_string() +} + +fn verified_artifact(raw: &RawGguf) -> TestResult<(tempfile::TempDir, VerifiedArtifact)> { + let fixture = serialize_raw_gguf(raw)?; + let directory = tempfile::tempdir()?; + let path = directory.path().join("synthetic-qwen3-rank.gguf"); + std::fs::write(&path, &fixture.bytes)?; + let limit = NonZeroU64::new(fixture.byte_len).ok_or("serialized fixture is empty")?; + Ok(( + directory, + VerifiedArtifact::load( + &path, + Sha256Digest::from_bytes(fixture.sha256), + ArtifactByteLimit::new(limit), + )?, + )) +} + +fn raw_rank_fixture() -> TestResult { + let tokens = vocabulary(); + let mut embedding = vec![0.0; tokens.len() * HIDDEN]; + set_row( + &mut embedding, + token_id(&tokens, POSITIVE_DOCUMENT)?, + [1.0, 0.0], + )?; + set_row( + &mut embedding, + token_id(&tokens, NEGATIVE_DOCUMENT)?, + [0.0, 1.0], + )?; + let zeros = |width: usize| vec![0.0; width]; + Ok(RawGguf { + metadata: vec![ + metadata_string("general.architecture", "qwen3"), + metadata_u32("qwen3.block_count", 1), + metadata_u32("qwen3.context_length", u32::try_from(CONTEXT)?), + metadata_u32("qwen3.embedding_length", u32::try_from(HIDDEN)?), + metadata_u32("qwen3.feed_forward_length", u32::try_from(FEED_FORWARD)?), + metadata_u32("qwen3.attention.head_count", 1), + metadata_u32("qwen3.attention.head_count_kv", 1), + metadata_u32("qwen3.attention.key_length", u32::try_from(HIDDEN)?), + metadata_u32("qwen3.attention.value_length", u32::try_from(HIDDEN)?), + metadata_f32("qwen3.attention.layer_norm_rms_epsilon", EPSILON), + metadata_bool("qwen3.attention.causal", true), + metadata_u32("qwen3.rope.dimension_count", u32::try_from(HIDDEN)?), + metadata_f32("qwen3.rope.freq_base", 10_000.0), + metadata_u32("qwen3.pooling_type", 4), + RawMetadata { + key: "qwen3.classifier.output_labels".to_string(), + value: RawMetadataValue::StringArray(vec!["yes".to_string(), "no".to_string()]), + }, + RawMetadata { + key: "tokenizer.ggml.tokens".to_string(), + value: RawMetadataValue::StringArray(tokens.clone()), + }, + metadata_string("tokenizer.chat_template", TEMPLATE), + metadata_bool("tokenizer.ggml.add_bos_token", false), + metadata_bool("tokenizer.ggml.add_eos_token", false), + ], + tensors: vec![ + f32_tensor("token_embd.weight", &[HIDDEN, tokens.len()], &embedding)?, + f32_tensor("output_norm.weight", &[HIDDEN], &[1.0, 1.0])?, + f32_tensor("blk.0.attn_norm.weight", &[HIDDEN], &[1.0, 1.0])?, + f32_tensor("blk.0.attn_q_norm.weight", &[HIDDEN], &[1.0, 1.0])?, + f32_tensor("blk.0.attn_k_norm.weight", &[HIDDEN], &[1.0, 1.0])?, + f32_tensor("blk.0.ffn_norm.weight", &[HIDDEN], &[1.0, 1.0])?, + f32_tensor( + "blk.0.attn_q.weight", + &[HIDDEN, HIDDEN], + &zeros(HIDDEN * HIDDEN), + )?, + f32_tensor( + "blk.0.attn_k.weight", + &[HIDDEN, HIDDEN], + &zeros(HIDDEN * HIDDEN), + )?, + f32_tensor( + "blk.0.attn_v.weight", + &[HIDDEN, HIDDEN], + &zeros(HIDDEN * HIDDEN), + )?, + f32_tensor( + "blk.0.attn_output.weight", + &[HIDDEN, HIDDEN], + &zeros(HIDDEN * HIDDEN), + )?, + f32_tensor( + "blk.0.ffn_gate.weight", + &[HIDDEN, FEED_FORWARD], + &zeros(HIDDEN * FEED_FORWARD), + )?, + f32_tensor( + "blk.0.ffn_up.weight", + &[HIDDEN, FEED_FORWARD], + &zeros(HIDDEN * FEED_FORWARD), + )?, + f32_tensor( + "blk.0.ffn_down.weight", + &[FEED_FORWARD, HIDDEN], + &zeros(FEED_FORWARD * HIDDEN), + )?, + f32_tensor("cls.output.weight", &[HIDDEN, 2], &[2.0, -1.0, -1.0, 2.0])?, + ], + }) +} + +fn vocabulary() -> Vec { + [ + "[UNK]", + "<|im_start|>", + "<|im_end|>", + "system", + "query", + "document", + INSTRUCTION, + POSITIVE_QUERY, + NEGATIVE_QUERY, + POSITIVE_DOCUMENT, + NEGATIVE_DOCUMENT, + "different", + ] + .into_iter() + .map(str::to_string) + .collect() +} + +fn token_id(tokens: &[String], token: &str) -> TestResult { + tokens + .iter() + .position(|candidate| candidate == token) + .ok_or_else(|| format!("synthetic vocabulary does not contain `{token}`").into()) +} + +fn set_row(values: &mut [f32], row: usize, replacement: [f32; HIDDEN]) -> TestResult<()> { + let start = row + .checked_mul(HIDDEN) + .ok_or("embedding row start overflow")?; + let target = values + .get_mut(start..start + HIDDEN) + .ok_or("embedding row is outside synthetic matrix")?; + target.copy_from_slice(&replacement); + Ok(()) +} + +fn f32_tensor(name: &str, dimensions: &[usize], values: &[f32]) -> TestResult { + let expected = dimensions.iter().try_fold(1_usize, |total, dimension| { + total + .checked_mul(*dimension) + .ok_or("tensor element count overflow") + })?; + if values.len() != expected { + return Err(format!( + "synthetic tensor `{name}` has {} values, expected {expected}", + values.len() + ) + .into()); + } + let mut payload = Vec::with_capacity(expected * std::mem::size_of::()); + for value in values { + payload.extend_from_slice(&value.to_le_bytes()); + } + Ok(RawTensor { + name: name.to_string(), + dims: dimensions + .iter() + .map(|dimension| u64::try_from(*dimension)) + .collect::, _>>()?, + format: F32, + payload, + }) +} + +fn metadata_string(key: &str, value: &str) -> RawMetadata { + RawMetadata { + key: key.to_string(), + value: RawMetadataValue::String(value.to_string()), + } +} + +fn metadata_u32(key: &str, value: u32) -> RawMetadata { + RawMetadata { + key: key.to_string(), + value: RawMetadataValue::U32(value), + } +} + +fn metadata_f32(key: &str, value: f32) -> RawMetadata { + RawMetadata { + key: key.to_string(), + value: RawMetadataValue::F32(value), + } +} + +fn metadata_bool(key: &str, value: bool) -> RawMetadata { + RawMetadata { + key: key.to_string(), + value: RawMetadataValue::Bool(value), + } +} + +fn enable_automatic_bos(raw: &mut RawGguf) -> TestResult<()> { + replace_metadata( + raw, + "tokenizer.ggml.add_bos_token", + RawMetadataValue::Bool(true), + ) +} + +fn assert_metadata_refusal( + mutate: fn(&mut RawGguf) -> TestResult<()>, + key: &'static str, +) -> TestResult<()> { + let mut raw = raw_rank_fixture()?; + mutate(&mut raw)?; + let (_directory, artifact) = verified_artifact(&raw)?; + let error = reranker(&artifact, limits(128, 6, 1)?) + .err() + .ok_or("malformed metadata unexpectedly passed rerank admission")?; + if !matches!(&error, Error::Qwen3Metadata { key: actual, .. } if *actual == key) { + return Err(format!("metadata key `{key}` was not precisely reported").into()); + } + Ok(()) +} + +fn remove_chat_template(raw: &mut RawGguf) -> TestResult<()> { + let position = raw + .metadata + .iter() + .position(|entry| entry.key == "tokenizer.chat_template") + .ok_or("synthetic template metadata is missing")?; + raw.metadata.remove(position); + Ok(()) +} + +fn invalid_chat_template(raw: &mut RawGguf) -> TestResult<()> { + replace_metadata( + raw, + "tokenizer.chat_template", + RawMetadataValue::String("{% if".to_string()), + ) +} + +fn reverse_artifact_vocabulary(raw: &mut RawGguf) -> TestResult<()> { + replace_metadata( + raw, + "tokenizer.ggml.tokens", + RawMetadataValue::StringArray(vocabulary().into_iter().rev().collect()), + ) +} + +fn replace_metadata(raw: &mut RawGguf, key: &str, value: RawMetadataValue) -> TestResult<()> { + let entry = raw + .metadata + .iter_mut() + .find(|entry| entry.key == key) + .ok_or_else(|| format!("synthetic metadata `{key}` is missing"))?; + entry.value = value; + Ok(()) +} + +fn set_f32_prefix(raw: &mut RawGguf, name: &str, value: f32) -> TestResult<()> { + let tensor = raw + .tensors + .iter_mut() + .find(|tensor| tensor.name == name) + .ok_or_else(|| format!("synthetic tensor `{name}` is missing"))?; + tensor + .payload + .get_mut(..4) + .ok_or("synthetic tensor has no first F32 lane")? + .copy_from_slice(&value.to_le_bytes()); + Ok(()) +} From 0a810d8bf9bc945eed884ede691d94a1b41e5b18 Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 20:10:04 -0500 Subject: [PATCH 13/15] fix(rerank): retain typed adapter test errors --- crates/rerank/src/qwen3/tests.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/rerank/src/qwen3/tests.rs b/crates/rerank/src/qwen3/tests.rs index 3673a75..3b6dce1 100644 --- a/crates/rerank/src/qwen3/tests.rs +++ b/crates/rerank/src/qwen3/tests.rs @@ -160,14 +160,16 @@ fn malformed_artifact_metadata_and_tokenizer_cannot_bypass_admission() -> TestRe let (_directory, artifact) = verified_artifact(&raw)?; let error = reranker(&artifact, limits(128, 6, 1)?) .err() - .ok_or("reversed artifact vocabulary unexpectedly passed admission")?; + .ok_or("reversed artifact vocabulary unexpectedly passed admission")? + .downcast_ref::() + .ok_or("artifact vocabulary admission failure was not a rerank error")?; if !matches!( - &error, + error, Error::Qwen3Tokenizer { source: TokenizerError::VocabularyMismatch { id: 0, .. }, .. } - ) || StdError::source(&error).is_none() + ) || StdError::source(error).is_none() { return Err("artifact vocabulary refusal lost its typed source chain".into()); } @@ -177,8 +179,10 @@ fn malformed_artifact_metadata_and_tokenizer_cannot_bypass_admission() -> TestRe let (_directory, artifact) = verified_artifact(&raw)?; let error = reranker(&artifact, limits(128, 6, 1)?) .err() - .ok_or("invalid artifact template unexpectedly passed admission")?; - if !matches!(&error, Error::Qwen3Template { .. }) || StdError::source(&error).is_none() { + .ok_or("invalid artifact template unexpectedly passed admission")? + .downcast_ref::() + .ok_or("artifact template admission failure was not a rerank error")?; + if !matches!(error, Error::Qwen3Template { .. }) || StdError::source(error).is_none() { return Err("template admission failure lost its typed source chain".into()); } @@ -648,8 +652,10 @@ fn assert_metadata_refusal( let (_directory, artifact) = verified_artifact(&raw)?; let error = reranker(&artifact, limits(128, 6, 1)?) .err() - .ok_or("malformed metadata unexpectedly passed rerank admission")?; - if !matches!(&error, Error::Qwen3Metadata { key: actual, .. } if *actual == key) { + .ok_or("malformed metadata unexpectedly passed rerank admission")? + .downcast_ref::() + .ok_or("malformed metadata admission failure was not a rerank error")?; + if !matches!(error, Error::Qwen3Metadata { key: actual, .. } if *actual == key) { return Err(format!("metadata key `{key}` was not precisely reported").into()); } Ok(()) From 43904d35f9149b880e1bf267fbc9a7d16ab51dfb Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 20:10:35 -0500 Subject: [PATCH 14/15] test(rerank): retain ownership across typed error borrows --- crates/rerank/src/qwen3/tests.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/rerank/src/qwen3/tests.rs b/crates/rerank/src/qwen3/tests.rs index 3b6dce1..851bfd2 100644 --- a/crates/rerank/src/qwen3/tests.rs +++ b/crates/rerank/src/qwen3/tests.rs @@ -160,7 +160,8 @@ fn malformed_artifact_metadata_and_tokenizer_cannot_bypass_admission() -> TestRe let (_directory, artifact) = verified_artifact(&raw)?; let error = reranker(&artifact, limits(128, 6, 1)?) .err() - .ok_or("reversed artifact vocabulary unexpectedly passed admission")? + .ok_or("reversed artifact vocabulary unexpectedly passed admission")?; + let error = error .downcast_ref::() .ok_or("artifact vocabulary admission failure was not a rerank error")?; if !matches!( @@ -179,7 +180,8 @@ fn malformed_artifact_metadata_and_tokenizer_cannot_bypass_admission() -> TestRe let (_directory, artifact) = verified_artifact(&raw)?; let error = reranker(&artifact, limits(128, 6, 1)?) .err() - .ok_or("invalid artifact template unexpectedly passed admission")? + .ok_or("invalid artifact template unexpectedly passed admission")?; + let error = error .downcast_ref::() .ok_or("artifact template admission failure was not a rerank error")?; if !matches!(error, Error::Qwen3Template { .. }) || StdError::source(error).is_none() { @@ -652,7 +654,8 @@ fn assert_metadata_refusal( let (_directory, artifact) = verified_artifact(&raw)?; let error = reranker(&artifact, limits(128, 6, 1)?) .err() - .ok_or("malformed metadata unexpectedly passed rerank admission")? + .ok_or("malformed metadata unexpectedly passed rerank admission")?; + let error = error .downcast_ref::() .ok_or("malformed metadata admission failure was not a rerank error")?; if !matches!(error, Error::Qwen3Metadata { key: actual, .. } if *actual == key) { From 4208bb777194b82875cba10917f6a5128dd30f8e Mon Sep 17 00:00:00 2001 From: Cody Kickertz Date: Sun, 6 Sep 2026 20:13:18 -0500 Subject: [PATCH 15/15] fix(rerank): satisfy strict integrated lint contracts --- crates/rerank/src/qwen3.rs | 5 ++--- crates/rerank/src/qwen3/tests.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/rerank/src/qwen3.rs b/crates/rerank/src/qwen3.rs index 5b64948..6961635 100644 --- a/crates/rerank/src/qwen3.rs +++ b/crates/rerank/src/qwen3.rs @@ -92,9 +92,8 @@ impl<'artifact> Qwen3Reranker<'artifact> { require_no_automatic_specials(metadata, ADD_EOS)?; verify_special(&tokenizer, vocabulary.len(), IM_START)?; verify_special(&tokenizer, vocabulary.len(), IM_END)?; - let template_source = match metadata.get(CHAT_TEMPLATE) { - Some(MetaValue::String(source)) => source, - _ => return Qwen3MetadataSnafu { key: CHAT_TEMPLATE }.fail(), + let Some(MetaValue::String(template_source)) = metadata.get(CHAT_TEMPLATE) else { + return Qwen3MetadataSnafu { key: CHAT_TEMPLATE }.fail(); }; let template = BoundedTemplate::new(template_source, limits.template).context(Qwen3TemplateSnafu)?; diff --git a/crates/rerank/src/qwen3/tests.rs b/crates/rerank/src/qwen3/tests.rs index 851bfd2..3a4427f 100644 --- a/crates/rerank/src/qwen3/tests.rs +++ b/crates/rerank/src/qwen3/tests.rs @@ -33,7 +33,7 @@ const TEMPLATE: &str = "{% if messages[0].role == \"system\" and messages[1].rol fn signed_score_is_raw_yes_minus_no_and_refuses_f32_overflow() -> TestResult<()> { let positive = signed_relevance_score(0, [3.5, -1.25])?; let negative = signed_relevance_score(1, [-1.25, 3.5])?; - if positive != 4.75 || negative != -4.75 { + if positive.to_bits() != 4.75_f32.to_bits() || negative.to_bits() != (-4.75_f32).to_bits() { return Err("signed rerank score must remain raw yes minus no logits".into()); } assert!(matches!( @@ -328,10 +328,10 @@ fn later_item_refusal_discards_partial_batch_and_allows_same_instance_retry() -> ) } -fn reranker<'artifact>( - artifact: &'artifact VerifiedArtifact, +fn reranker( + artifact: &VerifiedArtifact, limits: Qwen3RerankerLimits, -) -> TestResult> { +) -> TestResult> { reranker_with_instruction(artifact, limits, INSTRUCTION) } @@ -454,6 +454,10 @@ fn verified_artifact(raw: &RawGguf) -> TestResult<(tempfile::TempDir, VerifiedAr )) } +#[expect( + clippy::too_many_lines, + reason = "keep the complete original synthetic model inventory together for oracle review" +)] fn raw_rank_fixture() -> TestResult { let tokens = vocabulary(); let mut embedding = vec![0.0; tokens.len() * HIDDEN];