Skip to main content

tract_libcli/
tensor.rs

1use std::collections::HashSet;
2use std::io::{Read, Seek};
3use std::ops::Range;
4use std::str::FromStr;
5use std::sync::Mutex;
6
7use crate::model::Model;
8use tract_hir::internal::*;
9use tract_num_traits::Zero;
10
11#[cfg(feature = "transformers")]
12use tract_transformers::figure_out_causal_llm_b_s_p;
13
14#[derive(Debug, Default, Clone)]
15pub struct TensorsValues(pub Vec<TensorValues>);
16
17impl TensorsValues {
18    pub fn by_name(&self, name: &str) -> Option<&TensorValues> {
19        self.0.iter().find(|t| t.name.as_deref() == Some(name))
20    }
21    pub fn by_name_mut(&mut self, name: &str) -> Option<&mut TensorValues> {
22        self.0.iter_mut().find(|t| t.name.as_deref() == Some(name))
23    }
24    pub fn by_name_mut_with_default(&mut self, name: &str) -> &mut TensorValues {
25        if self.by_name_mut(name).is_none() {
26            self.add(TensorValues { name: Some(name.to_string()), ..TensorValues::default() });
27        }
28        self.by_name_mut(name).unwrap()
29    }
30
31    pub fn by_input_ix(&self, ix: usize) -> Option<&TensorValues> {
32        self.0.iter().find(|t| t.input_index == Some(ix))
33    }
34    pub fn by_input_ix_mut(&mut self, ix: usize) -> Option<&mut TensorValues> {
35        self.0.iter_mut().find(|t| t.input_index == Some(ix))
36    }
37    pub fn by_input_ix_mut_with_default(&mut self, ix: usize) -> &mut TensorValues {
38        if self.by_input_ix_mut(ix).is_none() {
39            self.add(TensorValues { input_index: Some(ix), ..TensorValues::default() });
40        }
41        self.by_input_ix_mut(ix).unwrap()
42    }
43
44    pub fn add(&mut self, other: TensorValues) {
45        let mut tensor = other.input_index.and_then(|ix| self.by_input_ix_mut(ix));
46
47        if tensor.is_none() {
48            tensor = other.name.as_deref().and_then(|ix| self.by_name_mut(ix))
49        }
50
51        if let Some(tensor) = tensor {
52            if tensor.fact.is_none() {
53                tensor.fact = other.fact;
54            }
55            if tensor.values.is_none() {
56                tensor.values = other.values;
57            }
58        } else {
59            self.0.push(other.clone());
60        };
61    }
62
63    pub fn input_by_name(&self, name: &str) -> Option<&TensorValues> {
64        self.0
65            .iter()
66            .filter(|tv| tv.output_index.is_none() && !tv.only_output)
67            .find(|t| t.name.as_deref() == Some(name))
68    }
69}
70
71#[derive(Debug, PartialEq, Clone, Default)]
72pub struct TensorValues {
73    pub input_index: Option<usize>,
74    pub output_index: Option<usize>,
75    pub name: Option<String>,
76    pub fact: Option<InferenceFact>,
77    pub values: Option<Vec<TValue>>,
78    pub random_range: Option<Range<f32>>,
79    pub only_input: bool,
80    pub only_output: bool,
81}
82
83fn parse_values<T: Datum + FromStr>(shape: &[usize], it: Vec<&str>) -> TractResult<Tensor> {
84    let values = it
85        .into_iter()
86        .map(|v| v.parse::<T>().map_err(|_| format_err!("Failed to parse {}", v)))
87        .collect::<TractResult<Vec<T>>>()?;
88    Ok(tract_ndarray::Array::from_shape_vec(shape, values)?.into())
89}
90
91fn tensor_for_text_data(
92    symbol_table: &SymbolScope,
93    _filename: &str,
94    mut reader: impl Read,
95) -> TractResult<Tensor> {
96    let mut data = String::new();
97    reader.read_to_string(&mut data)?;
98
99    let mut lines = data.lines();
100    let proto = InferenceFact::from_spec(symbol_table, lines.next().context("Empty data file")?)?;
101    let shape = proto.shape.concretize().unwrap();
102
103    let values = lines.flat_map(|l| l.split_whitespace()).collect::<Vec<&str>>();
104
105    // We know there is at most one streaming dimension, so we can deduce the
106    // missing value with a simple division.
107    let product: usize = shape.iter().map(|o| o.to_usize().unwrap_or(1)).product();
108    let missing = values.len() / product;
109
110    let shape: Vec<_> = shape.iter().map(|d| d.to_usize().unwrap_or(missing)).collect();
111    dispatch_numbers!(parse_values(proto.datum_type.concretize().unwrap())(&*shape, values))
112}
113
114/// Parses the `data` command-line argument.
115pub fn for_data(
116    symbol_table: &SymbolScope,
117    filename: &str,
118    reader: impl Read + std::io::Seek,
119) -> TractResult<(Option<String>, InferenceFact)> {
120    #[allow(unused_imports)]
121    use std::convert::TryFrom;
122    if filename.ends_with(".pb") {
123        #[cfg(feature = "onnx")]
124        {
125            use tract_onnx::data_resolver::FopenDataResolver;
126            use tract_onnx::tensor::load_tensor;
127            let proto = ::tract_onnx::tensor::proto_from_reader(reader)?;
128            let tensor = load_tensor(&FopenDataResolver, &proto, None)?;
129            Ok((Some(proto.name.to_string()).filter(|s| !s.is_empty()), tensor.into()))
130        }
131        #[cfg(not(feature = "onnx"))]
132        {
133            panic!("Loading tensor from protobuf requires onnx features");
134        }
135    } else if filename.contains(".npz:") {
136        let mut tokens = filename.split(':');
137        let (_filename, inner) = (tokens.next().unwrap(), tokens.next().unwrap());
138        let mut npz = ndarray_npy::NpzReader::new(reader)?;
139        Ok((None, for_npz(&mut npz, inner)?.into()))
140    } else {
141        Ok((None, tensor_for_text_data(symbol_table, filename, reader)?.into()))
142    }
143}
144
145pub fn for_npz(
146    npz: &mut ndarray_npy::NpzReader<impl Read + Seek>,
147    name: &str,
148) -> TractResult<Tensor> {
149    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<f32>, tract_ndarray::IxDyn>(name) {
150        return Ok(t.into_tensor());
151    }
152    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<f64>, tract_ndarray::IxDyn>(name) {
153        return Ok(t.into_tensor());
154    }
155    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<i8>, tract_ndarray::IxDyn>(name) {
156        return Ok(t.into_tensor());
157    }
158    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<i16>, tract_ndarray::IxDyn>(name) {
159        return Ok(t.into_tensor());
160    }
161    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<i32>, tract_ndarray::IxDyn>(name) {
162        return Ok(t.into_tensor());
163    }
164    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<i64>, tract_ndarray::IxDyn>(name) {
165        return Ok(t.into_tensor());
166    }
167    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<u8>, tract_ndarray::IxDyn>(name) {
168        return Ok(t.into_tensor());
169    }
170    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<u16>, tract_ndarray::IxDyn>(name) {
171        return Ok(t.into_tensor());
172    }
173    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<u32>, tract_ndarray::IxDyn>(name) {
174        return Ok(t.into_tensor());
175    }
176    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<u64>, tract_ndarray::IxDyn>(name) {
177        return Ok(t.into_tensor());
178    }
179    if let Ok(t) = npz.by_name::<tract_ndarray::OwnedRepr<bool>, tract_ndarray::IxDyn>(name) {
180        return Ok(t.into_tensor());
181    }
182    bail!("Can not extract tensor from {}", name);
183}
184
185pub fn for_string(
186    symbol_table: &SymbolScope,
187    value: &str,
188) -> TractResult<(Option<String>, InferenceFact)> {
189    if let Some(path) = value.strip_prefix('@') {
190        bail!(
191            "Tensor specs do not read files (got @{path}): use --input-facts-from-bundle for shapes and types, or run --input-from-npz for values"
192        );
193    }
194    let (name, value) = if value.contains(':') {
195        let mut splits = value.split(':');
196        (Some(splits.next().unwrap().to_string()), splits.next().unwrap())
197    } else {
198        (None, value)
199    };
200    if value.contains('=') {
201        let mut split = value.split('=');
202        let spec = InferenceFact::from_spec(symbol_table, split.next().unwrap())?;
203        let value = split.next().unwrap().split(',');
204        let dt =
205            spec.datum_type.concretize().context("Must specify type when giving tensor value")?;
206        let shape = spec
207            .shape
208            .as_concrete_finite()?
209            .context("Must specify concrete shape when giving tensor value")?;
210        let tensor = if dt == TDim::datum_type() {
211            let mut tensor = Tensor::zero::<TDim>(&shape)?;
212            let values =
213                value.map(|v| parse_tdim(symbol_table, v)).collect::<TractResult<Vec<_>>>()?;
214            tensor
215                .try_as_plain_ram_mut()?
216                .as_slice_mut::<TDim>()?
217                .iter_mut()
218                .zip(values)
219                .for_each(|(t, v)| *t = v);
220            tensor
221        } else {
222            dispatch_numbers!(parse_values(dt)(&*shape, value.collect()))?
223        };
224        Ok((name, tensor.into()))
225    } else {
226        Ok((name, InferenceFact::from_spec(symbol_table, value)?))
227    }
228}
229
230lazy_static::lazy_static! {
231    static ref MESSAGE_ONCE: Mutex<HashSet<String>> = Mutex::new(HashSet::new());
232}
233
234fn info_once(msg: String) {
235    if MESSAGE_ONCE.lock().unwrap().insert(msg.clone()) {
236        info!("{msg}");
237    }
238}
239
240pub struct RunParams {
241    pub tensors_values: TensorsValues,
242    pub allow_random_input: bool,
243    pub allow_float_casts: bool,
244    pub symbols: SymbolValues,
245    pub prompt_chunk_size: Option<usize>,
246    pub drop_partial_pulse: bool,
247}
248
249pub struct RunTensors {
250    pub sources: Vec<TVec<TValue>>,
251    /// In pulse mode, the *real* input length on the streaming axis
252    /// (i.e. before the trailing turns of zero-padding that
253    /// `get_or_make_tensors` adds to flush the pipeline).  Used by the
254    /// runner to bind the streaming symbol so PulsePad and friends
255    /// resolve `end_input` correctly at end-of-stream.  `None` for
256    /// non-pulse runs.
257    pub streaming_input_len: Option<usize>,
258}
259
260#[cfg(not(feature = "transformers"))]
261fn chunk_fact(
262    fact: &TypedFact,
263    _params: &RunParams,
264    _model: &Arc<dyn Model>,
265) -> TractResult<Vec<TypedFact>> {
266    Ok(vec![fact.clone()])
267}
268
269#[cfg(not(feature = "transformers"))]
270fn chunk_tensor(
271    tensor: Tensor,
272    _fact: &TypedFact,
273    _params: &RunParams,
274    _model: &Arc<dyn Model>,
275) -> TractResult<Vec<TValue>> {
276    Ok(vec![tensor.into_tvalue()])
277}
278
279#[cfg(feature = "transformers")]
280fn chunk_fact(
281    fact: &TypedFact,
282    params: &RunParams,
283    model: &Arc<dyn Model>,
284) -> TractResult<Vec<TypedFact>> {
285    let Some(chunk_size) = params.prompt_chunk_size else {
286        return Ok(vec![fact.clone()]);
287    };
288    let Some(model) = model.downcast_ref::<TypedModel>() else {
289        return Ok(vec![fact.clone()]);
290    };
291    let (_, s, _) = figure_out_causal_llm_b_s_p(model)?;
292    let Some(s) = s else {
293        return Ok(vec![fact.clone()]);
294    };
295
296    let dims = fact.shape.dims();
297    let Some(sym_idx) = dims.iter().position(|d| *d == TDim::Sym(s.clone())) else {
298        return Ok(vec![fact.clone()]);
299    };
300
301    let resolved_sym = dims[sym_idx].eval_to_i64(&params.symbols)? as usize;
302    if resolved_sym <= chunk_size {
303        return Ok(vec![fact.clone()]);
304    }
305
306    let num_chunks = resolved_sym.div_ceil(chunk_size);
307    let mut out = Vec::with_capacity(num_chunks);
308
309    for start in (0..resolved_sym).step_by(chunk_size) {
310        let this = chunk_size.min(resolved_sym - start) as i64;
311
312        let mut new_fact = fact.clone();
313        new_fact.shape = new_fact
314            .shape
315            .iter()
316            .enumerate()
317            .map(|(i, d)| if i == sym_idx { TDim::Val(this) } else { d.eval(&params.symbols) })
318            .collect();
319
320        out.push(new_fact);
321    }
322
323    Ok(out)
324}
325
326#[cfg(feature = "transformers")]
327fn chunk_tensor(
328    tensor: Tensor,
329    fact: &TypedFact,
330    params: &RunParams,
331    model: &Arc<dyn Model>,
332) -> TractResult<Vec<TValue>> {
333    let Some(chunk_size) = params.prompt_chunk_size else {
334        return Ok(vec![tensor.into_tvalue()]);
335    };
336
337    let Some(model) = model.downcast_ref::<TypedModel>() else {
338        return Ok(vec![tensor.into_tvalue()]);
339    };
340    let (_, s, _) = figure_out_causal_llm_b_s_p(model)?;
341    let Some(s) = s else {
342        return Ok(vec![tensor.into_tvalue()]);
343    };
344
345    let dims = fact.shape.dims();
346    let Some(symb_axis) = dims.iter().position(|d| *d == TDim::Sym(s.clone())) else {
347        return Ok(vec![tensor.into_tvalue()]);
348    };
349
350    let resolved_sym = tensor.shape()[symb_axis];
351    if resolved_sym <= chunk_size {
352        return Ok(vec![tensor.into_tvalue()]);
353    }
354
355    let num_chunks = resolved_sym.div_ceil(chunk_size);
356    let mut out = Vec::with_capacity(num_chunks);
357
358    for start in (0..resolved_sym).step_by(chunk_size) {
359        let this = chunk_size.min(resolved_sym - start);
360        out.push(tensor.slice(symb_axis, start, start + this)?.into_tvalue());
361    }
362
363    Ok(out)
364}
365
366fn get_or_make_tensors(
367    model: &Arc<dyn Model>,
368    params: &RunParams,
369    fact: TypedFact,
370    name: &str,
371    input_idx: usize,
372    target: &mut TVec<Vec<TValue>>,
373    streaming_input_len: &mut Option<usize>,
374) -> TractResult<()> {
375    // What the caller feeds is not what the model runs: an autobatched model keeps its
376    // batch axis symbolic while every stream feeds one row, so the shapes here
377    // come from `--set` rather than from the facts.
378    let mut fact = fact;
379    fact.shape = fact.shape.iter().map(|dim| dim.eval(&params.symbols)).collect();
380    if let Some(mut value) = params
381        .tensors_values
382        .by_name(name)
383        .or_else(|| params.tensors_values.by_input_ix(input_idx))
384        .and_then(|t| t.values.clone())
385    {
386        if !value[0].datum_type().is_quantized()
387            && fact.datum_type.is_quantized()
388            && value[0].datum_type() == fact.datum_type.unquantized()
389        {
390            value = value
391                .iter()
392                .map(|v| {
393                    let mut v = v.clone().into_tensor();
394                    unsafe { v.set_datum_type(fact.datum_type) };
395                    v.into()
396                })
397                .collect();
398        }
399        let mut chunked_tensors: Vec<TValue> = vec![];
400        for t in &value {
401            let tensor = if TypedFact::shape_and_dt_of(&value[0]).compatible_with(&fact) {
402                info_once(format!(
403                    "Using fixed input for input called {} ({} turn(s))",
404                    name,
405                    value.len()
406                ));
407                t.clone().into_tensor()
408            } else if fact.datum_type == f16::datum_type()
409                && value[0].datum_type() == f32::datum_type()
410                && params.allow_float_casts
411            {
412                debug!("Casting input to F16 for input called {} ({} turn(s))", name, value.len());
413                t.cast_to::<f16>()?.into_owned()
414            } else {
415                break;
416            };
417
418            chunked_tensors.extend(chunk_tensor(tensor, &fact, params, model)?);
419        }
420        if !chunked_tensors.is_empty() {
421            target.push(chunked_tensors);
422            return Ok(());
423        }
424
425        if value.len() == 1 && model.properties().contains_key("pulse.delay") {
426            let value = &value[0];
427            let input_pulse_axis = model
428                .properties()
429                .get("pulse.input_axes")
430                .context("Expect pulse.input_axes property")?
431                .cast_to::<i64>()?
432                .try_as_plain_ram()?
433                .as_slice::<i64>()?[input_idx] as usize;
434            let input_pulse = fact.shape.get(input_pulse_axis).unwrap().to_usize().unwrap();
435            let mut input_len = value.shape()[input_pulse_axis];
436            if params.drop_partial_pulse && input_len % input_pulse != 0 {
437                input_len = (input_len / input_pulse) * input_pulse;
438                info!(
439                    "Dropping partial trailing pulse: truncating input from {} to {} on axis {}.",
440                    value.shape()[input_pulse_axis],
441                    input_len,
442                    input_pulse_axis
443                );
444            }
445            // Record the real streaming-axis length on the *first* input we
446            // see; downstream uses it to bind the streaming symbol so that
447            // PulsePad's `end_input` resolves to the correct end-of-stream.
448            // (All inputs share the same streaming dim, so picking the first
449            // is fine.)
450            if streaming_input_len.is_none() {
451                *streaming_input_len = Some(input_len);
452            }
453
454            // how many pulses do we need to push full result out ?
455            // guess by looking at len and delay of the first output
456            let output_pulse_axis = model
457                .properties()
458                .get("pulse.output_axes")
459                .context("Expect pulse.output_axes property")?
460                .cast_to::<i64>()?
461                .try_as_plain_ram()?
462                .as_slice::<i64>()?[0] as usize;
463            let output_fact = model.outlet_typedfact(model.output_outlets()[0])?;
464            let output_pulse =
465                output_fact.shape.get(output_pulse_axis).unwrap().to_usize().unwrap();
466            let output_len = input_len * output_pulse / input_pulse;
467            let output_delay = model.properties()["pulse.delay"]
468                .try_as_plain_ram()?
469                .as_slice::<i64>()?[0] as usize;
470            let last_frame = output_len + output_delay;
471            let needed_pulses = last_frame.divceil(output_pulse);
472            let mut values = vec![];
473            for ix in 0..needed_pulses {
474                let shape = fact.shape.as_concrete().with_context(|| {
475                    format!("Input {name} pulses into {fact:?}, bind its free symbols with --set")
476                })?;
477                let mut t = Tensor::zero_dt(fact.datum_type, shape)?;
478                let start = ix * input_pulse;
479                let end = (start + input_pulse).min(input_len);
480                if end > start {
481                    t.assign_slice(0..end - start, value, start..end, input_pulse_axis)?;
482                }
483                values.push(t.into());
484            }
485            info!(
486                "Generated {} pulses of shape {:?} for input {}.",
487                needed_pulses, fact.shape, input_idx
488            );
489            target.push(values);
490        } else {
491            bail!(
492                "For input {}, can not reconcile model input fact {:?} with provided input {:?}",
493                name,
494                fact,
495                value[0]
496            );
497        };
498    } else if fact.shape.is_concrete() && fact.shape.volume() == TDim::zero() {
499        let shape = fact.shape.as_concrete().unwrap();
500        let tensor = Tensor::zero_dt(fact.datum_type, shape)?;
501        target.push(vec![tensor.into()]);
502    } else if params.allow_random_input {
503        info_once(format!("Using random input for input called {name:?}: {fact:?}"));
504        let tv = params
505            .tensors_values
506            .by_name(name)
507            .or_else(|| params.tensors_values.by_input_ix(input_idx));
508
509        let mut chunked_facts = chunk_fact(&fact, params, model)?;
510
511        let mut chunked_tensors = Vec::with_capacity(chunked_facts.len());
512        for fact in &mut chunked_facts {
513            chunked_tensors.push(tensor_for_fact(fact, None, tv)?.into());
514        }
515        target.push(chunked_tensors);
516    } else {
517        bail!(
518            "Unmatched tensor {}. Fix the input or use \"--allow-random-input\" if this was intended",
519            name
520        );
521    }
522    Ok(())
523}
524
525pub fn get_or_make_inputs(tract: &Arc<dyn Model>, params: &RunParams) -> TractResult<RunTensors> {
526    // Resolve source inputs
527    let mut tmp_inputs = tvec![];
528    let mut streaming_input_len = None;
529    for (ix, input) in tract.input_outlets().iter().enumerate() {
530        let fact = tract.outlet_typedfact(*input)?;
531        let name = tract.node_name(input.node);
532        get_or_make_tensors(
533            tract,
534            params,
535            fact,
536            name,
537            ix,
538            &mut tmp_inputs,
539            &mut streaming_input_len,
540        )?;
541    }
542
543    let n_turns = tmp_inputs.iter().map(|t| t.len()).max().unwrap_or(0);
544    let sources = (0..n_turns)
545        .map(|i| {
546            tmp_inputs
547                .iter()
548                .map(|t| if i < t.len() { t[i].clone() } else { t[t.len() - 1].clone() })
549                .collect::<TVec<_>>()
550        })
551        .collect::<Vec<_>>();
552
553    Ok(RunTensors { sources, streaming_input_len })
554}
555
556fn make_inputs(values: &[impl std::borrow::Borrow<TypedFact>]) -> TractResult<TVec<TValue>> {
557    values.iter().map(|v| tensor_for_fact(v.borrow(), None, None).map(|t| t.into())).collect()
558}
559
560pub fn make_inputs_for_model(model: &dyn Model) -> TractResult<TVec<TValue>> {
561    make_inputs(
562        &model
563            .input_outlets()
564            .iter()
565            .map(|&t| model.outlet_typedfact(t))
566            .collect::<TractResult<Vec<TypedFact>>>()?,
567    )
568}
569
570#[allow(unused_variables)]
571pub fn tensor_for_fact(
572    fact: &TypedFact,
573    streaming_dim: Option<usize>,
574    tv: Option<&TensorValues>,
575) -> TractResult<Tensor> {
576    if let Some(value) = &fact.konst {
577        return Ok(value.clone().into_tensor());
578    }
579    Ok(random(
580        fact.shape
581            .as_concrete()
582            .with_context(|| format!("Expected concrete shape, found: {fact:?}"))?,
583        fact.datum_type,
584        tv,
585    ))
586}
587
588/// Generates a random tensor of a given size and type.
589pub fn random(sizes: &[usize], datum_type: DatumType, tv: Option<&TensorValues>) -> Tensor {
590    use rand::{RngExt, SeedableRng};
591    let mut rng = rand::rngs::StdRng::seed_from_u64(21242);
592    let mut tensor = Tensor::zero::<f32>(sizes).unwrap();
593    let mut tensor_plain = tensor.try_as_plain_ram_mut().unwrap();
594    let slice = tensor_plain.as_slice_mut::<f32>().unwrap();
595    if let Some(range) = tv.and_then(|tv| tv.random_range.as_ref()) {
596        slice.iter_mut().for_each(|x| *x = rng.random_range(range.clone()))
597    } else {
598        slice.iter_mut().for_each(|x| *x = rng.random())
599    };
600    tensor.cast_to_dt(datum_type).unwrap().into_owned()
601}
602
603#[cfg(test)]
604mod tests {
605    use super::*;
606
607    #[test]
608    fn named_spec_with_value() {
609        let (name, fact) = for_string(&SymbolScope::default(), "x:2,f32=1,2").unwrap();
610        assert_eq!(name.as_deref(), Some("x"));
611        assert_eq!(fact.value.concretize().unwrap().into_tensor(), tensor1(&[1f32, 2.]));
612    }
613
614    #[test]
615    fn file_spec_is_rejected() {
616        let err = for_string(&SymbolScope::default(), "@inputs.npz:x.npy").unwrap_err();
617        assert!(err.to_string().contains("do not read files"), "{err}");
618    }
619}