optirs_nas/multimodal_nas/types.rs
1//! Core value types for the multimodal NAS module: modalities, layer and
2//! fusion-operator enums, the per-modality encoder, and the helper
3//! functions that classify layers and fusion operators.
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8// =======================================================================
9// Modalities
10// =======================================================================
11
12/// Input modality consumed by a multimodal architecture.
13///
14/// `Modality` is `Ord` so that declared-modality lists have a stable,
15/// deterministic ordering and so the type can key ordered collections.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
17pub enum Modality {
18 /// Visual input (pixels / feature maps).
19 Image,
20 /// Textual / token input.
21 Text,
22 /// Audio waveform / spectrogram input.
23 Audio,
24}
25
26impl fmt::Display for Modality {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 match self {
29 Modality::Image => write!(f, "image"),
30 Modality::Text => write!(f, "text"),
31 Modality::Audio => write!(f, "audio"),
32 }
33 }
34}
35
36// =======================================================================
37// Activation kinds
38// =======================================================================
39
40/// Non-linear activation applied by [`MultimodalLayer::Activation`].
41///
42/// Kept as a fieldless enum so [`MultimodalLayer`] can derive `Eq` / `Hash`.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub enum ActivationKind {
45 /// Rectified linear unit.
46 Relu,
47 /// Gaussian error linear unit.
48 Gelu,
49 /// Hyperbolic tangent.
50 Tanh,
51 /// Logistic sigmoid.
52 Sigmoid,
53}
54
55// =======================================================================
56// Layers
57// =======================================================================
58
59/// Encoder / head layer types available in multimodal architectures.
60///
61/// Each variant carries the hyperparameters that influence its compute /
62/// memory footprint. Concrete fields use `u32` (rather than `usize`)
63/// because configurations are persisted to JSON and compared for equality
64/// across machines with different pointer widths.
65///
66/// Layers fall into three groups:
67///
68/// * *Image* encoders ([`MultimodalLayer::ImageConv`],
69/// [`MultimodalLayer::ImagePool`]),
70/// * *Text* encoders ([`MultimodalLayer::TextEmbed`],
71/// [`MultimodalLayer::TextTransformer`]),
72/// * *Audio* encoders ([`MultimodalLayer::AudioConv`],
73/// [`MultimodalLayer::AudioRecurrent`]),
74///
75/// plus the *generic* layers ([`MultimodalLayer::Dense`],
76/// [`MultimodalLayer::Norm`], [`MultimodalLayer::Activation`]) which may
77/// appear in any modality's encoder and in the head. The mapping from a
78/// layer to its owning modality (or `None` for generic layers) is exposed
79/// by [`layer_modality`].
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub enum MultimodalLayer {
82 /// 2D convolution for the image encoder.
83 ImageConv {
84 /// Number of output channels.
85 channels: u32,
86 /// Square convolution kernel size.
87 kernel: u32,
88 },
89 /// Spatial pooling for the image encoder.
90 ImagePool {
91 /// Pooling window size.
92 kernel: u32,
93 },
94 /// Token-embedding layer for the text encoder.
95 TextEmbed {
96 /// Embedding dimension.
97 dim: u32,
98 },
99 /// Transformer block for the text encoder.
100 TextTransformer {
101 /// Number of attention heads.
102 heads: u32,
103 /// Per-head feature dimension.
104 dim: u32,
105 },
106 /// 1D convolution for the audio encoder.
107 AudioConv {
108 /// Number of output channels.
109 channels: u32,
110 /// Convolution kernel size (in frames).
111 kernel: u32,
112 },
113 /// Recurrent cell for the audio encoder.
114 AudioRecurrent {
115 /// Hidden state size.
116 hidden: u32,
117 },
118 /// Generic dense projection (usable in any encoder or in the head).
119 Dense {
120 /// Output feature dimension.
121 out: u32,
122 },
123 /// Generic normalisation layer.
124 Norm,
125 /// Generic non-linear activation.
126 Activation {
127 /// The activation function applied.
128 kind: ActivationKind,
129 },
130}
131
132/// Return the modality a layer belongs to, or `None` for generic layers.
133///
134/// This is the single source of truth for encoder membership in the
135/// multimodal-NAS module — [`super::MultimodalArchitecture::validate`] uses
136/// it to reject foreign layers placed in the wrong encoder.
137pub fn layer_modality(layer: &MultimodalLayer) -> Option<Modality> {
138 match layer {
139 MultimodalLayer::ImageConv { .. } | MultimodalLayer::ImagePool { .. } => {
140 Some(Modality::Image)
141 }
142 MultimodalLayer::TextEmbed { .. } | MultimodalLayer::TextTransformer { .. } => {
143 Some(Modality::Text)
144 }
145 MultimodalLayer::AudioConv { .. } | MultimodalLayer::AudioRecurrent { .. } => {
146 Some(Modality::Audio)
147 }
148 MultimodalLayer::Dense { .. }
149 | MultimodalLayer::Norm
150 | MultimodalLayer::Activation { .. } => None,
151 }
152}
153
154/// Rough compute / parameter "capacity" of a single layer.
155///
156/// The value is a unitless cost used solely for the modality-balance
157/// constraint; it is intentionally monotone in the dominant size
158/// hyperparameter of each layer. Every layer has capacity `>= 1.0`, so
159/// encoder capacities are always strictly positive.
160pub fn layer_capacity(layer: &MultimodalLayer) -> f64 {
161 match layer {
162 MultimodalLayer::ImageConv { channels, .. } => f64::from(*channels),
163 MultimodalLayer::ImagePool { .. } => 8.0,
164 MultimodalLayer::TextEmbed { dim } => f64::from(*dim),
165 MultimodalLayer::TextTransformer { heads, dim } => f64::from(*heads) * f64::from(*dim),
166 MultimodalLayer::AudioConv { channels, .. } => f64::from(*channels),
167 MultimodalLayer::AudioRecurrent { hidden } => f64::from(*hidden),
168 MultimodalLayer::Dense { out } => f64::from(*out),
169 MultimodalLayer::Norm => 4.0,
170 MultimodalLayer::Activation { .. } => 1.0,
171 }
172}
173
174// =======================================================================
175// Fusion operators
176// =======================================================================
177
178/// Fusion operators combining the per-modality encoder outputs.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
180pub enum FusionOp {
181 /// Early fusion: concatenate encoder features before the head.
182 EarlyConcat,
183 /// Late fusion: combine per-modality predictions (e.g. averaging).
184 LateFusion,
185 /// Cross-modal attention between encoder streams.
186 CrossAttention {
187 /// Number of attention heads.
188 heads: u32,
189 /// Per-head feature dimension.
190 dim: u32,
191 },
192 /// Gated fusion: one modality gates another.
193 Gated {
194 /// Gate / projection dimension.
195 dim: u32,
196 },
197 /// Bilinear pooling of exactly two modality features.
198 Bilinear {
199 /// Bilinear projection dimension.
200 dim: u32,
201 },
202}
203
204/// Minimum number of modalities a fusion operator can combine.
205pub fn fusion_min_modalities(fusion: &FusionOp) -> usize {
206 match fusion {
207 FusionOp::EarlyConcat | FusionOp::LateFusion => 1,
208 FusionOp::CrossAttention { .. } | FusionOp::Gated { .. } | FusionOp::Bilinear { .. } => 2,
209 }
210}
211
212/// Maximum number of modalities a fusion operator can combine, if bounded.
213pub fn fusion_max_modalities(fusion: &FusionOp) -> Option<usize> {
214 match fusion {
215 FusionOp::Bilinear { .. } => Some(2),
216 _ => None,
217 }
218}
219
220/// Return `true` iff `fusion` is compatible with `num_modalities` inputs.
221pub fn fusion_supports(fusion: &FusionOp, num_modalities: usize) -> bool {
222 let min_m = fusion_min_modalities(fusion);
223 match fusion_max_modalities(fusion) {
224 None => num_modalities >= min_m,
225 Some(max_m) => num_modalities >= min_m && num_modalities <= max_m,
226 }
227}
228
229// =======================================================================
230// Modality encoder
231// =======================================================================
232
233/// A per-modality encoder: the modality it encodes and its layer sequence.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct ModalityEncoder {
236 /// The modality this encoder consumes.
237 pub modality: Modality,
238 /// Layers applied in execution order.
239 pub layers: Vec<MultimodalLayer>,
240}
241
242/// Sum of the per-layer capacities of an encoder.
243pub fn encoder_capacity(encoder: &ModalityEncoder) -> f64 {
244 encoder.layers.iter().map(layer_capacity).sum()
245}