leindex 2.1.1

LeIndex MCP and semantic code search engine for AI tools and large codebases
//! Fresh-pass KV-cache input feeding for decoder-style ONNX exports.
//!
//! `use_cache`-style exports (the default `qwen3-embed-0.6b-dynamic-uint8`
//! model) declare two `past_key_values.{layer}.{key,value}` inputs per layer —
//! 56 tensors for the 28-layer default model. ONNX Runtime requires every
//! declared input to be fed; omitting them fails inference at the first KV
//! `Concat` with `Missing Input: past_key_values.0.value` and the whole neural
//! path silently degrades to TF-IDF.
//!
//! Embedding workloads never use a cache: every request encodes a fresh
//! sequence. The correct feed for a fresh pass is a ZERO-LENGTH cache —
//! `[batch, num_kv_heads, 0, head_dim]` per input — which is exactly what
//! `transformers` passes when `use_cache=False` is exported with legacy
//! cache inputs kept. Verified against the shipped model: feeding zero-length
//! caches yields a clean `last_hidden_state` of `[batch, seq, hidden]`.
//!
//! Models without KV inputs (BERT/GTE-style, e.g. `sfr-embedding-code-400m`)
//! get an empty vector back and nothing changes for them.

use crate::embed::protocol::{ErrorKind, WorkerError};
use ndarray::ArrayD;
use ort::session::Session;
use ort::value::Tensor;

/// Prefix matching every legacy KV-cache input name (`past_key_values.0.key`, …).
pub const PAST_KEY_VALUES_PREFIX: &str = "past_key_values";

/// One declared KV-cache input with the shape information needed to build a
/// zero-length stand-in for it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KvInput {
    /// Full input name, e.g. `past_key_values.13.value`.
    pub name: String,
    /// Key-value head count (shape dim 1) when statically declared.
    pub kv_heads: Option<usize>,
    /// Head dimension (shape dim 3) when statically declared.
    pub head_dim: Option<usize>,
}

impl KvInput {
    /// Parse one session input outlet into a [`KvInput`] if it is a
    /// KV-cache input, `None` otherwise.
    fn from_outlet(outlet: &ort::value::Outlet) -> Option<Self> {
        let name = outlet.name();
        if !name.starts_with(PAST_KEY_VALUES_PREFIX) {
            return None;
        }
        let dims: Vec<i64> = outlet
            .dtype()
            .tensor_shape()
            .map(|shape| shape.iter().copied().collect())
            .unwrap_or_default();
        // Declared layout is [batch, num_kv_heads, past_sequence_length, head_dim].
        let static_dim = |index: usize| {
            dims.get(index)
                .and_then(|dim| (*dim > 0).then_some(*dim as usize))
        };
        Some(Self {
            name: name.to_string(),
            kv_heads: static_dim(1),
            head_dim: static_dim(3),
        })
    }
}

/// Detect every KV-cache input the session declares.
///
/// Returns an empty vector for models without legacy cache inputs, so callers
/// can append the result unconditionally.
pub fn detect_kv_inputs(session: &Session) -> Vec<KvInput> {
    session
        .inputs()
        .iter()
        .filter_map(KvInput::from_outlet)
        .collect()
}

/// Default head layout for the Qwen3-0.6B embedder when the export leaves the
/// KV dimensions symbolic. The shipped `qwen3-embed-0.6b-dynamic-uint8` model
/// declares 8 KV heads × 128-dim heads (GQA: 4 of the 32 query heads).
const DEFAULT_KV_HEADS: usize = 8;
const DEFAULT_HEAD_DIM: usize = 128;

/// Build zero-length KV-cache tensors for one inference batch.
///
/// Every tensor has shape `[batch_size, kv_heads, 0, head_dim]` — no bytes of
/// cache, which is the mathematically correct fresh-pass feed. Returns
/// `(name, tensor)` pairs ready to append to a dynamic input list.
pub fn zero_length_kv_tensors(
    kv_inputs: &[KvInput],
    batch_size: usize,
) -> Result<Vec<(String, Tensor<f32>)>, WorkerError> {
    kv_inputs
        .iter()
        .map(|input| {
            let kv_heads = input.kv_heads.unwrap_or(DEFAULT_KV_HEADS);
            let head_dim = input.head_dim.unwrap_or(DEFAULT_HEAD_DIM);
            // [batch, heads, 0, dim]: the zero-length sequence dimension makes
            // the cache empty regardless of the other dims.
            let array = ArrayD::<f32>::from_shape_vec(
                ndarray::IxDyn(&[batch_size, kv_heads, 0, head_dim]),
                Vec::new(),
            )
            .map_err(|e| WorkerError {
                kind: ErrorKind::Inference,
                message: format!("failed to create {} array: {}", input.name, e),
            })?;
            let tensor = Tensor::from_array(array).map_err(|e| WorkerError {
                kind: ErrorKind::Inference,
                message: format!("failed to create {} tensor: {}", input.name, e),
            })?;
            Ok((input.name.clone(), tensor))
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn kv_input_parses_declared_shape() {
        // Shape iterator yields i64 dims; positive = static, negative/None = symbolic.
        // Unit-tested through detect via a real session in the integration
        // tests; here we validate the static-dim extraction logic directly.
        let input = KvInput {
            name: "past_key_values.0.key".to_string(),
            kv_heads: Some(8),
            head_dim: Some(128),
        };
        assert_eq!(input.name, "past_key_values.0.key");
        assert_eq!(input.kv_heads, Some(8));
        assert_eq!(input.head_dim, Some(128));
    }

    #[test]
    fn zero_length_tensors_have_empty_sequence_dim() {
        // Constructing an ort Tensor calls into the dynamically loaded ORT
        // library; skip (loudly) on machines without it rather than crash.
        // CI never executes onnx-feature tests, so this only affects dev
        // machines — same pattern as tests/embed_migraphx_dynamic_test.rs.
        if matches!(
            crate::embed::ort_discovery::discover_and_init(),
            crate::embed::ort_discovery::InitResult::NotFound { .. }
        ) {
            eprintln!("SKIP: no ONNX Runtime library available for tensor construction");
            return;
        }
        let kv = vec![
            KvInput {
                name: "past_key_values.0.key".to_string(),
                kv_heads: Some(8),
                head_dim: Some(128),
            },
            KvInput {
                name: "past_key_values.27.value".to_string(),
                kv_heads: None,
                head_dim: None,
            },
        ];
        let tensors = zero_length_kv_tensors(&kv, 2).unwrap();
        assert_eq!(tensors.len(), 2);
        assert_eq!(tensors[0].0, "past_key_values.0.key");
        let shape: Vec<usize> = tensors[0].1.shape().iter().map(|&d| d as usize).collect();
        assert_eq!(shape, vec![2, 8, 0, 128]);
        // Symbolic-dim input falls back to the Qwen3-0.6B defaults.
        let shape2: Vec<usize> = tensors[1].1.shape().iter().map(|&d| d as usize).collect();
        assert_eq!(shape2, vec![2, 8, 0, 128]);
    }

    #[test]
    fn empty_kv_list_is_noop() {
        assert!(zero_length_kv_tensors(&[], 4).unwrap().is_empty());
    }
}