use edgefirst_decoder::ProtoLayout;
use edgefirst_tensor::{DType, QuantMode};
use super::Int8InterpolationMode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ProtoUpload {
I8Compute,
I8CpuRepack,
F32R32f,
F32ToRgba16f,
F16Rgba16f,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ProtoProgram {
Int8Nearest,
Int8Bilinear,
Int8TwoPass,
F32,
F16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum CountUniform {
NumProtos,
NumLayers,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct ProtoPlan {
pub(super) upload: ProtoUpload,
pub(super) program: ProtoProgram,
pub(super) count_uniform: CountUniform,
}
pub(super) fn plan_proto(
proto_dtype: DType,
layout: ProtoLayout,
compute_active: bool,
has_float_linear: bool,
int8_mode: Int8InterpolationMode,
) -> crate::Result<ProtoPlan> {
if layout == ProtoLayout::Nchw {
return Err(crate::Error::NotSupported(
"GL segmentation path does not yet support NCHW proto layout; \
caller should use CPU fallback"
.into(),
));
}
let plan = match proto_dtype {
DType::I8 => {
let upload = if compute_active {
ProtoUpload::I8Compute
} else {
ProtoUpload::I8CpuRepack
};
let program = match int8_mode {
Int8InterpolationMode::Nearest => ProtoProgram::Int8Nearest,
Int8InterpolationMode::Bilinear => ProtoProgram::Int8Bilinear,
Int8InterpolationMode::TwoPass => ProtoProgram::Int8TwoPass,
};
ProtoPlan {
upload,
program,
count_uniform: CountUniform::NumProtos,
}
}
DType::F32 => {
if has_float_linear {
ProtoPlan {
upload: ProtoUpload::F32R32f,
program: ProtoProgram::F32,
count_uniform: CountUniform::NumProtos,
}
} else {
ProtoPlan {
upload: ProtoUpload::F32ToRgba16f,
program: ProtoProgram::F16,
count_uniform: CountUniform::NumLayers,
}
}
}
DType::F16 => ProtoPlan {
upload: ProtoUpload::F16Rgba16f,
program: ProtoProgram::F16,
count_uniform: CountUniform::NumLayers,
},
other => {
return Err(crate::Error::InvalidShape(format!(
"GL seg path: proto dtype {other:?} not supported"
)));
}
};
Ok(plan)
}
pub(super) fn repack_layers<T: Copy + Default>(protos: ndarray::ArrayView3<'_, T>) -> Vec<T> {
let (height, width, num_protos) = protos.dim();
let mut out = vec![T::default(); height * width * num_protos];
for k in 0..num_protos {
for y in 0..height {
for x in 0..width {
out[k * height * width + y * width + x] = protos[[y, x, k]];
}
}
}
out
}
pub(super) fn repack_rgba_f16_layers<S: Copy>(
protos: ndarray::ArrayView3<'_, S>,
conv: impl Fn(S) -> half::f16,
) -> (Vec<u8>, usize) {
let (height, width, num_protos) = protos.dim();
let num_layers = num_protos.div_ceil(4);
let layer_stride = height * width * 4;
let mut buf = vec![0u16; layer_stride * num_layers];
for y in 0..height {
for x in 0..width {
for k in 0..num_layers * 4 {
let val = if k < num_protos {
conv(protos[[y, x, k]])
} else {
half::f16::ZERO
};
let layer = k / 4;
let channel = k % 4;
buf[layer * layer_stride + y * width * 4 + x * 4 + channel] = val.to_bits();
}
}
}
let byte_buf =
unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, buf.len() * 2).to_vec() };
(byte_buf, num_layers)
}
pub(super) fn dequant_coeffs<T: Copy + Into<f32>>(
vals: &[T],
mode: Option<QuantMode<'_>>,
dtype_label: &str,
) -> crate::Result<Vec<f32>> {
let (scale, zp) = match mode {
None => (1.0_f32, 0.0_f32),
Some(QuantMode::PerTensor { scale, zero_point }) => (scale, zero_point as f32),
Some(QuantMode::PerTensorSymmetric { scale }) => (scale, 0.0),
Some(other) => {
return Err(crate::Error::NotSupported(format!(
"{dtype_label} mask_coefficients quantization mode {other:?} \
not supported on GL seg path"
)));
}
};
Ok(vals.iter().map(|&v| (v.into() - zp) * scale).collect())
}
#[cfg(test)]
mod tests {
use super::*;
const MODES: [Int8InterpolationMode; 3] = [
Int8InterpolationMode::Nearest,
Int8InterpolationMode::Bilinear,
Int8InterpolationMode::TwoPass,
];
#[test]
fn plan_proto_full_table() {
for compute in [false, true] {
for linear in [false, true] {
for mode in MODES {
let plan =
plan_proto(DType::I8, ProtoLayout::Nhwc, compute, linear, mode).unwrap();
assert_eq!(
plan.upload,
if compute {
ProtoUpload::I8Compute
} else {
ProtoUpload::I8CpuRepack
}
);
assert_eq!(
plan.program,
match mode {
Int8InterpolationMode::Nearest => ProtoProgram::Int8Nearest,
Int8InterpolationMode::Bilinear => ProtoProgram::Int8Bilinear,
Int8InterpolationMode::TwoPass => ProtoProgram::Int8TwoPass,
}
);
assert_eq!(plan.count_uniform, CountUniform::NumProtos);
}
for mode in MODES {
let plan =
plan_proto(DType::F32, ProtoLayout::Nhwc, compute, linear, mode).unwrap();
if linear {
assert_eq!(plan.upload, ProtoUpload::F32R32f);
assert_eq!(plan.program, ProtoProgram::F32);
assert_eq!(plan.count_uniform, CountUniform::NumProtos);
} else {
assert_eq!(plan.upload, ProtoUpload::F32ToRgba16f);
assert_eq!(plan.program, ProtoProgram::F16);
assert_eq!(plan.count_uniform, CountUniform::NumLayers);
}
}
for mode in MODES {
let plan =
plan_proto(DType::F16, ProtoLayout::Nhwc, compute, linear, mode).unwrap();
assert_eq!(plan.upload, ProtoUpload::F16Rgba16f);
assert_eq!(plan.program, ProtoProgram::F16);
assert_eq!(plan.count_uniform, CountUniform::NumLayers);
}
}
}
}
#[test]
fn plan_proto_rejects_nchw_for_every_dtype() {
for dtype in [DType::I8, DType::F32, DType::F16] {
let err = plan_proto(
dtype,
ProtoLayout::Nchw,
false,
true,
Int8InterpolationMode::Bilinear,
)
.unwrap_err();
assert!(
matches!(err, crate::Error::NotSupported(ref m) if m.contains("NCHW")),
"{dtype:?}: {err:?}"
);
}
}
#[test]
fn plan_proto_rejects_unsupported_dtypes() {
for dtype in [DType::U8, DType::I16, DType::I32, DType::F64] {
let err = plan_proto(
dtype,
ProtoLayout::Nhwc,
false,
true,
Int8InterpolationMode::Bilinear,
)
.unwrap_err();
assert!(
matches!(err, crate::Error::InvalidShape(ref m) if m.contains("not supported")),
"{dtype:?}: {err:?}"
);
}
}
#[test]
fn repack_layers_transposes_hwc_to_layer_first() {
let data: Vec<i32> = (0..2 * 2 * 3)
.map(|i| {
let (y, x, k) = (i / 6, (i / 3) % 2, i % 3);
y * 100 + x * 10 + k
})
.collect();
let view = ndarray::ArrayView3::from_shape((2, 2, 3), &data).unwrap();
let out = repack_layers(view);
assert_eq!(out.len(), 12);
for k in 0..3 {
for y in 0..2 {
for x in 0..2 {
assert_eq!(out[k * 4 + y * 2 + x] as usize, y * 100 + x * 10 + k);
}
}
}
}
#[test]
fn repack_rgba_f16_layers_packs_quads_and_zero_pads_tail() {
let data: Vec<f32> = (0..2 * 6).map(|i| i as f32).collect();
let view = ndarray::ArrayView3::from_shape((1, 2, 6), &data).unwrap();
let (bytes, num_layers) = repack_rgba_f16_layers(view, half::f16::from_f32);
assert_eq!(num_layers, 2);
let halves: Vec<half::f16> = bytes
.chunks_exact(2)
.map(|b| half::f16::from_bits(u16::from_le_bytes([b[0], b[1]])))
.collect();
let layer_stride = 2 * 4; for x in 0..2 {
for k in 0..8 {
let expected = if k < 6 { (x * 6 + k) as f32 } else { 0.0 };
let got = halves[(k / 4) * layer_stride + x * 4 + (k % 4)].to_f32();
assert_eq!(got, expected, "x={x} k={k}");
}
}
let data_f16: Vec<half::f16> = data.iter().map(|v| half::f16::from_f32(*v)).collect();
let view_f16 = ndarray::ArrayView3::from_shape((1, 2, 6), &data_f16).unwrap();
let (bytes_native, layers_native) = repack_rgba_f16_layers(view_f16, |v| v);
assert_eq!(layers_native, 2);
assert_eq!(bytes_native, bytes);
}
#[test]
fn dequant_coeffs_per_tensor() {
let vals: [i8; 4] = [-115, 0, 20, 127];
let out = dequant_coeffs(
&vals,
Some(QuantMode::PerTensor {
scale: 0.5,
zero_point: 20,
}),
"I8",
)
.unwrap();
assert_eq!(out, vec![-67.5, -10.0, 0.0, 53.5]);
}
#[test]
fn dequant_coeffs_symmetric_and_passthrough() {
let vals: [i16; 3] = [-2, 0, 2];
let sym = dequant_coeffs(
&vals,
Some(QuantMode::PerTensorSymmetric { scale: 0.25 }),
"I16",
)
.unwrap();
assert_eq!(sym, vec![-0.5, 0.0, 0.5]);
let plain = dequant_coeffs(&vals, None, "I16").unwrap();
assert_eq!(plain, vec![-2.0, 0.0, 2.0]);
}
#[test]
fn dequant_coeffs_rejects_per_channel() {
let vals: [i8; 2] = [1, 2];
let scales = [0.5_f32];
let err = dequant_coeffs(
&vals,
Some(QuantMode::PerChannelSymmetric {
scales: &scales,
axis: 0,
}),
"I8",
)
.unwrap_err();
assert!(
matches!(err, crate::Error::NotSupported(ref m) if m.contains("quantization mode")),
"{err:?}"
);
}
}