edgefirst_decoder/per_scale/mod.rs
1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! Per-scale quantized decoder — see
5//! `.claude/plans/2026-04-28-per-scale-decoder-optimized-design.md`.
6
7pub mod helper;
8pub(crate) mod kernels;
9pub(crate) mod outputs;
10pub(crate) mod pipeline;
11pub(crate) mod plan;
12pub(crate) mod prefilter;
13
14pub use helper::apply_schema_quant;
15
16/// Output element type chosen by the user at `DecoderBuilder::with_decode_dtype()`.
17///
18/// The whole post-merge pipeline (boxes, scores, mask coefs, protos) is
19/// emitted in this dtype. `F16` saves ~2× memory bandwidth at the cost of
20/// 10-bit mantissa precision — empirically safe for YOLO-family models.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
22pub enum DecodeDtype {
23 /// 32-bit float output (default).
24 #[default]
25 F32,
26 /// 16-bit float output. Halves the memory bandwidth of the decode
27 /// pipeline; on aarch64 it also keeps the NEON FP16 kernels in their
28 /// native width instead of widening.
29 F16,
30}
31
32/// Activation function applied after dequantization on a logical output.
33///
34/// Sourced from the schema's `activation_required` field. Currently only
35/// `Sigmoid` is wired through the per-scale pipeline; future activations
36/// (e.g. `Softmax` on objectness) extend this enum without ripple.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38#[allow(dead_code)] // consumed by per-scale pipeline tasks
39pub(crate) enum Activation {
40 #[default]
41 None,
42 Sigmoid,
43}
44
45impl Activation {
46 /// Translate a schema activation to a per_scale Activation.
47 /// Returns Activation::None when the schema declares no activation.
48 #[allow(dead_code)] // consumed by per-scale pipeline tasks
49 pub(crate) fn from_schema(s: Option<crate::schema::Activation>) -> Self {
50 match s {
51 Some(crate::schema::Activation::Sigmoid) => Self::Sigmoid,
52 _ => Self::None,
53 }
54 }
55}
56
57pub(crate) use outputs::{DecodedOutputBuffers, DecodedOutputsRef};
58pub(crate) use plan::PerScalePlan;
59
60/// Per-scale decoder for schema-v2 per-scale models. Built once at
61/// `DecoderBuilder::build()` time; consumed per-frame via `run()`.
62#[derive(Debug)]
63#[allow(dead_code)] // Wired by Task 24's Decoder integration.
64pub(crate) struct PerScaleDecoder {
65 pub(crate) plan: PerScalePlan,
66 pub(crate) buffers: DecodedOutputBuffers,
67}
68
69impl PerScaleDecoder {
70 /// Build a decoder from a plan, allocating output buffers.
71 #[allow(dead_code)] // Wired by Task 23's builder.
72 pub(crate) fn new(plan: PerScalePlan) -> Self {
73 let buffers = DecodedOutputBuffers::new(
74 plan.out_dtype,
75 plan.total_anchors,
76 plan.num_classes,
77 plan.num_mask_coefs,
78 plan.proto_nhwc_shape.as_deref(),
79 );
80 Self { plan, buffers }
81 }
82
83 /// Decode one frame's worth of inputs.
84 ///
85 /// With a `score_threshold`, only anchors that can reach it are
86 /// decoded and the outputs hold just those, in anchor order; `None`
87 /// decodes every anchor.
88 #[allow(dead_code)] // Wired by Task 24.
89 pub(crate) fn run<'a>(
90 &'a mut self,
91 inputs: &[&edgefirst_tensor::TensorDyn],
92 score_threshold: Option<f32>,
93 ) -> crate::DecoderResult<DecodedOutputsRef<'a>> {
94 pipeline::run(&self.plan, &mut self.buffers, inputs, score_threshold)
95 }
96}
97
98/// Owned f32 snapshot of pre-NMS per-scale outputs.
99///
100/// Returned by [`crate::Decoder::_testing_run_per_scale_pre_nms`] and
101/// used by integration tests to compare against fixture intermediates
102/// without the noise of NMS ordering.
103#[doc(hidden)]
104pub struct PreNmsCapture {
105 pub boxes_xywh: ndarray::Array2<f32>,
106 pub scores: ndarray::Array2<f32>,
107 pub mask_coefs: Option<ndarray::Array2<f32>>,
108 pub protos: Option<ndarray::Array4<f32>>,
109}