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