Skip to main content

llama_cpp_4/context/
tensor_transaction.rs

1//! Bounded, owned tensor capture and transactional mutation during decode.
2//!
3//! A [`TensorTransactions`] value is moved into
4//! [`LlamaContextParams::with_tensor_transactions`](crate::LlamaContextParams::with_tensor_transactions).
5//! The resulting [`crate::LlamaContext`] owns the callback state for its complete
6//! native lifetime. Matching tensors are synchronized by llama.cpp, copied into
7//! Rust-owned storage, and optionally transformed. Mutable tensors are written
8//! back exactly once only after the handler returns successfully and every
9//! output value passes validation.
10
11// This module already documents every `unsafe` block; the lint locks that in.
12// See the note on it in the workspace `Cargo.toml`.
13#![deny(clippy::undocumented_unsafe_blocks)]
14
15use std::ffi::c_void;
16use std::fmt;
17use std::panic::{catch_unwind, AssertUnwindSafe};
18
19/// Maximum exact graph selectors attached to one context.
20pub const MAX_TENSOR_SELECTORS: usize = 128;
21/// Maximum UTF-8 bytes in one exact graph-node name.
22pub const MAX_TENSOR_NAME_BYTES: usize = 256;
23/// Maximum rows accepted from one selected tensor invocation.
24pub const MAX_TENSOR_ROWS: usize = 4_096;
25/// Maximum elements copied by one selected tensor invocation.
26pub const MAX_TENSOR_ELEMENTS: usize = 16_777_216;
27/// Maximum retained tensor bytes from one decode.
28pub const MAX_RETAINED_TENSOR_BYTES: usize = MAX_TENSOR_ELEMENTS * size_of::<f32>();
29/// Maximum retained callback failure bytes.
30pub const MAX_TENSOR_FAILURE_BYTES: usize = 1_024;
31
32/// Element representation required by an exact tensor selector.
33#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
34pub enum TensorElementType {
35    /// IEEE-754 single-precision values.
36    F32,
37    /// Signed 32-bit integer values.
38    I32,
39}
40
41impl TensorElementType {
42    const fn native(self) -> llama_cpp_sys_4::ggml_type {
43        match self {
44            Self::F32 => llama_cpp_sys_4::GGML_TYPE_F32,
45            Self::I32 => llama_cpp_sys_4::GGML_TYPE_I32,
46        }
47    }
48}
49
50/// Native access granted to one selected tensor.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum TensorAccess {
53    /// Copy and optionally retain the tensor without native mutation.
54    ReadOnly,
55    /// Run the handler over finite `f32` values and commit once on success.
56    ReadWriteF32,
57}
58
59/// Native write-back decision returned by a transaction handler.
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum TensorWriteback {
62    /// Discard handler-side edits and leave the native tensor unchanged.
63    Unchanged,
64    /// Validate and commit the complete edited tensor exactly once.
65    Commit,
66}
67
68/// Mapping between tensor rows and the submitted decode batch.
69#[derive(Clone, Copy, Debug, PartialEq, Eq)]
70pub enum TensorRowMapping {
71    /// Rows correspond in order to logical token entries in the decode batch.
72    BatchTokens,
73}
74
75/// How aggressively a selector validates that `f32` values are finite.
76///
77/// Finiteness validation is a full pass over the tensor copy. For a
78/// `ReadWriteF32` selector the strict policy scans the native values *and* the
79/// committed output, so a large mutable capture pays for two passes per
80/// callback. Relax the policy when the caller already trusts the data.
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
82pub enum TensorFiniteValidation {
83    /// Reject a non-finite native value before the handler runs and a
84    /// non-finite handler output before commit. This is the default.
85    #[default]
86    Strict,
87    /// Skip the pre-handler scan; still reject a non-finite committed output.
88    OutputOnly,
89    /// Skip both scans. The caller guarantees finite values.
90    Trusted,
91}
92
93impl TensorFiniteValidation {
94    /// Whether the native values are scanned before the handler runs.
95    const fn checks_input(self) -> bool {
96        matches!(self, Self::Strict)
97    }
98
99    /// Whether the committed output is scanned before write-back.
100    const fn checks_output(self) -> bool {
101        matches!(self, Self::Strict | Self::OutputOnly)
102    }
103}
104
105/// Exact bounded graph-node contract.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct TensorSelector {
108    name: String,
109    element_type: TensorElementType,
110    row_elements: usize,
111    maximum_rows: usize,
112    access: TensorAccess,
113    row_mapping: TensorRowMapping,
114    retain: bool,
115    finite: TensorFiniteValidation,
116}
117
118impl TensorSelector {
119    /// Constructs an exact tensor selector.
120    ///
121    /// # Errors
122    ///
123    /// Returns an error for an empty, NUL-containing, or excessive name,
124    /// unusable dimensions, an excessive element bound, or mutable non-`f32`
125    /// data.
126    pub fn new(
127        name: impl Into<String>,
128        element_type: TensorElementType,
129        row_elements: usize,
130        maximum_rows: usize,
131        access: TensorAccess,
132        row_mapping: TensorRowMapping,
133        retain: bool,
134    ) -> Result<Self, TensorTransactionError> {
135        let selector = Self {
136            name: name.into(),
137            element_type,
138            row_elements,
139            maximum_rows,
140            access,
141            row_mapping,
142            retain,
143            finite: TensorFiniteValidation::default(),
144        };
145        selector.validate()?;
146        Ok(selector)
147    }
148
149    /// Sets how aggressively `f32` values are validated as finite.
150    ///
151    /// Defaults to [`TensorFiniteValidation::Strict`]. Relaxing this trades a
152    /// full pass (or two, for `ReadWriteF32`) over the tensor for the caller's
153    /// guarantee that the values are already finite.
154    #[must_use]
155    pub const fn with_finite_validation(mut self, finite: TensorFiniteValidation) -> Self {
156        self.finite = finite;
157        self
158    }
159
160    /// Returns the finiteness-validation policy.
161    #[must_use]
162    pub const fn finite_validation(&self) -> TensorFiniteValidation {
163        self.finite
164    }
165
166    /// Constructs one exact residual layer-output selector.
167    ///
168    /// llama.cpp names these graph nodes `l_out-{layer}` at the pinned
169    /// implementation. The caller remains responsible for binding that naming
170    /// profile to its backend revision.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error for unusable dimensions or bounds.
175    pub fn layer_output(
176        layer: u32,
177        row_elements: usize,
178        maximum_rows: usize,
179        access: TensorAccess,
180        retain: bool,
181    ) -> Result<Self, TensorTransactionError> {
182        Self::new(
183            format!("l_out-{layer}"),
184            TensorElementType::F32,
185            row_elements,
186            maximum_rows,
187            access,
188            TensorRowMapping::BatchTokens,
189            retain,
190        )
191    }
192
193    /// Returns the exact graph-node name.
194    #[must_use]
195    pub fn name(&self) -> &str {
196        &self.name
197    }
198
199    /// Returns the required element representation.
200    #[must_use]
201    pub const fn element_type(&self) -> TensorElementType {
202        self.element_type
203    }
204
205    /// Returns the required elements per row.
206    #[must_use]
207    pub const fn row_elements(&self) -> usize {
208        self.row_elements
209    }
210
211    /// Returns the inclusive row bound.
212    #[must_use]
213    pub const fn maximum_rows(&self) -> usize {
214        self.maximum_rows
215    }
216
217    /// Returns native access granted to the handler.
218    #[must_use]
219    pub const fn access(&self) -> TensorAccess {
220        self.access
221    }
222
223    /// Returns how native rows map to the submitted decode batch.
224    #[must_use]
225    pub const fn row_mapping(&self) -> TensorRowMapping {
226        self.row_mapping
227    }
228
229    /// Returns whether the completed owned tensor is retained after decode.
230    #[must_use]
231    pub const fn retains_capture(&self) -> bool {
232        self.retain
233    }
234
235    fn validate(&self) -> Result<(), TensorTransactionError> {
236        if self.name.is_empty()
237            || self.name.len() > MAX_TENSOR_NAME_BYTES
238            || self.name.as_bytes().contains(&0)
239        {
240            return Err(TensorTransactionError::new(
241                "tensor name must be bounded, nonempty UTF-8 without NUL",
242            ));
243        }
244        let elements = self
245            .row_elements
246            .checked_mul(self.maximum_rows)
247            .ok_or_else(|| TensorTransactionError::new("tensor element bound overflowed"))?;
248        if self.row_elements == 0
249            || self.maximum_rows == 0
250            || self.maximum_rows > MAX_TENSOR_ROWS
251            || elements > MAX_TENSOR_ELEMENTS
252        {
253            return Err(TensorTransactionError::new(
254                "tensor row shape is outside the supported bound",
255            ));
256        }
257        if self.access == TensorAccess::ReadWriteF32 && self.element_type != TensorElementType::F32
258        {
259            return Err(TensorTransactionError::new(
260                "only f32 tensors support transactional write-back",
261            ));
262        }
263        Ok(())
264    }
265}
266
267/// Exact sequence and causal-position metadata for one tensor row.
268#[derive(Clone, Debug, PartialEq, Eq)]
269pub struct TensorBatchRow {
270    /// Zero-based logical batch entry.
271    pub batch_index: u32,
272    /// Native causal position.
273    pub position: i32,
274    /// Exact sequence IDs attached to this entry.
275    pub sequence_ids: Vec<i32>,
276}
277
278/// Validated tensor dimensions.
279#[derive(Clone, Copy, Debug, PartialEq, Eq)]
280pub struct TensorShape {
281    /// Elements in each logical row.
282    pub row_elements: usize,
283    /// Logical rows in this callback invocation.
284    pub rows: usize,
285    /// Total elements.
286    pub elements: usize,
287}
288
289/// Typed Rust-owned tensor storage supplied to a callback.
290pub enum TensorDataMut<'a> {
291    /// Mutable finite `f32` storage.
292    F32(&'a mut [f32]),
293    /// Mutable copied `i32` storage. Read-only selectors never commit changes.
294    I32(&'a mut [i32]),
295}
296
297impl fmt::Debug for TensorDataMut<'_> {
298    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
299        match self {
300            Self::F32(values) => formatter
301                .debug_tuple("F32")
302                .field(&format_args!("{} elements", values.len()))
303                .finish(),
304            Self::I32(values) => formatter
305                .debug_tuple("I32")
306                .field(&format_args!("{} elements", values.len()))
307                .finish(),
308        }
309    }
310}
311
312/// One owned tensor transaction presented synchronously to Rust.
313pub struct TensorTransaction<'a> {
314    /// Exact graph-node name.
315    pub name: &'a str,
316    /// Validated shape.
317    pub shape: TensorShape,
318    /// Exact logical rows represented by this native tensor.
319    pub rows: &'a [TensorBatchRow],
320    /// Native access granted by the selector.
321    pub access: TensorAccess,
322    /// Typed Rust-owned values.
323    pub data: TensorDataMut<'a>,
324}
325
326impl fmt::Debug for TensorTransaction<'_> {
327    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
328        formatter
329            .debug_struct("TensorTransaction")
330            .field("name", &self.name)
331            .field("shape", &self.shape)
332            .field("rows", &self.rows)
333            .field("access", &self.access)
334            .field("data", &self.data)
335            .finish()
336    }
337}
338
339/// Synchronous safe handler for selected tensor transactions.
340pub trait TensorTransactionHandler: Send {
341    /// Applies caller-defined mechanics to one Rust-owned tensor copy.
342    ///
343    /// For [`TensorAccess::ReadWriteF32`], successful finite output is written
344    /// back exactly once after this method returns. Returning an error or
345    /// unwinding causes no write-back.
346    ///
347    /// # Errors
348    ///
349    /// Returns an implementation-defined bounded failure.
350    fn apply(
351        &mut self,
352        transaction: TensorTransaction<'_>,
353    ) -> Result<TensorWriteback, TensorTransactionError>;
354}
355
356/// Any suitable closure is a handler, so callers can pass one directly to
357/// [`TensorTransactions::new`] instead of defining a dedicated type.
358impl<F> TensorTransactionHandler for F
359where
360    F: FnMut(TensorTransaction<'_>) -> Result<TensorWriteback, TensorTransactionError> + Send,
361{
362    fn apply(
363        &mut self,
364        transaction: TensorTransaction<'_>,
365    ) -> Result<TensorWriteback, TensorTransactionError> {
366        self(transaction)
367    }
368}
369
370/// Error returned by a tensor transaction handler or selector validator.
371#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
372#[error("{message}")]
373pub struct TensorTransactionError {
374    message: String,
375}
376
377impl TensorTransactionError {
378    /// Creates a bounded transaction error.
379    pub fn new(message: impl Into<String>) -> Self {
380        let mut message = message.into();
381        if message.len() > MAX_TENSOR_FAILURE_BYTES {
382            message.truncate(MAX_TENSOR_FAILURE_BYTES);
383        }
384        Self { message }
385    }
386
387    /// Returns the bounded failure message.
388    #[must_use]
389    pub fn message(&self) -> &str {
390        &self.message
391    }
392}
393
394/// Contained callback failure returned by [`crate::LlamaContext::decode`].
395#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
396#[error("tensor callback failed{tensor_suffix}: {message}")]
397pub struct TensorCallbackFailure {
398    tensor: Option<String>,
399    tensor_suffix: String,
400    panicked: bool,
401    message: String,
402}
403
404impl TensorCallbackFailure {
405    fn new(tensor: Option<&str>, panicked: bool, message: impl Into<String>) -> Self {
406        let mut message = message.into();
407        if message.len() > MAX_TENSOR_FAILURE_BYTES {
408            message.truncate(MAX_TENSOR_FAILURE_BYTES);
409        }
410        let tensor = tensor.map(ToOwned::to_owned);
411        let tensor_suffix = tensor
412            .as_deref()
413            .map_or_else(String::new, |name| format!(" for {name}"));
414        Self {
415            tensor,
416            tensor_suffix,
417            panicked,
418            message,
419        }
420    }
421
422    /// Returns the exact tensor name when failure happened after selection.
423    #[must_use]
424    pub fn tensor(&self) -> Option<&str> {
425        self.tensor.as_deref()
426    }
427
428    /// Returns whether Rust unwinding was contained.
429    #[must_use]
430    pub const fn panicked(&self) -> bool {
431        self.panicked
432    }
433
434    /// Returns the bounded failure message.
435    #[must_use]
436    pub fn message(&self) -> &str {
437        &self.message
438    }
439}
440
441/// Complete retained typed tensor from one callback invocation.
442#[derive(Clone, Debug, PartialEq)]
443pub struct TransactionalTensorCapture {
444    /// Exact graph-node name.
445    pub name: String,
446    /// Validated shape.
447    pub shape: TensorShape,
448    /// Exact logical batch rows.
449    pub rows: Vec<TensorBatchRow>,
450    /// Typed copied values after a successful handler invocation.
451    pub data: CapturedTensorData,
452}
453
454/// Typed retained tensor storage.
455#[derive(Clone, Debug, PartialEq)]
456pub enum CapturedTensorData {
457    /// Finite `f32` values.
458    F32(Vec<f32>),
459    /// Signed integer values.
460    I32(Vec<i32>),
461}
462
463impl CapturedTensorData {
464    /// Returns the retained element count.
465    #[must_use]
466    pub fn len(&self) -> usize {
467        match self {
468            Self::F32(values) => values.len(),
469            Self::I32(values) => values.len(),
470        }
471    }
472
473    /// Returns whether no elements are retained.
474    #[must_use]
475    pub fn is_empty(&self) -> bool {
476        self.len() == 0
477    }
478}
479
480/// Owned, bounded callback program attached to one context.
481pub struct TensorTransactions {
482    selectors: Vec<TensorSelector>,
483    handler: Option<Box<dyn TensorTransactionHandler>>,
484    captures: Vec<TransactionalTensorCapture>,
485    retained_bytes: usize,
486    pending_captures: Vec<TransactionalTensorCapture>,
487    pending_retained_bytes: usize,
488    batch_rows: Vec<TensorBatchRow>,
489    /// Rows covered so far this decode, indexed by selector position (the
490    /// selectors are sorted, so the index is stable and avoids a keyed map).
491    rows_seen: Vec<usize>,
492    /// Reused scratch for the pre-handler rollback copy of a `ReadWriteF32`
493    /// tensor, so a mutable selector does not allocate every callback.
494    rollback_f32: Vec<f32>,
495    failure: Option<TensorCallbackFailure>,
496    decode_active: bool,
497}
498
499impl fmt::Debug for TensorTransactions {
500    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
501        formatter
502            .debug_struct("TensorTransactions")
503            .field("selectors", &self.selectors)
504            .field("has_handler", &self.handler.is_some())
505            .field("captures", &self.captures.len())
506            .field("retained_bytes", &self.retained_bytes)
507            .field("pending_captures", &self.pending_captures.len())
508            .field("pending_retained_bytes", &self.pending_retained_bytes)
509            .field("failure", &self.failure)
510            .field("decode_active", &self.decode_active)
511            .finish_non_exhaustive()
512    }
513}
514
515impl TensorTransactions {
516    /// Constructs a read-only capture program.
517    ///
518    /// # Errors
519    ///
520    /// Returns an error for empty, excessive, duplicate, unordered, mutable,
521    /// or collectively over-bound selectors.
522    pub fn capture(selectors: Vec<TensorSelector>) -> Result<Self, TensorTransactionError> {
523        Self::build(selectors, None)
524    }
525
526    /// Constructs a capture/mutation program with one synchronous handler.
527    ///
528    /// # Errors
529    ///
530    /// Returns an error for empty, excessive, duplicate, unordered, or
531    /// collectively over-bound selectors.
532    pub fn new(
533        selectors: Vec<TensorSelector>,
534        handler: impl TensorTransactionHandler + 'static,
535    ) -> Result<Self, TensorTransactionError> {
536        Self::build(selectors, Some(Box::new(handler)))
537    }
538
539    fn build(
540        selectors: Vec<TensorSelector>,
541        handler: Option<Box<dyn TensorTransactionHandler>>,
542    ) -> Result<Self, TensorTransactionError> {
543        if selectors.is_empty() || selectors.len() > MAX_TENSOR_SELECTORS {
544            return Err(TensorTransactionError::new(
545                "selector count is outside the supported bound",
546            ));
547        }
548        let mut total_elements = 0_usize;
549        let mut prior_name: Option<&str> = None;
550        let mut needs_handler = false;
551        for selector in &selectors {
552            selector.validate()?;
553            if prior_name.is_some_and(|prior| prior >= selector.name()) {
554                return Err(TensorTransactionError::new(
555                    "selectors must have unique canonically ordered names",
556                ));
557            }
558            prior_name = Some(selector.name());
559            needs_handler |= selector.access == TensorAccess::ReadWriteF32;
560            total_elements = total_elements
561                .checked_add(
562                    selector
563                        .row_elements
564                        .checked_mul(selector.maximum_rows)
565                        .ok_or_else(|| {
566                            TensorTransactionError::new("selector element bound overflowed")
567                        })?,
568                )
569                .ok_or_else(|| {
570                    TensorTransactionError::new("total selector element bound overflowed")
571                })?;
572        }
573        if total_elements > MAX_TENSOR_ELEMENTS {
574            return Err(TensorTransactionError::new(
575                "total selector element bound is excessive",
576            ));
577        }
578        if needs_handler && handler.is_none() {
579            return Err(TensorTransactionError::new(
580                "mutable selectors require a transaction handler",
581            ));
582        }
583        if !needs_handler && handler.is_some() {
584            return Err(TensorTransactionError::new(
585                "a transaction handler requires at least one mutable selector",
586            ));
587        }
588        let selector_count = selectors.len();
589        Ok(Self {
590            selectors,
591            handler,
592            captures: Vec::new(),
593            retained_bytes: 0,
594            pending_captures: Vec::new(),
595            pending_retained_bytes: 0,
596            batch_rows: Vec::new(),
597            rows_seen: vec![0; selector_count],
598            rollback_f32: Vec::new(),
599            failure: None,
600            decode_active: false,
601        })
602    }
603
604    /// Returns exact selector contracts.
605    #[must_use]
606    pub fn selectors(&self) -> &[TensorSelector] {
607        &self.selectors
608    }
609
610    /// Returns successful retained tensors accumulated since the last drain.
611    ///
612    /// A native speculative operation may perform several internal decodes.
613    /// Each decode commits its retained tensors only after its lifecycle and
614    /// selector coverage complete successfully.
615    #[must_use]
616    pub fn captures(&self) -> &[TransactionalTensorCapture] {
617        &self.captures
618    }
619
620    /// Removes retained tensors accumulated since the previous drain.
621    pub fn take_captures(&mut self) -> Vec<TransactionalTensorCapture> {
622        self.retained_bytes = 0;
623        std::mem::take(&mut self.captures)
624    }
625
626    /// Returns the contained failure from the most recent decode.
627    #[must_use]
628    pub const fn failure(&self) -> Option<&TensorCallbackFailure> {
629        self.failure.as_ref()
630    }
631
632    fn begin_decode_raw(
633        &mut self,
634        batch: &llama_cpp_sys_4::llama_batch,
635    ) -> Result<(), TensorCallbackFailure> {
636        if let Some(failure) = self.failure.clone() {
637            return Err(failure);
638        }
639        if self.decode_active {
640            return Err(TensorCallbackFailure::new(
641                None,
642                false,
643                "tensor callback decode was already active",
644            ));
645        }
646        self.pending_captures.clear();
647        self.pending_retained_bytes = 0;
648        self.rows_seen.fill(0);
649        self.batch_rows = copy_batch_rows(batch)?;
650        self.decode_active = true;
651        Ok(())
652    }
653
654    pub(crate) fn finish_decode(
655        &mut self,
656        native_succeeded: bool,
657    ) -> Result<(), TensorCallbackFailure> {
658        self.decode_active = false;
659        let expected_rows = self.batch_rows.len();
660        self.batch_rows.clear();
661        if let Some(failure) = self.failure.clone() {
662            self.pending_captures.clear();
663            self.pending_retained_bytes = 0;
664            return Err(failure);
665        }
666        if !native_succeeded {
667            self.pending_captures.clear();
668            self.pending_retained_bytes = 0;
669            return Ok(());
670        }
671        for (index, selector) in self.selectors.iter().enumerate() {
672            let rows = self.rows_seen[index];
673            let complete = match selector.row_mapping {
674                TensorRowMapping::BatchTokens => rows == expected_rows,
675            };
676            if !complete {
677                let failure = TensorCallbackFailure::new(
678                    Some(selector.name()),
679                    false,
680                    format!(
681                        "selected tensor covered {rows} rows but the decode submitted \
682                         {expected_rows}"
683                    ),
684                );
685                self.failure = Some(failure.clone());
686                self.pending_captures.clear();
687                self.pending_retained_bytes = 0;
688                return Err(failure);
689            }
690        }
691        self.retained_bytes = self
692            .retained_bytes
693            .checked_add(self.pending_retained_bytes)
694            .ok_or_else(|| {
695                TensorCallbackFailure::new(None, false, "committed retained byte count overflowed")
696            })?;
697        self.captures.append(&mut self.pending_captures);
698        self.pending_retained_bytes = 0;
699        Ok(())
700    }
701
702    fn selected(&self, name: &[u8]) -> Option<usize> {
703        self.selectors
704            .binary_search_by(|selector| selector.name().as_bytes().cmp(name))
705            .ok()
706    }
707
708    fn process(
709        &mut self,
710        tensor: *mut llama_cpp_sys_4::ggml_tensor,
711        selector_index: usize,
712    ) -> Result<(), TensorTransactionError> {
713        // Disjoint field borrows so the per-callback path neither clones the
714        // whole selector nor allocates the batch rows on the common path.
715        let staged = {
716            let Self {
717                selectors,
718                handler,
719                batch_rows,
720                rows_seen,
721                rollback_f32,
722                ..
723            } = &mut *self;
724            let selector = &selectors[selector_index];
725            let shape = validate_tensor(tensor, selector)?;
726            let start = match selector.row_mapping {
727                TensorRowMapping::BatchTokens => rows_seen[selector_index],
728            };
729            let end = start
730                .checked_add(shape.rows)
731                .ok_or_else(|| TensorTransactionError::new("tensor row mapping overflowed"))?;
732            if end > batch_rows.len() {
733                return Err(TensorTransactionError::new(
734                    "tensor rows exceed submitted decode batch",
735                ));
736            }
737
738            let captured: Option<CapturedTensorData> = match selector.element_type {
739                TensorElementType::F32 => {
740                    // The native copy fills the entire buffer, so it is left
741                    // uninitialized rather than zeroed first.
742                    let mut values = read_tensor::<f32>(tensor, shape.elements)?;
743                    if selector.finite.checks_input() && !all_finite(&values) {
744                        return Err(TensorTransactionError::new(
745                            "selected f32 tensor contains a non-finite value",
746                        ));
747                    }
748                    if selector.access == TensorAccess::ReadWriteF32 {
749                        // Keep a rollback copy only when the pre-handler values
750                        // are also retained; the scratch is reused each call.
751                        let rolled_back = selector.retain;
752                        if rolled_back {
753                            rollback_f32.clear();
754                            rollback_f32.extend_from_slice(&values);
755                        }
756                        let handler = handler.as_deref_mut().ok_or_else(|| {
757                            TensorTransactionError::new("mutable tensor handler is unavailable")
758                        })?;
759                        let writeback = handler.apply(TensorTransaction {
760                            name: selector.name(),
761                            shape,
762                            rows: &batch_rows[start..end],
763                            access: selector.access,
764                            data: TensorDataMut::F32(&mut values),
765                        })?;
766                        match writeback {
767                            TensorWriteback::Unchanged => {
768                                if rolled_back {
769                                    values.clear();
770                                    values.extend_from_slice(rollback_f32);
771                                }
772                            }
773                            TensorWriteback::Commit => {
774                                if selector.finite.checks_output() && !all_finite(&values) {
775                                    return Err(TensorTransactionError::new(
776                                        "transaction produced a non-finite f32 value",
777                                    ));
778                                }
779                                copy_tensor_set(tensor, &values)?;
780                            }
781                        }
782                    }
783                    selector.retain.then_some(CapturedTensorData::F32(values))
784                }
785                TensorElementType::I32 => {
786                    let values = read_tensor::<i32>(tensor, shape.elements)?;
787                    selector.retain.then_some(CapturedTensorData::I32(values))
788                }
789            };
790
791            rows_seen[selector_index] = end;
792
793            // Only the retained path pays for the owned name and row copies.
794            captured.map(|data| {
795                (
796                    selectors[selector_index].name().to_owned(),
797                    shape,
798                    batch_rows[start..end].to_vec(),
799                    data,
800                )
801            })
802        };
803
804        if let Some((name, shape, rows, data)) = staged {
805            self.retain(name, shape, rows, data)?;
806        }
807        Ok(())
808    }
809
810    fn retain(
811        &mut self,
812        name: String,
813        shape: TensorShape,
814        rows: Vec<TensorBatchRow>,
815        data: CapturedTensorData,
816    ) -> Result<(), TensorTransactionError> {
817        let bytes = data
818            .len()
819            .checked_mul(size_of::<f32>())
820            .ok_or_else(|| TensorTransactionError::new("retained byte count overflowed"))?;
821        self.pending_retained_bytes = self
822            .pending_retained_bytes
823            .checked_add(bytes)
824            .ok_or_else(|| TensorTransactionError::new("retained byte count overflowed"))?;
825        let total_retained_bytes = self
826            .retained_bytes
827            .checked_add(self.pending_retained_bytes)
828            .ok_or_else(|| TensorTransactionError::new("retained byte count overflowed"))?;
829        if total_retained_bytes > MAX_RETAINED_TENSOR_BYTES {
830            return Err(TensorTransactionError::new(
831                "retained tensor bytes exceed the supported bound",
832            ));
833        }
834        self.pending_captures.push(TransactionalTensorCapture {
835            name,
836            shape,
837            rows,
838            data,
839        });
840        Ok(())
841    }
842
843    fn record_failure(&mut self, tensor: Option<&str>, panicked: bool, message: impl Into<String>) {
844        if self.failure.is_none() {
845            self.failure = Some(TensorCallbackFailure::new(tensor, panicked, message));
846        }
847    }
848}
849
850fn validate_tensor(
851    tensor: *mut llama_cpp_sys_4::ggml_tensor,
852    selector: &TensorSelector,
853) -> Result<TensorShape, TensorTransactionError> {
854    if tensor.is_null() {
855        return Err(TensorTransactionError::new(
856            "native tensor pointer was null",
857        ));
858    }
859    // SAFETY: llama.cpp supplies a live graph tensor for the synchronous
860    // callback. No reference escapes this function.
861    let tensor_ref = unsafe { &*tensor };
862    if tensor_ref.type_ != selector.element_type.native() {
863        return Err(TensorTransactionError::new(
864            "native tensor element type does not match selector",
865        ));
866    }
867    if tensor_ref.ne[2] != 1 || tensor_ref.ne[3] != 1 {
868        return Err(TensorTransactionError::new(
869            "selected tensor must be a two-dimensional row matrix",
870        ));
871    }
872    let row_elements = usize::try_from(tensor_ref.ne[0])
873        .map_err(|_| TensorTransactionError::new("native row width is negative or excessive"))?;
874    let rows = usize::try_from(tensor_ref.ne[1])
875        .map_err(|_| TensorTransactionError::new("native row count is negative or excessive"))?;
876    let elements = row_elements
877        .checked_mul(rows)
878        .ok_or_else(|| TensorTransactionError::new("native tensor element count overflowed"))?;
879    if row_elements != selector.row_elements
880        || rows == 0
881        || rows > selector.maximum_rows
882        || elements > MAX_TENSOR_ELEMENTS
883    {
884        return Err(TensorTransactionError::new(
885            "native tensor shape does not match selector",
886        ));
887    }
888    // SAFETY: the pointer is live for this callback.
889    if !unsafe { llama_cpp_sys_4::ggml_is_contiguous(tensor) } {
890        return Err(TensorTransactionError::new(
891            "selected tensor is not contiguous",
892        ));
893    }
894    let expected_bytes = elements
895        .checked_mul(size_of::<f32>())
896        .ok_or_else(|| TensorTransactionError::new("native tensor byte count overflowed"))?;
897    // SAFETY: the pointer is live for this callback.
898    if unsafe { llama_cpp_sys_4::ggml_nbytes(tensor) } != expected_bytes {
899        return Err(TensorTransactionError::new(
900            "native tensor byte size does not match selector",
901        ));
902    }
903    Ok(TensorShape {
904        row_elements,
905        rows,
906        elements,
907    })
908}
909
910/// Reads a contiguous native tensor into a fresh, exactly-sized `Vec<T>`.
911///
912/// The buffer is left uninitialized and filled directly by the native copy —
913/// the previous implementation zeroed a `vec![0; n]` that was immediately
914/// overwritten. `T` must be a plain `Copy` element type (`f32`/`i32`).
915fn read_tensor<T: Copy>(
916    tensor: *mut llama_cpp_sys_4::ggml_tensor,
917    elements: usize,
918) -> Result<Vec<T>, TensorTransactionError> {
919    let bytes = elements
920        .checked_mul(size_of::<T>())
921        .ok_or_else(|| TensorTransactionError::new("native tensor byte count overflowed"))?;
922    if bytes == 0 {
923        return Err(TensorTransactionError::new(
924            "cannot copy an empty native tensor",
925        ));
926    }
927    let mut values: Vec<T> = Vec::with_capacity(elements);
928    // SAFETY: `validate_tensor` proved the native tensor is exactly `bytes`
929    // contiguous bytes; `Vec::with_capacity(elements)` reserves that many `T`
930    // slots and `ggml_backend_tensor_get` writes every one before `set_len`, so
931    // no uninitialized element is ever read. `T: Copy` has no drop glue.
932    unsafe {
933        llama_cpp_sys_4::ggml_backend_tensor_get(
934            tensor,
935            values.as_mut_ptr().cast::<c_void>(),
936            0,
937            bytes,
938        );
939        values.set_len(elements);
940    }
941    Ok(values)
942}
943
944/// Branchless finiteness scan. A finite `f32` has an exponent field that is not
945/// all ones (an all-ones exponent encodes `inf`/`nan`). The `|` fold lets the
946/// compiler vectorize the pass rather than emit a per-element `is_finite`.
947fn all_finite(values: &[f32]) -> bool {
948    const EXPONENT_MASK: u32 = 0x7F80_0000;
949    let mut non_finite = 0_u32;
950    for &value in values {
951        non_finite |= u32::from((value.to_bits() & EXPONENT_MASK) == EXPONENT_MASK);
952    }
953    non_finite == 0
954}
955
956fn copy_tensor_set<T>(
957    tensor: *mut llama_cpp_sys_4::ggml_tensor,
958    values: &[T],
959) -> Result<(), TensorTransactionError> {
960    let bytes = size_of_val(values);
961    if bytes == 0 {
962        return Err(TensorTransactionError::new(
963            "cannot write an empty native tensor",
964        ));
965    }
966    // SAFETY: `validate_tensor` proves the selected native tensor has exactly
967    // this contiguous byte size. llama.cpp synchronized this node before the
968    // callback and later dependent nodes have not executed.
969    unsafe {
970        llama_cpp_sys_4::ggml_backend_tensor_set(
971            tensor,
972            values.as_ptr().cast::<c_void>(),
973            0,
974            bytes,
975        );
976    }
977    Ok(())
978}
979
980fn copy_batch_rows(
981    batch: &llama_cpp_sys_4::llama_batch,
982) -> Result<Vec<TensorBatchRow>, TensorCallbackFailure> {
983    let count = usize::try_from(batch.n_tokens).map_err(|_| {
984        TensorCallbackFailure::new(None, false, "decode batch token count is negative")
985    })?;
986    if count == 0 || count > MAX_TENSOR_ROWS {
987        return Err(TensorCallbackFailure::new(
988            None,
989            false,
990            "decode batch token count is outside the callback bound",
991        ));
992    }
993    if batch.pos.is_null() || batch.n_seq_id.is_null() || batch.seq_id.is_null() {
994        return Err(TensorCallbackFailure::new(
995            None,
996            false,
997            "decode batch metadata pointers are null",
998        ));
999    }
1000    let mut rows = Vec::with_capacity(count);
1001    for index in 0..count {
1002        // SAFETY: the native `llama_batch` contract provides arrays allocated
1003        // for at least `n_tokens` entries, and the begin hook synchronously
1004        // borrows the batch for this copy.
1005        let position = unsafe { *batch.pos.add(index) };
1006        // SAFETY: same allocation contract as `position`.
1007        let sequence_count = unsafe { *batch.n_seq_id.add(index) };
1008        let sequence_count = usize::try_from(sequence_count).map_err(|_| {
1009            TensorCallbackFailure::new(None, false, "decode batch sequence count is negative")
1010        })?;
1011        if sequence_count == 0 || sequence_count > MAX_TENSOR_ROWS {
1012            return Err(TensorCallbackFailure::new(
1013                None,
1014                false,
1015                "decode batch sequence count is outside the callback bound",
1016            ));
1017        }
1018        // SAFETY: the native `llama_batch` contract provides a sequence array
1019        // with exactly `n_seq_id[index]` entries.
1020        let sequence_ptr = unsafe { *batch.seq_id.add(index) };
1021        if sequence_ptr.is_null() {
1022            return Err(TensorCallbackFailure::new(
1023                None,
1024                false,
1025                "decode batch sequence pointer is null",
1026            ));
1027        }
1028        // SAFETY: validated count and live batch allocation above.
1029        let sequence_ids =
1030            unsafe { std::slice::from_raw_parts(sequence_ptr, sequence_count) }.to_vec();
1031        rows.push(TensorBatchRow {
1032            batch_index: u32::try_from(index)
1033                .map_err(|_| TensorCallbackFailure::new(None, false, "batch index exceeds u32"))?,
1034            position,
1035            sequence_ids,
1036        });
1037    }
1038    Ok(rows)
1039}
1040
1041pub(crate) unsafe extern "C" fn tensor_transaction_decode_begin(
1042    batch: *const llama_cpp_sys_4::llama_batch,
1043    user_data: *mut c_void,
1044) -> bool {
1045    if batch.is_null() || user_data.is_null() {
1046        return false;
1047    }
1048    // SAFETY: context parameters retain the pinned transaction owner for every
1049    // native decode and llama.cpp supplies a live batch for this call.
1050    let state = unsafe { &mut *user_data.cast::<TensorTransactions>() };
1051    // SAFETY: null was rejected and the batch remains live synchronously.
1052    let batch = unsafe { &*batch };
1053    let result = catch_unwind(AssertUnwindSafe(|| state.begin_decode_raw(batch)));
1054    match result {
1055        Ok(Ok(())) => true,
1056        Ok(Err(error)) => {
1057            state.record_failure(None, false, error.to_string());
1058            false
1059        }
1060        Err(_) => {
1061            state.record_failure(None, true, "tensor decode-begin callback panicked");
1062            false
1063        }
1064    }
1065}
1066
1067pub(crate) unsafe extern "C" fn tensor_transaction_decode_end(
1068    native_succeeded: bool,
1069    user_data: *mut c_void,
1070) -> bool {
1071    if user_data.is_null() {
1072        return false;
1073    }
1074    // SAFETY: context parameters retain the pinned transaction owner for every
1075    // native decode.
1076    let state = unsafe { &mut *user_data.cast::<TensorTransactions>() };
1077    let result = catch_unwind(AssertUnwindSafe(|| state.finish_decode(native_succeeded)));
1078    match result {
1079        Ok(Ok(())) => true,
1080        Ok(Err(error)) => {
1081            state.record_failure(error.tensor(), error.panicked(), error.message());
1082            false
1083        }
1084        Err(_) => {
1085            state.record_failure(None, true, "tensor decode-end callback panicked");
1086            false
1087        }
1088    }
1089}
1090
1091pub(crate) unsafe extern "C" fn tensor_transaction_callback(
1092    tensor: *mut llama_cpp_sys_4::ggml_tensor,
1093    ask: bool,
1094    user_data: *mut c_void,
1095) -> bool {
1096    if tensor.is_null() || user_data.is_null() {
1097        return false;
1098    }
1099    // SAFETY: `LlamaContextParams::with_tensor_transactions` installs a pointer
1100    // to pinned state owned by the context for the complete native lifetime.
1101    let state = unsafe { &mut *user_data.cast::<TensorTransactions>() };
1102    if !state.decode_active {
1103        state.record_failure(
1104            None,
1105            false,
1106            "tensor evaluation callback ran outside a decode lifecycle",
1107        );
1108        return false;
1109    }
1110    if state.failure.is_some() {
1111        // Do not request more callbacks, allowing the scheduler to finish the
1112        // remaining graph without another Rust boundary.
1113        return false;
1114    }
1115    // SAFETY: graph tensor names are fixed NUL-terminated arrays.
1116    let name_bytes = unsafe { &(*tensor).name };
1117    let length = name_bytes
1118        .iter()
1119        .position(|value| *value == 0)
1120        .unwrap_or(name_bytes.len());
1121    // SAFETY: `c_char` and `u8` have identical byte width. Names are matched as
1122    // raw bytes against the selector set, so the common per-node path skips
1123    // UTF-8 validation entirely.
1124    let raw_name = unsafe { std::slice::from_raw_parts(name_bytes.as_ptr().cast::<u8>(), length) };
1125    let Some(selector_index) = state.selected(raw_name) else {
1126        return false;
1127    };
1128    if ask {
1129        return true;
1130    }
1131
1132    let result = catch_unwind(AssertUnwindSafe(|| state.process(tensor, selector_index)));
1133    match result {
1134        Ok(Ok(())) => true,
1135        Ok(Err(error)) => {
1136            // The matched selector name is valid UTF-8 by construction.
1137            let name = state.selectors[selector_index].name().to_owned();
1138            state.record_failure(Some(&name), false, error.to_string());
1139            true
1140        }
1141        Err(payload) => {
1142            let message = payload
1143                .downcast_ref::<&str>()
1144                .map_or_else(
1145                    || {
1146                        payload
1147                            .downcast_ref::<String>()
1148                            .map_or("tensor handler panicked", String::as_str)
1149                    },
1150                    |message| *message,
1151                )
1152                .to_owned();
1153            let name = state.selectors[selector_index].name().to_owned();
1154            state.record_failure(Some(&name), true, message);
1155            true
1156        }
1157    }
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162    use super::*;
1163
1164    struct AddOne;
1165
1166    impl TensorTransactionHandler for AddOne {
1167        fn apply(
1168            &mut self,
1169            mut transaction: TensorTransaction<'_>,
1170        ) -> Result<TensorWriteback, TensorTransactionError> {
1171            let TensorDataMut::F32(values) = &mut transaction.data else {
1172                return Err(TensorTransactionError::new("expected f32"));
1173            };
1174            for value in values.iter_mut() {
1175                *value += 1.0;
1176            }
1177            Ok(TensorWriteback::Commit)
1178        }
1179    }
1180
1181    #[test]
1182    fn selectors_are_bounded_and_canonical() {
1183        let selector = TensorSelector::layer_output(1, 4, 2, TensorAccess::ReadOnly, true).unwrap();
1184        assert_eq!(selector.name(), "l_out-1");
1185        assert!(TensorSelector::new(
1186            "bad\0name",
1187            TensorElementType::F32,
1188            4,
1189            2,
1190            TensorAccess::ReadOnly,
1191            TensorRowMapping::BatchTokens,
1192            true,
1193        )
1194        .is_err());
1195        assert!(TensorSelector::new(
1196            "integer",
1197            TensorElementType::I32,
1198            4,
1199            2,
1200            TensorAccess::ReadWriteF32,
1201            TensorRowMapping::BatchTokens,
1202            true,
1203        )
1204        .is_err());
1205    }
1206
1207    #[test]
1208    fn transaction_sets_require_a_handler_and_ordered_names() {
1209        let mutable =
1210            TensorSelector::layer_output(1, 4, 2, TensorAccess::ReadWriteF32, false).unwrap();
1211        assert!(TensorTransactions::capture(vec![mutable.clone()]).is_err());
1212        assert!(TensorTransactions::new(vec![mutable], AddOne).is_ok());
1213
1214        let later = TensorSelector::layer_output(2, 4, 2, TensorAccess::ReadOnly, true).unwrap();
1215        let earlier = TensorSelector::layer_output(1, 4, 2, TensorAccess::ReadOnly, true).unwrap();
1216        assert!(TensorTransactions::capture(vec![later, earlier]).is_err());
1217    }
1218
1219    #[test]
1220    fn errors_and_failure_messages_are_bounded() {
1221        let error = TensorTransactionError::new("x".repeat(MAX_TENSOR_FAILURE_BYTES + 10));
1222        assert_eq!(error.message().len(), MAX_TENSOR_FAILURE_BYTES);
1223        let failure = TensorCallbackFailure::new(
1224            Some("l_out-1"),
1225            true,
1226            "y".repeat(MAX_TENSOR_FAILURE_BYTES + 10),
1227        );
1228        assert!(failure.panicked());
1229        assert_eq!(failure.message().len(), MAX_TENSOR_FAILURE_BYTES);
1230        assert_eq!(failure.tensor(), Some("l_out-1"));
1231    }
1232
1233    #[test]
1234    fn successful_internal_decodes_accumulate_and_failed_staging_is_discarded() {
1235        let selector = TensorSelector::layer_output(1, 1, 1, TensorAccess::ReadOnly, true).unwrap();
1236        let mut transactions = TensorTransactions::capture(vec![selector]).unwrap();
1237
1238        let stage = |transactions: &mut TensorTransactions, value: f32, succeeded: bool| {
1239            transactions.decode_active = true;
1240            transactions.batch_rows = vec![TensorBatchRow {
1241                batch_index: 0,
1242                position: 0,
1243                sequence_ids: vec![0],
1244            }];
1245            transactions.rows_seen[0] = 1;
1246            transactions
1247                .retain(
1248                    "l_out-1".to_owned(),
1249                    TensorShape {
1250                        row_elements: 1,
1251                        rows: 1,
1252                        elements: 1,
1253                    },
1254                    transactions.batch_rows.clone(),
1255                    CapturedTensorData::F32(vec![value]),
1256                )
1257                .unwrap();
1258            transactions.finish_decode(succeeded).unwrap();
1259        };
1260
1261        stage(&mut transactions, 1.0, true);
1262        stage(&mut transactions, 2.0, true);
1263        stage(&mut transactions, 3.0, false);
1264        let captures = transactions.take_captures();
1265        assert_eq!(captures.len(), 2);
1266        assert!(matches!(
1267            captures[0].data,
1268            CapturedTensorData::F32(ref values) if values == &[1.0]
1269        ));
1270        assert!(matches!(
1271            captures[1].data,
1272            CapturedTensorData::F32(ref values) if values == &[2.0]
1273        ));
1274        assert!(transactions.captures().is_empty());
1275    }
1276
1277    #[test]
1278    fn closures_are_handlers() {
1279        // A closure is accepted directly, without a dedicated handler type.
1280        let selector =
1281            TensorSelector::layer_output(1, 4, 2, TensorAccess::ReadWriteF32, false).unwrap();
1282        let transactions =
1283            TensorTransactions::new(vec![selector], |mut txn: TensorTransaction<'_>| {
1284                if let TensorDataMut::F32(values) = &mut txn.data {
1285                    for value in values.iter_mut() {
1286                        *value *= 2.0;
1287                    }
1288                }
1289                Ok(TensorWriteback::Commit)
1290            });
1291        assert!(transactions.is_ok());
1292    }
1293
1294    #[test]
1295    fn all_finite_detects_non_finite() {
1296        assert!(all_finite(&[0.0, 1.0, -1.0, f32::MAX, f32::MIN, -0.0]));
1297        assert!(all_finite(&[]));
1298        assert!(!all_finite(&[1.0, f32::INFINITY]));
1299        assert!(!all_finite(&[f32::NEG_INFINITY]));
1300        assert!(!all_finite(&[f32::NAN]));
1301    }
1302
1303    #[test]
1304    fn finite_validation_policy() {
1305        assert!(TensorFiniteValidation::Strict.checks_input());
1306        assert!(TensorFiniteValidation::Strict.checks_output());
1307        assert!(!TensorFiniteValidation::OutputOnly.checks_input());
1308        assert!(TensorFiniteValidation::OutputOnly.checks_output());
1309        assert!(!TensorFiniteValidation::Trusted.checks_input());
1310        assert!(!TensorFiniteValidation::Trusted.checks_output());
1311
1312        let selector = TensorSelector::layer_output(1, 4, 2, TensorAccess::ReadOnly, true)
1313            .unwrap()
1314            .with_finite_validation(TensorFiniteValidation::Trusted);
1315        assert_eq!(
1316            selector.finite_validation(),
1317            TensorFiniteValidation::Trusted
1318        );
1319    }
1320}