diff --git a/rust/CHANGELOG.md b/rust/CHANGELOG.md index f84912d..7f8fffc 100644 --- a/rust/CHANGELOG.md +++ b/rust/CHANGELOG.md @@ -6,6 +6,11 @@ crate is versioned independently of the Python package in this repository. Entries for releases before 0.3.0 were reconstructed from the commit history, since those versions were published manually and without tags. +## Unreleased + +- Add `Trustmark::with_options` and `SessionOptions` to set the intra-op thread count and + spinning of the ONNX Runtime sessions. `Trustmark::new` keeps its behaviour (8 threads). + ## 0.4.0 - Downgrade `ort` to 2.0.0-rc.10 to restore support for Intel macOS. diff --git a/rust/src/lib.rs b/rust/src/lib.rs index ce66dcb..63a5eb2 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -91,6 +91,29 @@ impl From for Error { pub use bits::Version; pub use model::Variant; +/// ONNX Runtime threading for the encoder and decoder sessions. +/// +/// The default keeps the behaviour of [`Trustmark::new`]: 8 intra-op threads per session and the +/// runtime's default spinning. A host that runs several models side by side (one per worker +/// thread, for example) sets `intra_threads` to its own budget and usually turns spinning off, +/// so idle sessions do not burn CPU. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SessionOptions { + /// Intra-op threads of each session (the encoder and the decoder each get this many). + pub intra_threads: usize, + /// Whether intra-op threads spin while waiting for work; `None` keeps the runtime default. + pub intra_op_spinning: Option, +} + +impl Default for SessionOptions { + fn default() -> Self { + Self { + intra_threads: 8, + intra_op_spinning: None, + } + } +} + impl Trustmark { /// Load a Trustmark model. pub fn new>( @@ -98,14 +121,27 @@ impl Trustmark { variant: Variant, version: Version, ) -> Result { - let encoder = Session::builder()? - .with_optimization_level(GraphOptimizationLevel::Level3)? - .with_intra_threads(8)? - .commit_from_file(models.as_ref().join(variant.encoder_filename()))?; - let decoder = Session::builder()? - .with_optimization_level(GraphOptimizationLevel::Level3)? - .with_intra_threads(8)? - .commit_from_file(models.as_ref().join(variant.decoder_filename()))?; + Self::with_options(models, variant, version, SessionOptions::default()) + } + + /// Load a Trustmark model with explicit ONNX Runtime threading. + pub fn with_options>( + models: P, + variant: Variant, + version: Version, + options: SessionOptions, + ) -> Result { + let session = |file: String| -> Result { + let mut builder = Session::builder()? + .with_optimization_level(GraphOptimizationLevel::Level3)? + .with_intra_threads(options.intra_threads)?; + if let Some(spinning) = options.intra_op_spinning { + builder = builder.with_intra_op_spinning(spinning)?; + } + Ok(builder.commit_from_file(models.as_ref().join(file))?) + }; + let encoder = session(variant.encoder_filename())?; + let decoder = session(variant.decoder_filename())?; Ok(Self { encoder: Mutex::new(encoder), decoder: Mutex::new(decoder), @@ -201,6 +237,22 @@ mod tests { Trustmark::new("./models", Variant::Q, Version::Bch5).unwrap(); } + #[test] + fn roundtrip_with_one_thread_and_no_spinning() { + let options = SessionOptions { + intra_threads: 1, + intra_op_spinning: Some(false), + }; + let tm = Trustmark::with_options("./models", Variant::Q, Version::Bch5, options).unwrap(); + let input = image::open("../images/ghost.png").unwrap(); + let watermark = "1011011110011000111111000000011111011111011100000110110110111".to_owned(); + let encoded = tm.encode(watermark.clone(), input, 0.95).unwrap(); + let decoded = tm + .decode(DynamicImage::ImageRgba8(encoded.to_rgba8())) + .unwrap(); + assert_eq!(watermark, decoded); + } + fn roundtrip(path: impl AsRef) { let tm = Trustmark::new("./models", Variant::Q, Version::Bch5).unwrap(); let input = image::open(path.as_ref()).unwrap();