#[cfg(feature = "gpu-wgpu")]
use tinyquant_core::codec::{Codebook, CodecConfig};
use tinyquant_core::{
codec::{Codec, Parallelism, PreparedCodec},
errors::CodecError,
GpuComputeBackend, GPU_BATCH_THRESHOLD,
};
#[cfg(feature = "gpu-wgpu")]
fn make_prepared() -> (CodecConfig, Codebook, PreparedCodec) {
let config = CodecConfig::new(4, 42, 64, false).unwrap();
let training: Vec<f32> = (0..256 * 64)
.map(|i| (i as f32 * 0.001_f32).sin())
.collect();
let codebook = Codebook::train(&training, &config).unwrap();
let prepared = PreparedCodec::new(config.clone(), codebook.clone()).unwrap();
(config, codebook, prepared)
}
struct MockCpuBackend {
prepare_calls: usize,
inject_error: Option<CodecError>,
}
impl MockCpuBackend {
fn new() -> Self {
Self {
prepare_calls: 0,
inject_error: None,
}
}
#[cfg(feature = "gpu-wgpu")]
fn with_error(err: CodecError) -> Self {
Self {
prepare_calls: 0,
inject_error: Some(err),
}
}
}
impl GpuComputeBackend for MockCpuBackend {
type Error = CodecError;
fn prepare_for_device(&mut self, _prepared: &mut PreparedCodec) -> Result<(), Self::Error> {
self.prepare_calls += 1;
Ok(())
}
fn compress_batch(
&mut self,
input: &[f32],
rows: usize,
cols: usize,
prepared: &PreparedCodec,
) -> Result<Vec<tinyquant_core::codec::CompressedVector>, Self::Error> {
if let Some(ref e) = self.inject_error {
return Err(e.clone());
}
Codec::new().compress_batch_with(
input,
rows,
cols,
prepared.config(),
prepared.codebook(),
Parallelism::Serial,
)
}
}
#[test]
fn gpu_types_accessible_via_tinyquant_core() {
const _: () = assert!(GPU_BATCH_THRESHOLD > 0);
let _backend: &dyn GpuComputeBackend<Error = CodecError> = &MockCpuBackend::new();
}
#[test]
fn gpu_batch_threshold_is_512() {
assert_eq!(GPU_BATCH_THRESHOLD, 512);
}
#[test]
fn codec_error_gpu_unavailable_variant_is_accessible() {
let err = CodecError::GpuUnavailable("no adapter found".into());
let msg = err.to_string();
assert!(msg.contains("GPU unavailable"), "unexpected message: {msg}");
}
#[test]
fn codec_error_gpu_error_variant_is_accessible() {
let err = CodecError::GpuError("buffer map failed".into());
let msg = err.to_string();
assert!(msg.contains("GPU error"), "unexpected message: {msg}");
}
#[test]
fn codec_error_gpu_unavailable_message_contains_detail() {
let detail = "no wgpu adapter found";
let err = CodecError::GpuUnavailable(detail.into());
let msg = err.to_string();
assert!(
msg.contains(detail),
"error message should contain the detail string; got: {msg}"
);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn compress_batch_gpu_with_below_threshold_uses_cpu_path() {
let (config, codebook, mut prepared) = make_prepared();
let rows = 1_usize; let cols = 64_usize;
let vectors: Vec<f32> = (0..rows * cols)
.map(|i| (i as f32 * 0.001_f32).cos())
.collect();
let mut backend = MockCpuBackend::new();
let cpu_result = Codec::new()
.compress_batch_with(
&vectors,
rows,
cols,
&config,
&codebook,
Parallelism::Serial,
)
.unwrap();
let gpu_path_result = Codec::new()
.compress_batch_gpu_with(
&vectors,
rows,
cols,
&mut prepared,
&mut backend,
Parallelism::Serial,
)
.unwrap();
assert_eq!(cpu_result.len(), gpu_path_result.len());
for (cpu_cv, gpu_cv) in cpu_result.iter().zip(gpu_path_result.iter()) {
assert_eq!(
cpu_cv.indices(),
gpu_cv.indices(),
"CPU and GPU-routed results must be byte-identical for below-threshold batch"
);
assert_eq!(cpu_cv.config_hash(), gpu_cv.config_hash());
}
assert_eq!(
backend.prepare_calls, 0,
"prepare_for_device should not be called for below-threshold rows"
);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn compress_batch_gpu_with_above_threshold_calls_backend() {
let (_config, _codebook, mut prepared) = make_prepared();
let rows = GPU_BATCH_THRESHOLD; let cols = 64_usize;
let vectors: Vec<f32> = (0..rows * cols)
.map(|i| (i as f32 / 1000.0_f32).sin())
.collect();
let mut backend = MockCpuBackend::new();
let result = Codec::new()
.compress_batch_gpu_with(
&vectors,
rows,
cols,
&mut prepared,
&mut backend,
Parallelism::Serial,
)
.unwrap();
assert_eq!(
result.len(),
rows,
"must return one CompressedVector per row"
);
assert_eq!(
backend.prepare_calls, 1,
"prepare_for_device must be called exactly once for above-threshold rows"
);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn prepare_for_device_is_idempotent_via_codec() {
let (_config, _codebook, mut prepared) = make_prepared();
let rows = GPU_BATCH_THRESHOLD;
let cols = 64_usize;
let vectors: Vec<f32> = (0..rows * cols)
.map(|i| (i as f32 * 0.002_f32).cos())
.collect();
let mut backend = MockCpuBackend::new();
let codec = Codec::new();
let result1 = codec
.compress_batch_gpu_with(
&vectors,
rows,
cols,
&mut prepared,
&mut backend,
Parallelism::Serial,
)
.unwrap();
let result2 = codec
.compress_batch_gpu_with(
&vectors,
rows,
cols,
&mut prepared,
&mut backend,
Parallelism::Serial,
)
.unwrap();
assert_eq!(result1.len(), result2.len());
assert_eq!(
backend.prepare_calls, 2,
"prepare_for_device must be called once per compress_batch_gpu_with invocation"
);
}
#[cfg(feature = "gpu-wgpu")]
#[test]
fn compress_batch_gpu_with_propagates_backend_error() {
let (_config, _codebook, mut prepared) = make_prepared();
let rows = GPU_BATCH_THRESHOLD;
let cols = 64_usize;
let vectors: Vec<f32> = vec![0.0_f32; rows * cols];
let injected = CodecError::GpuUnavailable("no adapter".into());
let mut backend = MockCpuBackend::with_error(injected.clone());
let err = Codec::new()
.compress_batch_gpu_with(
&vectors,
rows,
cols,
&mut prepared,
&mut backend,
Parallelism::Serial,
)
.unwrap_err();
assert_eq!(
err, injected,
"backend error must propagate unchanged as CodecError"
);
}
#[test]
fn gpu_module_exports_trait_and_constant() {
use tinyquant_core::gpu::GPU_BATCH_THRESHOLD as THRESH;
assert_eq!(THRESH, 512);
}