From 22689cfdb6c9e571f5a208d164ddd18c638d24f8 Mon Sep 17 00:00:00 2001 From: dante <45801863+alexander-camuto@users.noreply.github.com> Date: Sat, 6 Jan 2024 17:31:13 +0000 Subject: [PATCH] feat: numerical accuracy reports post calibration (#647) --- Cargo.lock | 14 ++-- Cargo.toml | 2 +- src/execute.rs | 152 +++++++++++++++++++++++++++++++++++++++-- src/graph/input.rs | 41 ++++++++++- src/graph/mod.rs | 19 +++++- src/graph/model.rs | 140 +++++++++++++++++++++++++++++++------ src/graph/utilities.rs | 16 ++--- 7 files changed, 338 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 798421d1c..1599cd127 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5338,7 +5338,7 @@ dependencies = [ [[package]] name = "tract-core" version = "0.20.23-pre" -source = "git+https://github.com/sonos/tract/?rev=ee98004a2d8d7851da7b9fce954b2a7a7181eccb#ee98004a2d8d7851da7b9fce954b2a7a7181eccb" +source = "git+https://github.com/sonos/tract/?rev=4ee813d#4ee813dd327d3df3e5c958cab9903fb332c9ceca" dependencies = [ "anyhow", "bit-set", @@ -5362,7 +5362,7 @@ dependencies = [ [[package]] name = "tract-data" version = "0.20.23-pre" -source = "git+https://github.com/sonos/tract/?rev=ee98004a2d8d7851da7b9fce954b2a7a7181eccb#ee98004a2d8d7851da7b9fce954b2a7a7181eccb" +source = "git+https://github.com/sonos/tract/?rev=4ee813d#4ee813dd327d3df3e5c958cab9903fb332c9ceca" dependencies = [ "anyhow", "half 2.2.1", @@ -5381,7 +5381,7 @@ dependencies = [ [[package]] name = "tract-hir" version = "0.20.23-pre" -source = "git+https://github.com/sonos/tract/?rev=ee98004a2d8d7851da7b9fce954b2a7a7181eccb#ee98004a2d8d7851da7b9fce954b2a7a7181eccb" +source = "git+https://github.com/sonos/tract/?rev=4ee813d#4ee813dd327d3df3e5c958cab9903fb332c9ceca" dependencies = [ "derive-new", "log", @@ -5391,7 +5391,7 @@ dependencies = [ [[package]] name = "tract-linalg" version = "0.20.23-pre" -source = "git+https://github.com/sonos/tract/?rev=ee98004a2d8d7851da7b9fce954b2a7a7181eccb#ee98004a2d8d7851da7b9fce954b2a7a7181eccb" +source = "git+https://github.com/sonos/tract/?rev=4ee813d#4ee813dd327d3df3e5c958cab9903fb332c9ceca" dependencies = [ "cc", "derive-new", @@ -5415,7 +5415,7 @@ dependencies = [ [[package]] name = "tract-nnef" version = "0.20.23-pre" -source = "git+https://github.com/sonos/tract/?rev=ee98004a2d8d7851da7b9fce954b2a7a7181eccb#ee98004a2d8d7851da7b9fce954b2a7a7181eccb" +source = "git+https://github.com/sonos/tract/?rev=4ee813d#4ee813dd327d3df3e5c958cab9903fb332c9ceca" dependencies = [ "byteorder", "flate2", @@ -5429,7 +5429,7 @@ dependencies = [ [[package]] name = "tract-onnx" version = "0.20.23-pre" -source = "git+https://github.com/sonos/tract/?rev=ee98004a2d8d7851da7b9fce954b2a7a7181eccb#ee98004a2d8d7851da7b9fce954b2a7a7181eccb" +source = "git+https://github.com/sonos/tract/?rev=4ee813d#4ee813dd327d3df3e5c958cab9903fb332c9ceca" dependencies = [ "bytes", "derive-new", @@ -5446,7 +5446,7 @@ dependencies = [ [[package]] name = "tract-onnx-opl" version = "0.20.23-pre" -source = "git+https://github.com/sonos/tract/?rev=ee98004a2d8d7851da7b9fce954b2a7a7181eccb#ee98004a2d8d7851da7b9fce954b2a7a7181eccb" +source = "git+https://github.com/sonos/tract/?rev=4ee813d#4ee813dd327d3df3e5c958cab9903fb332c9ceca" dependencies = [ "getrandom", "log", diff --git a/Cargo.toml b/Cargo.toml index 139bdf7fb..88d5d8e73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ tokio-util = { version = "0.7.9", features = ["codec"] } pyo3 = { version = "0.18.3", features = ["extension-module", "abi3-py37", "macros"], default_features = false, optional = true } pyo3-asyncio = { version = "0.18.0", features = ["attributes", "tokio-runtime"], default_features = false, optional = true } pyo3-log = { version = "0.8.1", default_features = false, optional = true } -tract-onnx = { git = "https://github.com/sonos/tract/", rev= "ee98004a2d8d7851da7b9fce954b2a7a7181eccb", default_features = false, optional = true } +tract-onnx = { git = "https://github.com/sonos/tract/", rev= "4ee813d", default_features = false, optional = true } tabled = { version = "0.12.0", optional = true } diff --git a/src/execute.rs b/src/execute.rs index 9aa65d8a4..25652f521 100644 --- a/src/execute.rs +++ b/src/execute.rs @@ -19,6 +19,7 @@ use crate::pfsys::{ }; use crate::pfsys::{create_proof_circuit_kzg, verify_proof_circuit_kzg}; use crate::pfsys::{save_vk, srs::*}; +use crate::tensor::TensorError; use crate::RunArgs; #[cfg(not(target_arch = "wasm32"))] use ethers::types::H160; @@ -64,6 +65,7 @@ use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use std::sync::OnceLock; #[cfg(not(target_arch = "wasm32"))] use std::time::Duration; +use tabled::Tabled; use thiserror::Error; #[cfg(not(target_arch = "wasm32"))] @@ -625,6 +627,99 @@ pub(crate) fn init_bar(len: u64) -> ProgressBar { #[cfg(not(target_arch = "wasm32"))] use colored_json::ToColoredJson; +#[derive(Debug, Clone, Tabled)] +/// Accuracy tearsheet +pub struct AccuracyResults { + mean_error: f32, + median_error: f32, + max_error: f32, + min_error: f32, + mean_abs_error: f32, + median_abs_error: f32, + max_abs_error: f32, + min_abs_error: f32, + mean_squared_error: f32, + mean_percent_error: f32, + mean_abs_percent_error: f32, +} + +impl AccuracyResults { + /// Create a new accuracy results struct + pub fn new( + mut original_preds: Vec>, + mut calibrated_preds: Vec>, + ) -> Result> { + let mut errors = vec![]; + let mut abs_errors = vec![]; + let mut squared_errors = vec![]; + let mut percentage_errors = vec![]; + let mut abs_percentage_errors = vec![]; + + for (original, calibrated) in original_preds.iter_mut().zip(calibrated_preds.iter_mut()) { + original.flatten(); + calibrated.flatten(); + let error = (original.clone() - calibrated.clone())?; + let abs_error = error.map(|x| x.abs()); + let squared_error = error.map(|x| x.powi(2)); + let percentage_error = + error.enum_map(|i, x| Ok::<_, TensorError>(x / original[i].clone()))?; + let abs_percentage_error = percentage_error.map(|x| x.abs()); + + errors.extend(error.into_iter()); + abs_errors.extend(abs_error.into_iter()); + squared_errors.extend(squared_error.into_iter()); + percentage_errors.extend(percentage_error.into_iter()); + abs_percentage_errors.extend(abs_percentage_error.into_iter()); + } + + let mean_percent_error = + percentage_errors.iter().sum::() / percentage_errors.len() as f32; + let mean_abs_percent_error = + abs_percentage_errors.iter().sum::() / abs_percentage_errors.len() as f32; + let mean_error = errors.iter().sum::() / errors.len() as f32; + let median_error = errors[errors.len() / 2]; + let max_error = errors + .iter() + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap() + .clone(); + let min_error = errors + .iter() + .min_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap() + .clone(); + + let mean_abs_error = abs_errors.iter().sum::() / abs_errors.len() as f32; + let median_abs_error = abs_errors[abs_errors.len() / 2]; + let max_abs_error = abs_errors + .iter() + .max_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap() + .clone(); + let min_abs_error = abs_errors + .iter() + .min_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap() + .clone(); + + let mean_squared_error = squared_errors.iter().sum::() / squared_errors.len() as f32; + + Ok(Self { + mean_error, + median_error, + max_error, + min_error, + mean_abs_error, + median_abs_error, + max_abs_error, + min_abs_error, + mean_squared_error, + mean_percent_error, + mean_abs_percent_error, + }) + } +} + /// Calibrate the circuit parameters to a given a dataset #[cfg(not(target_arch = "wasm32"))] #[allow(trivial_casts)] @@ -637,6 +732,9 @@ pub(crate) fn calibrate( scales: Option>, max_logrows: Option, ) -> Result> { + use std::collections::HashMap; + use tabled::Table; + let data = GraphData::from_path(data)?; // load the pre-generated settings let settings = GraphSettings::load(&settings_path)?; @@ -655,6 +753,17 @@ pub(crate) fn calibrate( #[cfg(unix)] std::mem::drop(_r); + let chunks = data.split_into_batches(model.graph.input_shapes()?)?; + info!("num of calibration batches: {}", chunks.len()); + + info!("running onnx predictions..."); + let original_predictions = Model::run_onnx_predictions( + &settings.run_args, + &model_path, + &chunks, + model.graph.input_shapes()?, + )?; + let range = if let Some(scales) = scales { scales } else { @@ -664,10 +773,6 @@ pub(crate) fn calibrate( } }; - let chunks = data.split_into_batches(model.graph.input_shapes()?)?; - - info!("num of calibration batches: {}", chunks.len()); - let mut found_params: Vec = vec![]; let scale_rebase_multiplier = [1, 2, 10]; @@ -707,6 +812,8 @@ pub(crate) fn calibrate( .map(|(a, b)| (*a, *b)) .collect::>(); + let mut forward_pass_res = HashMap::new(); + let pb = init_bar(range_grid.len() as u64); pb.set_message("calibrating..."); @@ -729,6 +836,9 @@ pub(crate) fn calibrate( Err(_) => None, }; + let key = (input_scale, param_scale, scale_rebase_multiplier); + forward_pass_res.insert(key, vec![]); + let tasks = chunks .iter() .zip(run_args_iterable) @@ -757,10 +867,16 @@ pub(crate) fn calibrate( .load_graph_from_file_exclusively(&chunk) .map_err(|e| format!("failed to load circuit inputs: {}", e))?; - circuit + let forward_res = circuit .calibrate(&data, max_logrows, lookup_safety_margin) .map_err(|e| format!("failed to calibrate: {}", e))?; + // push result to the hashmap + forward_pass_res + .get_mut(&key) + .ok_or("key not found")? + .push(forward_res); + let settings = circuit.settings().clone(); let found_run_args = RunArgs { @@ -899,6 +1015,32 @@ pub(crate) fn calibrate( } }; + let outputs = forward_pass_res + .get(&( + best_params.run_args.input_scale, + best_params.run_args.param_scale, + best_params.run_args.scale_rebase_multiplier, + )) + .ok_or("no params found")? + .iter() + .map(|x| x.get_float_outputs(&best_params.model_output_scales)) + .collect::>(); + + let accuracy_res = AccuracyResults::new( + original_predictions.into_iter().flatten().collect(), + outputs.into_iter().flatten().collect(), + )?; + + let tear_sheet_table = Table::new(vec![accuracy_res]); + + println!( + "\n\n <------------- Numerical Fidelity Report (input_scale: {}, param_scale: {}, scale_input_multiplier: {}) ------------->\n\n{}\n\n", + best_params.run_args.input_scale, + best_params.run_args.param_scale, + best_params.run_args.scale_rebase_multiplier, + tear_sheet_table.to_string().as_str() + ); + if matches!(target, CalibrationTarget::Resources { col_overflow: true }) { let lookup_log_rows = ((best_params.run_args.lookup_range.1 - best_params.run_args.lookup_range.0) as f32) diff --git a/src/graph/input.rs b/src/graph/input.rs index 159a374f0..fc145cddf 100644 --- a/src/graph/input.rs +++ b/src/graph/input.rs @@ -1,3 +1,5 @@ +use super::quantize_float; +use super::GraphError; use crate::circuit::InputType; use crate::fieldutils::i128_to_felt; #[cfg(not(target_arch = "wasm32"))] @@ -18,11 +20,13 @@ use std::panic::UnwindSafe; #[cfg(not(target_arch = "wasm32"))] use std::thread; #[cfg(not(target_arch = "wasm32"))] +use tract_onnx::tract_core::{ + tract_data::{prelude::Tensor as TractTensor, TVec}, + value::TValue, +}; +#[cfg(not(target_arch = "wasm32"))] use tract_onnx::tract_hir::tract_num_traits::ToPrimitive; -use super::quantize_float; -use super::GraphError; - type Decimals = u8; type Call = String; type RPCUrl = String; @@ -445,6 +449,37 @@ pub struct GraphData { impl UnwindSafe for GraphData {} impl GraphData { + // not wasm + #[cfg(not(target_arch = "wasm32"))] + /// Convert the input data to tract data + pub fn to_tract_data( + &self, + shapes: &[Vec], + datum_types: &[tract_onnx::prelude::DatumType], + ) -> Result, Box> { + let mut inputs = TVec::new(); + match &self.input_data { + DataSource::File(data) => { + for (i, input) in data.iter().enumerate() { + if !input.is_empty() { + let dt = datum_types[i]; + let input = input.iter().map(|e| e.to_float()).collect::>(); + let tt = TractTensor::from_shape(&shapes[i], &input)?; + let tt = tt.cast_to_dt(dt)?; + inputs.push(tt.into_owned().into()); + } + } + } + _ => { + return Err(Box::new(GraphError::InvalidDims( + 0, + "non file data cannot be split into batches".to_string(), + ))) + } + } + Ok(inputs) + } + /// pub fn new(input_data: DataSource) -> Self { GraphData { diff --git a/src/graph/mod.rs b/src/graph/mod.rs index eec3d0e63..9e676557f 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -25,6 +25,7 @@ use crate::circuit::lookup::LookupOp; use crate::circuit::modules::ModulePlanner; use crate::circuit::table::{Table, RESERVED_BLINDING_ROWS_PAD}; use crate::circuit::{CheckMode, InputType}; +use crate::fieldutils::felt_to_f64; use crate::pfsys::PrettyElements; use crate::tensor::{Tensor, ValTensor}; use crate::RunArgs; @@ -156,6 +157,19 @@ pub struct GraphWitness { } impl GraphWitness { + /// + pub fn get_float_outputs(&self, scales: &[crate::Scale]) -> Vec> { + self.outputs + .iter() + .enumerate() + .map(|(i, x)| { + x.iter() + .map(|y| (felt_to_f64(*y) / scale_to_multiplier(scales[i])) as f32) + .collect::>() + }) + .collect() + } + /// pub fn new(inputs: Vec>, outputs: Vec>) -> Self { GraphWitness { @@ -1109,9 +1123,10 @@ impl GraphCircuit { input: &[Tensor], max_logrows: Option, lookup_safety_margin: i128, - ) -> Result<(), Box> { + ) -> Result> { let res = self.forward(&mut input.to_vec(), None, None)?; - self.calc_min_logrows(&res, max_logrows, lookup_safety_margin) + self.calc_min_logrows(&res, max_logrows, lookup_safety_margin)?; + Ok(res) } /// Runs the forward pass of the model / graph of computations and any associated hashing. diff --git a/src/graph/model.rs b/src/graph/model.rs index 1d6c69589..951dd7892 100644 --- a/src/graph/model.rs +++ b/src/graph/model.rs @@ -18,6 +18,8 @@ use crate::{ }; use halo2curves::bn256::Fr as Fp; +#[cfg(not(target_arch = "wasm32"))] +use super::input::GraphData; #[cfg(not(target_arch = "wasm32"))] use colored::Colorize; use halo2_proofs::{ @@ -26,19 +28,10 @@ use halo2_proofs::{ }; use halo2curves::ff::Field; use itertools::Itertools; -use serde::Deserialize; -use serde::Serialize; -#[cfg(not(target_arch = "wasm32"))] -use tract_onnx; -#[cfg(not(target_arch = "wasm32"))] -use tract_onnx::prelude::{ - Framework, Graph, InferenceFact, InferenceModelExt, SymbolValues, TypedFact, TypedOp, -}; -#[cfg(not(target_arch = "wasm32"))] -use tract_onnx::tract_hir::ops::scan::Scan; - use log::error; use log::{debug, info, trace}; +use serde::Deserialize; +use serde::Serialize; use std::collections::BTreeMap; #[cfg(not(target_arch = "wasm32"))] use std::collections::HashMap; @@ -49,6 +42,16 @@ use std::io::Read; use std::path::PathBuf; #[cfg(not(target_arch = "wasm32"))] use tabled::Table; +#[cfg(not(target_arch = "wasm32"))] +use tract_onnx; +#[cfg(not(target_arch = "wasm32"))] +use tract_onnx::prelude::{ + Framework, Graph, InferenceFact, InferenceModelExt, SymbolValues, TypedFact, TypedOp, +}; +#[cfg(not(target_arch = "wasm32"))] +use tract_onnx::tract_core::internal::DatumType; +#[cfg(not(target_arch = "wasm32"))] +use tract_onnx::tract_hir::ops::scan::Scan; use unzip_n::unzip_n; unzip_n!(pub 3); @@ -737,14 +740,12 @@ impl Model { /// * `scale` - The scale to use for quantization. /// * `public_params` - Whether to make the params public. #[cfg(not(target_arch = "wasm32"))] - fn load_onnx_model( + fn load_onnx_using_tract( reader: &mut dyn std::io::Read, run_args: &RunArgs, - visibility: &VarVisibility, - ) -> Result> { - use tract_onnx::tract_hir::internal::GenericFactoid; - - let start_time = instant::Instant::now(); + ) -> Result<(Graph>, SymbolValues), Box> { + use maybe_rayon::iter::{IntoParallelRefIterator, ParallelIterator}; + use tract_onnx::{tract_core::internal::TDim, tract_hir::internal::GenericFactoid}; let mut model = tract_onnx::onnx().model_for_read(reader).map_err(|e| { error!("Error loading model: {}", e); @@ -775,15 +776,76 @@ impl Model { for (i, _) in model.clone().outputs.iter().enumerate() { model.set_output_fact(i, InferenceFact::default())?; } - // Note: do not optimize the model, as the layout will depend on underlying hardware - let mut model = model.into_typed()?.into_decluttered()?; + let mut symbol_values = SymbolValues::default(); for (symbol, value) in run_args.variables.iter() { let symbol = model.symbol_table.sym(symbol); symbol_values = symbol_values.with(&symbol, *value as i64); info!("set {} to {}", symbol, value); + println!("set {} to {}", symbol, value); + } + + // Note: do not optimize the model, as the layout will depend on underlying hardware + let mut typed_model = model + .into_typed()? + .concretize_dims(&symbol_values)? + .into_decluttered()?; + + // concretize constants + for node in typed_model.eval_order()? { + let node = typed_model.node_mut(node); + if node.op_is::() { + // map option to err + let op = node + .op_as_mut::() + .unwrap(); + // get inner value to Arc + let constant = op.0.as_ref(); + + match constant.datum_type() { + DatumType::TDim => { + // Generally a shape or hyperparam + let vec = constant.as_slice::()?.to_vec(); + let data: Vec = + vec.par_iter().map(|x| x.eval(&symbol_values)).collect(); + + // allow unsafe + #[allow(unsafe_code)] + unsafe { + let bytes = std::slice::from_raw_parts( + data.as_ptr() as *const u8, + data.len() * DatumType::TDim.size_of(), + ); + + op.0 = std::sync::Arc::new(tract_onnx::prelude::Tensor::from_raw_dt( + DatumType::TDim, + constant.shape(), + bytes, + )?); + } + } + _ => {} + } + } } - model = model.concretize_dims(&symbol_values)?; + + Ok((typed_model, symbol_values)) + } + + /// Loads an Onnx model from a specified path. + /// # Arguments + /// * `reader` - A reader for an Onnx file. + /// * `scale` - The scale to use for quantization. + /// * `public_params` - Whether to make the params public. + #[cfg(not(target_arch = "wasm32"))] + fn load_onnx_model( + reader: &mut dyn std::io::Read, + run_args: &RunArgs, + visibility: &VarVisibility, + ) -> Result> { + let start_time = instant::Instant::now(); + + let (model, symbol_values) = Self::load_onnx_using_tract(reader, run_args)?; let scales = VarScales::from_args(run_args)?; let nodes = Self::nodes_from_graph( @@ -1050,6 +1112,44 @@ impl Model { }); } + #[cfg(not(target_arch = "wasm32"))] + /// Run tract onnx model on sample data ! + pub fn run_onnx_predictions( + run_args: &RunArgs, + model_path: &std::path::Path, + data_chunks: &[GraphData], + input_shapes: Vec>, + ) -> Result>>, Box> { + use tract_onnx::tract_core::internal::IntoArcTensor; + + let (model, _) = Model::load_onnx_using_tract( + &mut std::fs::File::open(model_path) + .map_err(|_| format!("failed to load model at {}", model_path.display()))?, + run_args, + )?; + + let datum_types: Vec = model + .input_outlets()? + .iter() + .map(|o| model.node(o.node).outputs[o.slot].fact.datum_type) + .collect(); + + let runnable_model = model.into_runnable()?; + let mut outputs = vec![]; + for chunk in data_chunks { + let result = runnable_model.run(chunk.to_tract_data(&input_shapes, &datum_types)?)?; + outputs.push( + result + .into_iter() + .map(|t| { + crate::graph::utilities::extract_tensor_value(t.into_arc_tensor()).unwrap() + }) + .collect(), + ); + } + Ok(outputs) + } + /// Creates a `Model` from parsed run_args /// # Arguments /// * `params` - A [GraphSettings] struct holding parsed CLI arguments. diff --git a/src/graph/utilities.rs b/src/graph/utilities.rs index b605b8eb2..7ca931dd2 100644 --- a/src/graph/utilities.rs +++ b/src/graph/utilities.rs @@ -103,9 +103,9 @@ pub fn node_output_shapes( #[cfg(not(target_arch = "wasm32"))] use tract_onnx::prelude::SymbolValues; #[cfg(not(target_arch = "wasm32"))] -fn extract_tensor_value( +/// Extracts the raw values from a tensor. +pub fn extract_tensor_value( input: Arc, - symbol_values: &SymbolValues, ) -> Result, Box> { use maybe_rayon::prelude::{IntoParallelRefIterator, ParallelIterator}; @@ -195,7 +195,7 @@ fn extract_tensor_value( .par_iter() .map(|x| match x.to_i64() { Ok(v) => Ok(v as f32), - Err(_) => match x.eval(symbol_values).to_i64() { + Err(_) => match x.to_i64() { Ok(v) => Ok(v as f32), Err(_) => Err("could not evaluate tdim"), }, @@ -477,7 +477,7 @@ pub fn new_op_from_onnx( let op: Const = load_op::(node.op(), idx, node.op().name().to_string())?; let dt = op.0.datum_type(); // Raw values are always f32 - let raw_value = extract_tensor_value(op.0, symbol_values)?; + let raw_value = extract_tensor_value(op.0)?; // If bool or a tensor dimension then don't scale let constant_scale = match dt { DatumType::Bool @@ -1074,12 +1074,12 @@ pub fn new_op_from_onnx( } }; - let kernel = extract_tensor_value(conv_node.kernel.clone(), symbol_values)?; + let kernel = extract_tensor_value(conv_node.kernel.clone())?; let kernel = quantize_tensor(kernel, scales.params, param_visibility)?; let bias = match conv_node.bias.clone() { Some(b) => { - let const_value = extract_tensor_value(b, symbol_values)?; + let const_value = extract_tensor_value(b)?; let val = quantize_tensor( const_value, @@ -1152,12 +1152,12 @@ pub fn new_op_from_onnx( } }; - let kernel = extract_tensor_value(deconv_node.kernel.clone(), symbol_values)?; + let kernel = extract_tensor_value(deconv_node.kernel.clone())?; let kernel = quantize_tensor(kernel, scales.params, param_visibility)?; let bias = match deconv_node.bias.clone() { Some(b) => { - let const_value = extract_tensor_value(b, symbol_values)?; + let const_value = extract_tensor_value(b)?; let val = quantize_tensor( const_value,