1#![warn(missing_docs)]
8
9extern crate self as eredu_nn;
10
11use std::fmt::Debug;
12
13use eredu_checkpoint::LinearFormat;
14
15pub use eredu_nn_macros::Parameterized;
16
17pub mod multimodal;
19pub mod operation_geometry;
21pub mod sequence_layout;
23
24#[derive(Debug, Clone, thiserror::Error)]
26#[error("{message}")]
27pub struct Error {
28 message: String,
29}
30
31impl Error {
32 pub fn backend(error: impl std::fmt::Display) -> Self {
35 Self {
36 message: error.to_string(),
37 }
38 }
39}
40
41#[derive(Debug, Clone, Copy, Eq, PartialEq)]
43pub enum Index {
44 Full,
46 At(i32),
48 Range(i32, i32),
50}
51
52#[derive(Debug, Clone, Copy, Eq, PartialEq)]
54pub enum PadMode {
55 Constant,
57 Edge,
59}
60
61#[derive(Debug, Clone, Copy)]
63pub enum AttentionMask<'a, T> {
64 None,
66 Causal,
68 Tensor(&'a T),
70}
71
72#[derive(Debug, Clone, Copy, Eq, PartialEq)]
76pub struct HeadExpansion {
77 pub axis: usize,
79 pub source_heads: i32,
81 pub target_heads: i32,
83}
84
85impl HeadExpansion {
86 pub fn validate<T: Tensor>(&self, input: &T) -> Result<(), Error> {
88 let shape = input.shape();
89 if self.source_heads <= 0
90 || self.target_heads <= 0
91 || self.target_heads % self.source_heads != 0
92 || shape.get(self.axis).copied() != Some(self.source_heads)
93 {
94 return Err(Error::backend(format!(
95 "invalid head expansion axis={} source={} target={} shape={shape:?}",
96 self.axis, self.source_heads, self.target_heads
97 )));
98 }
99 Ok(())
100 }
101
102 pub const fn repeats(self) -> i32 {
104 self.target_heads / self.source_heads
105 }
106}
107
108#[derive(Debug, Clone, Copy)]
110pub struct SegmentedAttentionInput<'a, T> {
111 pub queries: &'a T,
113 pub keys: &'a T,
115 pub values: &'a T,
117 pub segment_lengths: &'a [i32],
119 pub scale: f32,
121}
122
123impl<T: Tensor> SegmentedAttentionInput<'_, T> {
124 pub fn validate(&self) -> Result<(), Error> {
126 let query = self.queries.shape();
127 let key = self.keys.shape();
128 let value = self.values.shape();
129 if query.len() != 3
130 || key.len() != 3
131 || value.len() != 3
132 || query[0] <= 0
133 || query[1] <= 0
134 || query[2] <= 0
135 || query[0] != key[0]
136 || query[0] != value[0]
137 || query[1] != key[1]
138 || query[1] != value[1]
139 || query[2] != key[2]
140 || value[2] <= 0
141 || !self.scale.is_finite()
142 || self.scale <= 0.0
143 {
144 return Err(Error::backend(format!(
145 "invalid segmented attention geometry q={query:?} k={key:?} v={value:?} scale={}",
146 self.scale
147 )));
148 }
149 validate_segment_lengths(query[0], self.segment_lengths)
150 }
151}
152
153pub fn validate_segment_lengths(total: i32, segment_lengths: &[i32]) -> Result<(), Error> {
155 if total <= 0 || segment_lengths.is_empty() {
156 return Err(Error::backend(format!(
157 "segmented attention requires a positive total and at least one segment, got total={total} segments={segment_lengths:?}"
158 )));
159 }
160 let mut sum = 0i32;
161 for &length in segment_lengths {
162 if length <= 0 {
163 return Err(Error::backend(format!(
164 "segmented attention lengths must be positive, got {segment_lengths:?}"
165 )));
166 }
167 sum = sum.checked_add(length).ok_or_else(|| {
168 Error::backend("segmented attention length total overflowed signed 32-bit geometry")
169 })?;
170 if sum > total {
171 return Err(Error::backend(format!(
172 "segmented attention lengths exceed total {total}: {segment_lengths:?}"
173 )));
174 }
175 }
176 if sum != total {
177 return Err(Error::backend(format!(
178 "segmented attention lengths sum to {sum}, expected {total}"
179 )));
180 }
181 Ok(())
182}
183
184pub fn reference_expand_heads(
186 values: &[f32],
187 shape: &[usize],
188 axis: usize,
189 target_heads: usize,
190) -> Result<(Vec<f32>, Vec<usize>), Error> {
191 let source_heads = shape.get(axis).copied().unwrap_or(0);
192 if source_heads == 0 || target_heads == 0 || !target_heads.is_multiple_of(source_heads) {
193 return Err(Error::backend(format!(
194 "invalid reference head expansion axis={axis} target={target_heads} shape={shape:?}"
195 )));
196 }
197 let elements = shape.iter().try_fold(1usize, |total, width| {
198 total
199 .checked_mul(*width)
200 .ok_or_else(|| Error::backend("reference head expansion element count overflowed"))
201 })?;
202 if elements != values.len() {
203 return Err(Error::backend(format!(
204 "reference head expansion expected {elements} values, got {}",
205 values.len()
206 )));
207 }
208 let outer = shape[..axis].iter().product::<usize>();
209 let inner = shape[axis + 1..].iter().product::<usize>();
210 let repeats = target_heads / source_heads;
211 let mut output = Vec::with_capacity(outer * target_heads * inner);
212 for outer_index in 0..outer {
213 for source in 0..source_heads {
214 let start = (outer_index * source_heads + source) * inner;
215 for _ in 0..repeats {
216 output.extend_from_slice(&values[start..start + inner]);
217 }
218 }
219 }
220 let mut output_shape = shape.to_vec();
221 output_shape[axis] = target_heads;
222 Ok((output, output_shape))
223}
224
225#[allow(clippy::too_many_arguments)]
229pub fn reference_segmented_attention(
230 tokens: usize,
231 heads: usize,
232 dimensions: usize,
233 value_dimensions: usize,
234 queries: &[f32],
235 keys: &[f32],
236 values: &[f32],
237 segment_lengths: &[i32],
238 scale: f32,
239) -> Result<Vec<f32>, Error> {
240 let tokens_i32 = i32::try_from(tokens)
241 .map_err(|_| Error::backend("reference segmented attention token count exceeds i32"))?;
242 validate_segment_lengths(tokens_i32, segment_lengths)?;
243 if heads == 0
244 || dimensions == 0
245 || value_dimensions == 0
246 || !scale.is_finite()
247 || scale <= 0.0
248 || queries.len() != tokens * heads * dimensions
249 || keys.len() != tokens * heads * dimensions
250 || values.len() != tokens * heads * value_dimensions
251 {
252 return Err(Error::backend(
253 "invalid reference segmented attention geometry",
254 ));
255 }
256 let mut output = vec![0.0f32; tokens * heads * value_dimensions];
257 let mut segment_start = 0usize;
258 for &length in segment_lengths {
259 let length = usize::try_from(length).expect("validated positive segment length");
260 let segment_end = segment_start + length;
261 for query_token in segment_start..segment_end {
262 for head in 0..heads {
263 let mut scores = Vec::with_capacity(length);
264 for key_token in segment_start..segment_end {
265 let mut score = 0.0f32;
266 for dimension in 0..dimensions {
267 let query_index = (query_token * heads + head) * dimensions + dimension;
268 let key_index = (key_token * heads + head) * dimensions + dimension;
269 score += queries[query_index] * keys[key_index];
270 }
271 scores.push(score * scale);
272 }
273 let maximum = scores.iter().copied().fold(f32::NEG_INFINITY, f32::max);
274 let denominator = scores
275 .iter_mut()
276 .map(|score| {
277 *score = (*score - maximum).exp();
278 *score
279 })
280 .sum::<f32>();
281 for value_dimension in 0..value_dimensions {
282 let mut result = 0.0f32;
283 for (relative, key_token) in (segment_start..segment_end).enumerate() {
284 let value_index =
285 (key_token * heads + head) * value_dimensions + value_dimension;
286 result += scores[relative] / denominator * values[value_index];
287 }
288 let output_index =
289 (query_token * heads + head) * value_dimensions + value_dimension;
290 output[output_index] = result;
291 }
292 }
293 }
294 segment_start = segment_end;
295 }
296 Ok(output)
297}
298
299#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
301pub enum AttentionValueSource {
302 Projected,
304 ReuseKey,
306}
307
308#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
310pub enum AttentionStateSource {
311 Local {
313 value: AttentionValueSource,
315 },
316 Publish {
318 value: AttentionValueSource,
320 },
321 Shared,
323}
324
325impl AttentionStateSource {
326 pub const fn owns_state(self) -> bool {
328 !matches!(self, Self::Shared)
329 }
330
331 pub const fn publishes_state(self) -> bool {
333 matches!(self, Self::Publish { .. })
334 }
335
336 pub const fn value(self) -> Option<AttentionValueSource> {
338 match self {
339 Self::Local { value } | Self::Publish { value } => Some(value),
340 Self::Shared => None,
341 }
342 }
343}
344
345#[cfg(test)]
346mod attention_state_source_tests {
347 use super::{AttentionStateSource, AttentionValueSource};
348
349 #[test]
350 fn ownership_publication_and_key_as_value_are_independent() {
351 let local = AttentionStateSource::Local {
352 value: AttentionValueSource::Projected,
353 };
354 let publisher = AttentionStateSource::Publish {
355 value: AttentionValueSource::ReuseKey,
356 };
357 assert!(local.owns_state());
358 assert!(!local.publishes_state());
359 assert_eq!(local.value(), Some(AttentionValueSource::Projected));
360 assert!(publisher.owns_state());
361 assert!(publisher.publishes_state());
362 assert_eq!(publisher.value(), Some(AttentionValueSource::ReuseKey));
363 assert!(!AttentionStateSource::Shared.owns_state());
364 assert_eq!(AttentionStateSource::Shared.value(), None);
365 }
366}
367
368#[cfg(test)]
369mod recurrent_encoder_contract_tests {
370 use super::{
371 reference_expand_heads, reference_segmented_attention, validate_segment_lengths,
372 NormalizationConstructionSpec, NormalizationScale,
373 };
374
375 #[test]
376 fn normalization_construction_rejects_invalid_geometry_and_scalars() {
377 assert!(NormalizationConstructionSpec {
378 dimensions: 8,
379 epsilon: 1e-6,
380 scale: NormalizationScale::Unit,
381 }
382 .validate()
383 .is_ok());
384 assert!(NormalizationConstructionSpec {
385 dimensions: 0,
386 epsilon: 1e-6,
387 scale: NormalizationScale::Unit,
388 }
389 .validate()
390 .is_err());
391 assert!(NormalizationConstructionSpec {
392 dimensions: 8,
393 epsilon: f32::NAN,
394 scale: NormalizationScale::Unit,
395 }
396 .validate()
397 .is_err());
398 }
399
400 #[test]
401 fn head_expansion_reference_preserves_grouped_row_order() {
402 let (values, shape) =
403 reference_expand_heads(&[1.0, 2.0, 3.0, 4.0], &[1, 2, 2], 1, 4).unwrap();
404 assert_eq!(shape, vec![1, 4, 2]);
405 assert_eq!(values, vec![1.0, 2.0, 1.0, 2.0, 3.0, 4.0, 3.0, 4.0]);
406 assert!(reference_expand_heads(&[1.0, 2.0], &[1, 2], 1, 3).is_err());
407 }
408
409 #[test]
410 fn segmented_attention_reference_is_independent_per_contiguous_segment() {
411 let output = reference_segmented_attention(
412 3,
413 1,
414 1,
415 1,
416 &[0.0, 0.0, 0.0],
417 &[0.0, 0.0, 0.0],
418 &[2.0, 4.0, 9.0],
419 &[2, 1],
420 1.0,
421 )
422 .unwrap();
423 assert_eq!(output, vec![3.0, 3.0, 9.0]);
424 assert!(validate_segment_lengths(3, &[]).is_err());
425 assert!(validate_segment_lengths(3, &[2, 0, 1]).is_err());
426 assert!(validate_segment_lengths(3, &[2]).is_err());
427 assert!(validate_segment_lengths(3, &[2, 2]).is_err());
428 assert!(validate_segment_lengths(i32::MAX, &[i32::MAX, 1]).is_err());
429 }
430}
431
432#[derive(Debug, Clone, Copy)]
439pub struct IndexedAttentionInput<'a, T> {
440 pub queries: &'a T,
442 pub local_keys: &'a T,
444 pub local_values: &'a T,
446 pub pooled_keys: &'a T,
448 pub pooled_values: &'a T,
450 pub selected_positions: &'a T,
452 pub scale: f32,
454 pub local_mask: Option<&'a T>,
456 pub pooled_mask: Option<&'a T>,
458 pub sinks: Option<&'a T>,
460}
461
462#[derive(Debug, Clone, Copy)]
464pub struct PooledAttentionInput<'a, T> {
465 pub queries: &'a T,
467 pub local: &'a T,
469 pub pooled: &'a T,
471 pub scale: f32,
473 pub local_mask: Option<&'a T>,
475 pub pooled_mask: Option<&'a T>,
477 pub sinks: Option<&'a T>,
479}
480
481#[derive(Debug, Clone, Copy)]
486pub struct PooledPositionInput<'a, T> {
487 pub queries: &'a T,
489 pub pooled_keys: &'a T,
491 pub head_weights: &'a T,
493 pub mask: Option<&'a T>,
496 pub top_k: i32,
498 pub scale: f32,
500 pub head_scale: f32,
502}
503
504#[derive(Debug, Clone, Copy)]
510pub struct RelativeAttentionInput<'a, T> {
511 pub queries: &'a T,
513 pub keys: &'a T,
515 pub values: &'a T,
517 pub profiles: &'a T,
519 pub query_offset: i32,
521 pub key_offset: i32,
523 pub window: Option<i32>,
525 pub log_scaling_floor: Option<i32>,
527 pub log_scaling_alpha: f32,
529}
530
531impl<T: Tensor> RelativeAttentionInput<'_, T> {
532 pub fn validate(&self) -> Result<(), Error> {
534 let query = self.queries.shape();
535 let key = self.keys.shape();
536 let value = self.values.shape();
537 let profiles = self.profiles.shape();
538 if query.len() != 4
539 || key.len() != 4
540 || value.len() != 4
541 || profiles.len() != 4
542 || query[0] != key[0]
543 || key != value
544 || query[2] != profiles[2]
545 || query[0] != profiles[0]
546 || query[1] != profiles[1]
547 || query[3] != key[3]
548 || query[1] % key[1] != 0
549 || profiles[3] <= 0
550 || self.window.is_some_and(|window| window <= 0)
551 || self.log_scaling_floor.is_some_and(|floor| floor <= 0)
552 || !self.log_scaling_alpha.is_finite()
553 {
554 return Err(Error::backend(format!(
555 "invalid relative attention geometry q={query:?} k={key:?} v={value:?} profiles={profiles:?} window={:?} floor={:?} alpha={}",
556 self.window, self.log_scaling_floor, self.log_scaling_alpha
557 )));
558 }
559 Ok(())
560 }
561}
562
563impl<T: Tensor> IndexedAttentionInput<'_, T> {
564 pub fn validate(&self) -> Result<(), Error> {
567 let query = self.queries.shape();
568 let local_keys = self.local_keys.shape();
569 let local_values = self.local_values.shape();
570 let pooled_keys = self.pooled_keys.shape();
571 let pooled_values = self.pooled_values.shape();
572 let selected = self.selected_positions.shape();
573 if query.len() != 4
574 || local_keys.len() != 3
575 || local_values.len() != 3
576 || pooled_keys.len() != 3
577 || pooled_values.len() != 3
578 || selected.len() != 3
579 || query[0] != local_keys[0]
580 || query[0] != local_values[0]
581 || query[0] != pooled_keys[0]
582 || query[0] != pooled_values[0]
583 || query[0] != selected[0]
584 || query[2] != selected[1]
585 || query[3] != local_keys[2]
586 || query[3] != pooled_keys[2]
587 || local_keys[1] != local_values[1]
588 || pooled_keys[1] != pooled_values[1]
589 || local_values[2] != pooled_values[2]
590 || selected[2] <= 0
591 || pooled_keys[1] <= 0
592 {
593 return Err(Error::backend(format!(
594 "invalid indexed-attention geometry: queries={query:?} local_keys={local_keys:?} local_values={local_values:?} pooled_keys={pooled_keys:?} pooled_values={pooled_values:?} selected={selected:?}"
595 )));
596 }
597 if !self.scale.is_finite() || self.scale <= 0.0 {
598 return Err(Error::backend(format!(
599 "indexed-attention scale must be finite and positive, got {}",
600 self.scale
601 )));
602 }
603 if let Some(sinks) = self.sinks {
604 if sinks.shape() != [query[1]] {
605 return Err(Error::backend(format!(
606 "indexed-attention sinks require shape [{}], got {:?}",
607 query[1],
608 sinks.shape()
609 )));
610 }
611 }
612 Ok(())
613 }
614}
615
616#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
618pub struct ParameterId(String);
619
620impl ParameterId {
621 pub fn new(id: impl Into<String>) -> Result<Self, ParameterTopologyError> {
623 let id = id.into();
624 if id.trim().is_empty() {
625 return Err(ParameterTopologyError::EmptyId);
626 }
627 Ok(Self(id))
628 }
629
630 pub fn as_str(&self) -> &str {
632 &self.0
633 }
634}
635
636impl std::fmt::Display for ParameterId {
637 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
638 formatter.write_str(&self.0)
639 }
640}
641
642#[derive(Debug, Clone, Eq, PartialEq)]
644pub struct ParameterSpec {
645 pub id: ParameterId,
647 pub trainable: bool,
649 pub alias_of: Option<ParameterId>,
651 pub group: Option<String>,
653 pub linear_companion: Option<LinearCompanionRole>,
655 pub linear_companion_of: Option<ParameterId>,
657}
658
659impl ParameterSpec {
660 pub fn trainable(id: impl Into<String>) -> Result<Self, ParameterTopologyError> {
662 Ok(Self {
663 id: ParameterId::new(id)?,
664 trainable: true,
665 alias_of: None,
666 group: None,
667 linear_companion: None,
668 linear_companion_of: None,
669 })
670 }
671}
672
673#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
675pub enum LinearCompanionRole {
676 Scale,
678 AffineBias,
680}
681
682#[derive(Debug, Clone, Eq, PartialEq)]
684pub struct ParameterMetadata {
685 pub id: ParameterId,
687 pub trainable: bool,
689 pub alias_of: Option<ParameterId>,
691 pub group: Option<String>,
693 pub linear_companion: Option<LinearCompanionRole>,
695 pub linear_companion_of: Option<ParameterId>,
697}
698
699impl ParameterMetadata {
700 pub fn from_spec(spec: &ParameterSpec, trainable: bool) -> Self {
702 Self {
703 id: spec.id.clone(),
704 trainable,
705 alias_of: spec.alias_of.clone(),
706 group: spec.group.clone(),
707 linear_companion: spec.linear_companion,
708 linear_companion_of: spec.linear_companion_of.clone(),
709 }
710 }
711}
712
713#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
715pub enum ParameterTopologyError {
716 #[error("parameter identity must not be empty")]
718 EmptyId,
719 #[error("parameter identity {0} is duplicated")]
721 DuplicateId(ParameterId),
722 #[error("parameter alias {alias} points to missing destination {destination}")]
724 MissingAliasDestination {
725 alias: ParameterId,
727 destination: ParameterId,
729 },
730 #[error("parameter alias {alias} points to non-authoritative alias {destination}")]
732 AliasTargetsAlias {
733 alias: ParameterId,
735 destination: ParameterId,
737 },
738}
739
740pub trait ParameterVisitor<'a, T: 'a> {
742 fn visit(&mut self, metadata: ParameterMetadata, value: &'a T);
744}
745
746pub trait ParameterVisitorMut<'a, T: 'a> {
748 fn visit_mut(&mut self, metadata: ParameterMetadata, value: &'a mut T);
750}
751
752pub trait Parameterized<T: 'static> {
759 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
761 where
762 V: ParameterVisitor<'a, T>;
763
764 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
766 where
767 V: ParameterVisitorMut<'a, T>;
768
769 fn set_trainable(&mut self, trainable: bool);
771}
772
773pub fn validate_parameter_topology<T: 'static, M>(
775 module: &M,
776) -> Result<Vec<ParameterMetadata>, ParameterTopologyError>
777where
778 M: Parameterized<T>,
779{
780 struct Collector(Vec<ParameterMetadata>);
781 impl<'a, T: 'a> ParameterVisitor<'a, T> for Collector {
782 fn visit(&mut self, metadata: ParameterMetadata, _value: &'a T) {
783 self.0.push(metadata);
784 }
785 }
786
787 let mut collector = Collector(Vec::new());
788 module.visit_parameters(&mut collector);
789 let mut topology = std::collections::BTreeMap::new();
790 for metadata in &collector.0 {
791 if topology.insert(metadata.id.clone(), metadata).is_some() {
792 return Err(ParameterTopologyError::DuplicateId(metadata.id.clone()));
793 }
794 }
795 for metadata in &collector.0 {
796 let Some(destination) = &metadata.alias_of else {
797 continue;
798 };
799 let Some(target) = topology.get(destination) else {
800 return Err(ParameterTopologyError::MissingAliasDestination {
801 alias: metadata.id.clone(),
802 destination: destination.clone(),
803 });
804 };
805 if target.alias_of.is_some() {
806 return Err(ParameterTopologyError::AliasTargetsAlias {
807 alias: metadata.id.clone(),
808 destination: destination.clone(),
809 });
810 }
811 }
812 Ok(collector.0)
813}
814
815#[derive(Debug, Clone)]
817pub struct LinearSpec {
818 pub input: i32,
820 pub output: i32,
822 pub weight: ParameterSpec,
824 pub bias: Option<ParameterSpec>,
826 pub format: LinearFormatSpec,
828}
829
830#[derive(Debug, Clone)]
832pub struct EmbeddingSpec {
833 pub vocabulary: i32,
835 pub dimensions: i32,
837 pub weight: ParameterSpec,
839 pub format: LinearFormatSpec,
841}
842
843#[derive(Debug, Clone, Eq, PartialEq)]
848pub struct LinearFormatSpec {
849 format: LinearFormat,
850 scale: Option<ParameterSpec>,
851 affine_bias: Option<ParameterSpec>,
852}
853
854impl LinearFormatSpec {
855 pub fn unscaled(format: LinearFormat) -> Result<Self, Error> {
857 let spec = Self {
858 format,
859 scale: None,
860 affine_bias: None,
861 };
862 spec.validate()?;
863 Ok(spec)
864 }
865
866 pub fn scaled(format: LinearFormat, scale: ParameterSpec) -> Result<Self, Error> {
868 let mut scale = scale;
869 scale.linear_companion = Some(LinearCompanionRole::Scale);
870 scale.linear_companion_of = None;
871 let spec = Self {
872 format,
873 scale: Some(scale),
874 affine_bias: None,
875 };
876 spec.validate()?;
877 Ok(spec)
878 }
879
880 pub fn affine(
882 format: LinearFormat,
883 scale: ParameterSpec,
884 affine_bias: ParameterSpec,
885 ) -> Result<Self, Error> {
886 let mut scale = scale;
887 scale.linear_companion = Some(LinearCompanionRole::Scale);
888 scale.linear_companion_of = None;
889 let mut affine_bias = affine_bias;
890 affine_bias.linear_companion = Some(LinearCompanionRole::AffineBias);
891 affine_bias.linear_companion_of = None;
892 let spec = Self {
893 format,
894 scale: Some(scale),
895 affine_bias: Some(affine_bias),
896 };
897 spec.validate()?;
898 Ok(spec)
899 }
900
901 pub const fn encoding(&self) -> LinearFormat {
903 self.format
904 }
905
906 pub const fn scale(&self) -> Option<&ParameterSpec> {
908 self.scale.as_ref()
909 }
910
911 pub const fn affine_bias(&self) -> Option<&ParameterSpec> {
913 self.affine_bias.as_ref()
914 }
915
916 pub fn validate(&self) -> Result<(), Error> {
918 self.format.validate().map_err(Error::backend)?;
919 let expected = match self.format {
920 LinearFormat::Dense | LinearFormat::GgufIQuant { .. } => (false, false),
921 LinearFormat::MxFp4 | LinearFormat::E4M3BlockFp8(_) => (true, false),
922 LinearFormat::Affine(_) => (true, true),
923 };
924 if (self.scale.is_some(), self.affine_bias.is_some()) != expected {
925 return Err(Error::backend(format!(
926 "linear format {:?} requires scale/bias companions {:?}, got {:?}",
927 self.format,
928 expected,
929 (self.scale.is_some(), self.affine_bias.is_some())
930 )));
931 }
932 if self
933 .scale
934 .as_ref()
935 .zip(self.affine_bias.as_ref())
936 .is_some_and(|(scale, bias)| scale.id == bias.id)
937 {
938 return Err(Error::backend(
939 "linear scale and affine-bias companions require distinct identities",
940 ));
941 }
942 if self
943 .scale
944 .as_ref()
945 .is_some_and(|scale| scale.linear_companion != Some(LinearCompanionRole::Scale))
946 || self
947 .affine_bias
948 .as_ref()
949 .is_some_and(|bias| bias.linear_companion != Some(LinearCompanionRole::AffineBias))
950 {
951 return Err(Error::backend(
952 "linear format companions have invalid semantic roles",
953 ));
954 }
955 Ok(())
956 }
957
958 pub fn validate_for_weight(&self, weight: &ParameterSpec) -> Result<(), Error> {
960 self.validate()?;
961 if self
962 .scale
963 .as_ref()
964 .into_iter()
965 .chain(self.affine_bias.as_ref())
966 .any(|companion| companion.id == weight.id)
967 {
968 return Err(Error::backend(format!(
969 "linear format companion reuses primary weight identity {}",
970 weight.id
971 )));
972 }
973 Ok(())
974 }
975}
976
977#[derive(Debug, Clone, Eq, PartialEq)]
979pub struct VocabularyParallelRange {
980 pub global_vocabulary: usize,
982 pub local: std::ops::Range<usize>,
984}
985
986impl VocabularyParallelRange {
987 pub fn validate(&self) -> Result<(), Error> {
989 if self.global_vocabulary == 0
990 || self.local.is_empty()
991 || self.local.end > self.global_vocabulary
992 {
993 return Err(Error::backend(format!(
994 "invalid vocabulary-parallel range {:?} of {}",
995 self.local, self.global_vocabulary
996 )));
997 }
998 Ok(())
999 }
1000
1001 pub fn validate_global_rows(&self, rows: i32) -> Result<(), Error> {
1004 self.validate()?;
1005 if usize::try_from(rows).ok() != Some(self.global_vocabulary) {
1006 return Err(Error::backend(format!(
1007 "vocabulary-parallel operator declares {rows} rows but ownership covers {}",
1008 self.global_vocabulary
1009 )));
1010 }
1011 Ok(())
1012 }
1013
1014 pub fn balanced_peer_widths(
1021 &self,
1022 partitions: usize,
1023 rank: usize,
1024 ) -> Result<Vec<usize>, Error> {
1025 self.validate()?;
1026 if partitions == 0 || rank >= partitions {
1027 return Err(Error::backend(format!(
1028 "invalid vocabulary partition rank {rank} of {partitions}"
1029 )));
1030 }
1031 let base = self.global_vocabulary / partitions;
1032 let remainder = self.global_vocabulary % partitions;
1033 let widths = (0..partitions)
1034 .map(|peer| base + usize::from(peer < remainder))
1035 .collect::<Vec<_>>();
1036 let start = widths[..rank].iter().sum::<usize>();
1037 let expected = start..start + widths[rank];
1038 if self.local != expected {
1039 return Err(Error::backend(format!(
1040 "vocabulary-parallel range {:?} differs from balanced rank {rank} ownership {expected:?}",
1041 self.local
1042 )));
1043 }
1044 Ok(widths)
1045 }
1046}
1047
1048#[cfg(test)]
1049mod vocabulary_parallel_range_tests {
1050 use super::VocabularyParallelRange;
1051
1052 #[test]
1053 fn balanced_peer_widths_are_neutral_and_reject_local_layout_drift() {
1054 let range = VocabularyParallelRange {
1055 global_vocabulary: 11,
1056 local: 4..8,
1057 };
1058 assert_eq!(range.balanced_peer_widths(3, 1).unwrap(), [4, 4, 3]);
1059
1060 let drifted = VocabularyParallelRange {
1061 global_vocabulary: 11,
1062 local: 3..7,
1063 };
1064 assert!(drifted.balanced_peer_widths(3, 1).is_err());
1065 }
1066}
1067
1068#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1070pub enum EmbeddingLookupPolicy {
1071 Strict,
1073 ZeroSentinel(i32),
1076}
1077
1078impl EmbeddingLookupPolicy {
1079 pub fn validate(self) -> Result<(), Error> {
1081 if let Self::ZeroSentinel(sentinel) = self {
1082 if sentinel >= 0 {
1083 return Err(Error::backend(format!(
1084 "embedding zero sentinel must be negative, got {sentinel}"
1085 )));
1086 }
1087 }
1088 Ok(())
1089 }
1090}
1091
1092#[derive(Debug, Clone, Eq, PartialEq)]
1094pub struct FusedProjectionSegment {
1095 name: String,
1096 width: i32,
1097}
1098
1099impl FusedProjectionSegment {
1100 pub fn new(name: impl Into<String>, width: i32) -> Result<Self, Error> {
1102 let name = name.into();
1103 if name.trim().is_empty() || width <= 0 {
1104 return Err(Error::backend(format!(
1105 "fused projection segments require a name and positive width, got name={name:?} width={width}"
1106 )));
1107 }
1108 Ok(Self { name, width })
1109 }
1110
1111 pub fn name(&self) -> &str {
1113 &self.name
1114 }
1115
1116 pub const fn width(&self) -> i32 {
1118 self.width
1119 }
1120}
1121
1122#[derive(Debug, Clone, Eq, PartialEq)]
1124pub struct FusedProjectionLayout {
1125 segments: Vec<FusedProjectionSegment>,
1126 output_width: i32,
1127}
1128
1129impl FusedProjectionLayout {
1130 pub fn new(segments: impl IntoIterator<Item = FusedProjectionSegment>) -> Result<Self, Error> {
1132 let segments = segments.into_iter().collect::<Vec<_>>();
1133 if segments.is_empty() {
1134 return Err(Error::backend(
1135 "fused projection layout must contain at least one segment",
1136 ));
1137 }
1138 let mut names = std::collections::BTreeSet::new();
1139 let mut output_width = 0i32;
1140 for segment in &segments {
1141 if !names.insert(segment.name.clone()) {
1142 return Err(Error::backend(format!(
1143 "fused projection segment {:?} is duplicated",
1144 segment.name
1145 )));
1146 }
1147 output_width = output_width.checked_add(segment.width).ok_or_else(|| {
1148 Error::backend("fused projection output width overflowed signed 32-bit geometry")
1149 })?;
1150 }
1151 Ok(Self {
1152 segments,
1153 output_width,
1154 })
1155 }
1156
1157 pub fn segments(&self) -> &[FusedProjectionSegment] {
1159 &self.segments
1160 }
1161
1162 pub const fn output_width(&self) -> i32 {
1164 self.output_width
1165 }
1166
1167 pub fn split<T: Tensor>(&self, output: &T, context: &T::Context) -> Result<Vec<T>, Error> {
1169 let actual = output
1170 .shape()
1171 .last()
1172 .copied()
1173 .ok_or_else(|| Error::backend("fused projection output has no feature axis"))?;
1174 if actual != self.output_width {
1175 return Err(Error::backend(format!(
1176 "fused projection emitted width {actual}, expected {}",
1177 self.output_width
1178 )));
1179 }
1180 let mut start = 0i32;
1181 let mut indexes = vec![Index::Full; output.shape().len()];
1182 self.segments
1183 .iter()
1184 .map(|segment| {
1185 let end = start + segment.width;
1186 let last = indexes.len() - 1;
1187 indexes[last] = Index::Range(start, end);
1188 let selected = output.index(&indexes, context);
1189 start = end;
1190 selected
1191 })
1192 .collect()
1193 }
1194}
1195
1196#[derive(Debug, Clone)]
1202pub enum NormalizationScale {
1203 Learned(ParameterSpec),
1205 LearnedOffset {
1207 weight: ParameterSpec,
1209 offset: f32,
1211 },
1212 Unit,
1214}
1215
1216#[derive(Debug, Clone)]
1218pub struct NormalizationConstructionSpec {
1219 pub dimensions: i32,
1221 pub epsilon: f32,
1223 pub scale: NormalizationScale,
1225}
1226
1227impl NormalizationConstructionSpec {
1228 pub fn learned(dimensions: i32, epsilon: f32, weight: ParameterSpec) -> Self {
1230 Self {
1231 dimensions,
1232 epsilon,
1233 scale: NormalizationScale::Learned(weight),
1234 }
1235 }
1236
1237 pub fn validate(&self) -> Result<(), Error> {
1239 let offset = match &self.scale {
1240 NormalizationScale::LearnedOffset { offset, .. } => Some(*offset),
1241 NormalizationScale::Learned(_) | NormalizationScale::Unit => None,
1242 };
1243 if self.dimensions <= 0
1244 || !self.epsilon.is_finite()
1245 || self.epsilon <= 0.0
1246 || offset.is_some_and(|offset| !offset.is_finite())
1247 {
1248 return Err(Error::backend(format!(
1249 "invalid RMS normalization construction: dimensions={} epsilon={} offset={offset:?}",
1250 self.dimensions, self.epsilon
1251 )));
1252 }
1253 Ok(())
1254 }
1255}
1256
1257#[derive(Debug, Clone, Copy, PartialEq)]
1259pub enum RotaryAlgorithm {
1260 Default,
1262 Linear {
1264 factor: f32,
1266 },
1267 Llama3 {
1269 factor: f32,
1271 low_frequency_factor: f32,
1273 high_frequency_factor: f32,
1275 original_max_positions: i32,
1277 },
1278 Proportional {
1280 factor: f32,
1282 rotary_fraction: f32,
1284 },
1285 Yarn {
1287 factor: f32,
1289 original_max_positions: i32,
1291 beta_fast: f32,
1293 beta_slow: f32,
1295 concentration: f32,
1297 attention_factor: f32,
1299 truncate: bool,
1301 },
1302}
1303
1304impl RotaryAlgorithm {
1305 pub fn validate(self) -> Result<(), Error> {
1307 let positive = |value: f32| value.is_finite() && value > 0.0;
1308 let valid = match self {
1309 Self::Default => true,
1310 Self::Linear { factor } => positive(factor),
1311 Self::Llama3 {
1312 factor,
1313 low_frequency_factor,
1314 high_frequency_factor,
1315 original_max_positions,
1316 } => {
1317 positive(factor)
1318 && positive(low_frequency_factor)
1319 && positive(high_frequency_factor)
1320 && high_frequency_factor > low_frequency_factor
1321 && original_max_positions > 0
1322 }
1323 Self::Proportional {
1324 factor,
1325 rotary_fraction,
1326 } => positive(factor) && positive(rotary_fraction) && rotary_fraction <= 1.0,
1327 Self::Yarn {
1328 factor,
1329 original_max_positions,
1330 beta_fast,
1331 beta_slow,
1332 concentration,
1333 attention_factor,
1334 ..
1335 } => {
1336 positive(factor)
1337 && original_max_positions > 0
1338 && positive(beta_fast)
1339 && positive(beta_slow)
1340 && beta_fast > beta_slow
1341 && positive(concentration)
1342 && attention_factor.is_finite()
1343 && attention_factor >= 0.0
1344 }
1345 };
1346 if valid {
1347 Ok(())
1348 } else {
1349 Err(Error::backend(format!(
1350 "invalid normalized rotary algorithm: {self:?}"
1351 )))
1352 }
1353 }
1354}
1355
1356#[derive(Debug, Clone, Copy)]
1358pub struct RotarySpec {
1359 pub dimensions: i32,
1361 pub base: f32,
1363 pub traditional: bool,
1365 pub algorithm: RotaryAlgorithm,
1367}
1368
1369pub trait LinearOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
1371 fn forward(&mut self, input: &T, context: &T::Context) -> Result<T, Error>;
1373}
1374
1375pub trait EmbeddingOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
1377 fn forward(&mut self, input: &T, context: &T::Context) -> Result<T, Error>;
1379 fn lookup(
1381 &mut self,
1382 input: &T,
1383 policy: EmbeddingLookupPolicy,
1384 context: &T::Context,
1385 ) -> Result<T, Error> {
1386 policy.validate()?;
1387 match policy {
1388 EmbeddingLookupPolicy::Strict => self.forward(input, context),
1389 EmbeddingLookupPolicy::ZeroSentinel(sentinel) => Err(Error::backend(format!(
1390 "embedding backend does not implement zero sentinel {sentinel}"
1391 ))),
1392 }
1393 }
1394 fn as_linear(&mut self, input: &T, context: &T::Context) -> Result<T, Error>;
1396}
1397
1398pub trait NormalizationOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
1400 fn forward(&mut self, input: &T, context: &T::Context) -> Result<T, Error>;
1402}
1403
1404#[derive(Debug, Clone)]
1406pub struct LowRankProjectionSpec {
1407 pub first: Option<LinearSpec>,
1410 pub normalization: NormalizationConstructionSpec,
1412 pub second: LinearSpec,
1414}
1415
1416impl LowRankProjectionSpec {
1417 pub fn validate(&self) -> Result<(), Error> {
1419 let rank = self.normalization.dimensions;
1420 if rank <= 0 {
1421 return Err(Error::backend(format!(
1422 "low-rank normalization dimensions must be positive, got {rank}"
1423 )));
1424 }
1425 if self.second.input != rank {
1426 return Err(Error::backend(format!(
1427 "low-rank second projection expects {} inputs but rank width is {rank}",
1428 self.second.input
1429 )));
1430 }
1431 if let Some(first) = &self.first {
1432 if first.output != rank {
1433 return Err(Error::backend(format!(
1434 "low-rank first projection emits {} values but rank width is {rank}",
1435 first.output
1436 )));
1437 }
1438 }
1439 Ok(())
1440 }
1441}
1442
1443#[derive(Debug, Clone, Parameterized)]
1446#[parameterized(tensor = "B::Tensor")]
1447pub struct LowRankProjection<B: NeuralBackend> {
1448 pub first: Option<B::Linear>,
1450 pub normalization: B::Normalization,
1452 pub second: B::Linear,
1454}
1455
1456impl<B: NeuralBackend> LowRankProjection<B> {
1457 pub fn new(
1460 spec: LowRankProjectionSpec,
1461 context: &<B::Tensor as Tensor>::Context,
1462 ) -> Result<Self, Error> {
1463 spec.validate()?;
1464 Ok(Self {
1465 first: spec
1466 .first
1467 .map(|projection| B::linear(projection, context))
1468 .transpose()?,
1469 normalization: B::normalization(spec.normalization, context)?,
1470 second: B::linear(spec.second, context)?,
1471 })
1472 }
1473
1474 pub fn forward(
1477 &mut self,
1478 input: &B::Tensor,
1479 context: &<B::Tensor as Tensor>::Context,
1480 ) -> Result<B::Tensor, Error> {
1481 let rank = match &mut self.first {
1482 Some(first) => first.forward(input, context)?,
1483 None => input.clone(),
1484 };
1485 let rank = self.normalization.forward(&rank, context)?;
1486 self.second.forward(&rank, context)
1487 }
1488}
1489
1490pub trait RotaryOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
1492 fn forward(
1494 &mut self,
1495 input: &T,
1496 position: RotaryPosition<'_, T>,
1497 context: &T::Context,
1498 ) -> Result<T, Error>;
1499
1500 fn forward_subspace(
1503 &mut self,
1504 input: &T,
1505 subspace: RotarySubspace,
1506 position: RotaryPosition<'_, T>,
1507 context: &T::Context,
1508 ) -> Result<T, Error> {
1509 let width = *input
1510 .shape()
1511 .last()
1512 .ok_or_else(|| Error::backend("rotary input must have a feature axis"))?;
1513 let (start, dimensions) = subspace.resolve(width)?;
1514 if start == 0 && dimensions == width {
1515 return self.forward(input, position, context);
1516 }
1517 let end = start + dimensions;
1518 let mut indexes = vec![Index::Full; input.shape().len()];
1519 indexes[input.shape().len() - 1] = Index::Range(start, end);
1520 let selected = input.index(&indexes, context)?;
1521 let rotated = self.forward(&selected, position, context)?;
1522 let mut pieces = Vec::with_capacity(3);
1523 if start > 0 {
1524 indexes[input.shape().len() - 1] = Index::Range(0, start);
1525 pieces.push(input.index(&indexes, context)?);
1526 }
1527 pieces.push(rotated);
1528 if end < width {
1529 indexes[input.shape().len() - 1] = Index::Range(end, width);
1530 pieces.push(input.index(&indexes, context)?);
1531 }
1532 T::concatenate(&pieces, -1, context)
1533 }
1534}
1535
1536#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1538pub enum RotarySubspace {
1539 Full,
1541 Range {
1543 start: i32,
1545 dimensions: i32,
1547 },
1548}
1549
1550impl RotarySubspace {
1551 fn resolve(self, width: i32) -> Result<(i32, i32), Error> {
1552 let (start, dimensions) = match self {
1553 Self::Full => (0, width),
1554 Self::Range { start, dimensions } => (start, dimensions),
1555 };
1556 if width <= 0
1557 || start < 0
1558 || dimensions <= 0
1559 || dimensions % 2 != 0
1560 || start > width - dimensions
1561 {
1562 return Err(Error::backend(format!(
1563 "rotary subspace start={start} dimensions={dimensions} is invalid for width {width}"
1564 )));
1565 }
1566 Ok((start, dimensions))
1567 }
1568}
1569
1570#[derive(Debug)]
1572pub enum RotaryPosition<'a, T> {
1573 Offset(i32),
1575 Embeddings {
1577 cosine: &'a T,
1579 sine: &'a T,
1581 },
1582}
1583
1584impl<T> Copy for RotaryPosition<'_, T> {}
1585
1586impl<T> Clone for RotaryPosition<'_, T> {
1587 fn clone(&self) -> Self {
1588 *self
1589 }
1590}
1591
1592#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1594#[non_exhaustive]
1595pub enum GroupScoring {
1596 Softmax,
1598 SelectedSoftmax,
1600 Sigmoid,
1602 SqrtSoftplus,
1604}
1605
1606#[derive(Debug, Clone, Copy, PartialEq)]
1608pub struct TopKGroupSelectionSpec {
1609 group_count: i32,
1610 top_k: i32,
1611 scoring: GroupScoring,
1612 normalize_selected: bool,
1613 normalization_epsilon: f32,
1614 coefficient_scale: f32,
1615 selection_partitions: i32,
1616 selected_groups: i32,
1617}
1618
1619#[derive(Debug, Clone)]
1621pub struct TopKGroupSelectorSpec {
1622 input_dimensions: i32,
1624 weight: ParameterSpec,
1626 bias: Option<ParameterSpec>,
1629 correction_bias: Option<ParameterSpec>,
1632 input_transform: Option<SelectorInputTransformSpec>,
1635 coefficient_scale: Option<ParameterSpec>,
1637 format: LinearFormatSpec,
1639 selection: TopKGroupSelectionSpec,
1641}
1642
1643#[derive(Debug, Clone)]
1645pub struct SelectorInputTransformSpec {
1646 epsilon: f32,
1648 scale: ParameterSpec,
1650 inverse_sqrt_dimensions: bool,
1652}
1653
1654impl SelectorInputTransformSpec {
1655 pub fn new(
1657 epsilon: f32,
1658 scale: ParameterSpec,
1659 inverse_sqrt_dimensions: bool,
1660 ) -> Result<Self, Error> {
1661 if !epsilon.is_finite() || epsilon < 0.0 {
1662 return Err(Error::backend(
1663 "selector input RMS epsilon must be finite and nonnegative",
1664 ));
1665 }
1666 Ok(Self {
1667 epsilon,
1668 scale,
1669 inverse_sqrt_dimensions,
1670 })
1671 }
1672
1673 pub const fn epsilon(&self) -> f32 {
1675 self.epsilon
1676 }
1677 pub const fn scale(&self) -> &ParameterSpec {
1679 &self.scale
1680 }
1681 pub const fn inverse_sqrt_dimensions(&self) -> bool {
1683 self.inverse_sqrt_dimensions
1684 }
1685}
1686
1687impl TopKGroupSelectorSpec {
1688 pub fn new(
1690 input_dimensions: i32,
1691 weight: ParameterSpec,
1692 format: LinearFormatSpec,
1693 selection: TopKGroupSelectionSpec,
1694 ) -> Result<Self, Error> {
1695 let spec = Self {
1696 input_dimensions,
1697 weight,
1698 bias: None,
1699 correction_bias: None,
1700 input_transform: None,
1701 coefficient_scale: None,
1702 format,
1703 selection,
1704 };
1705 spec.validate()?;
1706 Ok(spec)
1707 }
1708
1709 pub fn with_bias(mut self, bias: ParameterSpec) -> Result<Self, Error> {
1711 self.bias = Some(bias);
1712 self.validate()?;
1713 Ok(self)
1714 }
1715 pub fn with_correction_bias(mut self, bias: ParameterSpec) -> Result<Self, Error> {
1717 self.correction_bias = Some(bias);
1718 self.validate()?;
1719 Ok(self)
1720 }
1721 pub fn with_input_transform(mut self, transform: SelectorInputTransformSpec) -> Self {
1723 self.input_transform = Some(transform);
1724 self
1725 }
1726 pub fn with_coefficient_scale(mut self, scale: ParameterSpec) -> Self {
1728 self.coefficient_scale = Some(scale);
1729 self
1730 }
1731 pub const fn input_dimensions(&self) -> i32 {
1733 self.input_dimensions
1734 }
1735 pub const fn weight(&self) -> &ParameterSpec {
1737 &self.weight
1738 }
1739 pub const fn bias(&self) -> Option<&ParameterSpec> {
1741 self.bias.as_ref()
1742 }
1743 pub const fn correction_bias(&self) -> Option<&ParameterSpec> {
1745 self.correction_bias.as_ref()
1746 }
1747 pub const fn input_transform(&self) -> Option<&SelectorInputTransformSpec> {
1749 self.input_transform.as_ref()
1750 }
1751 pub const fn coefficient_scale(&self) -> Option<&ParameterSpec> {
1753 self.coefficient_scale.as_ref()
1754 }
1755 pub const fn format(&self) -> &LinearFormatSpec {
1757 &self.format
1758 }
1759 pub const fn selection(&self) -> TopKGroupSelectionSpec {
1761 self.selection
1762 }
1763
1764 pub fn validate(&self) -> Result<(), Error> {
1766 self.format.validate_for_weight(&self.weight)?;
1767 if self.input_dimensions <= 0 {
1768 return Err(Error::backend(format!(
1769 "selector input dimensions must be positive, got {}",
1770 self.input_dimensions
1771 )));
1772 }
1773 if self
1774 .input_transform
1775 .as_ref()
1776 .is_some_and(|transform| !transform.epsilon.is_finite() || transform.epsilon < 0.0)
1777 {
1778 return Err(Error::backend(
1779 "selector input RMS epsilon must be finite and nonnegative",
1780 ));
1781 }
1782 if self
1783 .bias
1784 .as_ref()
1785 .zip(self.correction_bias.as_ref())
1786 .is_some_and(|(bias, correction_bias)| bias.id == correction_bias.id)
1787 {
1788 return Err(Error::backend(
1789 "selector projection bias and correction bias require distinct parameter identities",
1790 ));
1791 }
1792 Ok(())
1793 }
1794}
1795
1796impl TopKGroupSelectionSpec {
1797 pub fn new(
1799 group_count: i32,
1800 top_k: i32,
1801 scoring: GroupScoring,
1802 normalize_selected: bool,
1803 ) -> Result<Self, Error> {
1804 if group_count <= 0 {
1805 return Err(Error::backend(format!(
1806 "group count must be positive, got {group_count}"
1807 )));
1808 }
1809 if top_k <= 0 || top_k > group_count {
1810 return Err(Error::backend(format!(
1811 "top-k selection count must be in 1..={group_count}, got {top_k}"
1812 )));
1813 }
1814 Ok(Self {
1815 group_count,
1816 top_k,
1817 scoring,
1818 normalize_selected,
1819 normalization_epsilon: 0.0,
1820 coefficient_scale: 1.0,
1821 selection_partitions: 1,
1822 selected_groups: 1,
1823 })
1824 }
1825
1826 pub fn with_groups(
1828 mut self,
1829 selection_partitions: i32,
1830 selected_groups: i32,
1831 ) -> Result<Self, Error> {
1832 if selection_partitions <= 0
1833 || selected_groups <= 0
1834 || selected_groups > selection_partitions
1835 || self.group_count % selection_partitions != 0
1836 || self.top_k > selected_groups * (self.group_count / selection_partitions)
1837 {
1838 return Err(Error::backend(format!(
1839 "invalid grouped selection geometry: group_count={} top_k={} partitions={selection_partitions} selected_partitions={selected_groups}",
1840 self.group_count, self.top_k
1841 )));
1842 }
1843 self.selection_partitions = selection_partitions;
1844 self.selected_groups = selected_groups;
1845 Ok(self)
1846 }
1847
1848 pub fn with_weight_policy(
1850 mut self,
1851 normalization_epsilon: f32,
1852 coefficient_scale: f32,
1853 ) -> Result<Self, Error> {
1854 if !normalization_epsilon.is_finite()
1855 || normalization_epsilon < 0.0
1856 || !coefficient_scale.is_finite()
1857 || coefficient_scale <= 0.0
1858 {
1859 return Err(Error::backend(
1860 "selection normalization epsilon must be finite and nonnegative and grouped scaling must be finite and positive",
1861 ));
1862 }
1863 self.normalization_epsilon = normalization_epsilon;
1864 self.coefficient_scale = coefficient_scale;
1865 Ok(self)
1866 }
1867
1868 pub const fn group_count(self) -> i32 {
1870 self.group_count
1871 }
1872
1873 pub const fn top_k(self) -> i32 {
1875 self.top_k
1876 }
1877
1878 pub const fn scoring(self) -> GroupScoring {
1880 self.scoring
1881 }
1882
1883 pub const fn normalize_selected(self) -> bool {
1885 self.normalize_selected
1886 }
1887
1888 pub const fn normalization_epsilon(self) -> f32 {
1890 self.normalization_epsilon
1891 }
1892
1893 pub const fn coefficient_scale(self) -> f32 {
1895 self.coefficient_scale
1896 }
1897
1898 pub const fn selection_partitions(self) -> i32 {
1900 self.selection_partitions
1901 }
1902
1903 pub const fn selected_groups(self) -> i32 {
1905 self.selected_groups
1906 }
1907}
1908
1909#[derive(Debug, Clone)]
1911pub struct GroupSelection<T> {
1912 group_indices: T,
1914 selected_scores: T,
1916 coefficients: T,
1918}
1919
1920impl<T> GroupSelection<T> {
1921 pub fn new(group_indices: T, selected_scores: T, coefficients: T) -> Self {
1923 Self {
1924 group_indices,
1925 selected_scores,
1926 coefficients,
1927 }
1928 }
1929 pub const fn group_indices(&self) -> &T {
1931 &self.group_indices
1932 }
1933 pub const fn selected_scores(&self) -> &T {
1935 &self.selected_scores
1936 }
1937 pub const fn coefficients(&self) -> &T {
1939 &self.coefficients
1940 }
1941}
1942
1943#[derive(Debug, Clone, Copy, PartialEq)]
1945pub struct JointGroupSelectionSpec {
1946 selectable_groups: i32,
1947 always_on_groups: i32,
1948 top_k: i32,
1949 coefficient_scale: f32,
1950}
1951
1952impl JointGroupSelectionSpec {
1953 pub fn new(
1955 selectable_groups: i32,
1956 always_on_groups: i32,
1957 top_k: i32,
1958 coefficient_scale: f32,
1959 ) -> Result<Self, Error> {
1960 if selectable_groups <= 0
1961 || always_on_groups <= 0
1962 || top_k <= 0
1963 || top_k > selectable_groups
1964 || !coefficient_scale.is_finite()
1965 || coefficient_scale <= 0.0
1966 {
1967 return Err(Error::backend(format!(
1968 "invalid joint group-selection geometry selectable={selectable_groups} always_on={always_on_groups} top_k={top_k} coefficient_scale={coefficient_scale}"
1969 )));
1970 }
1971 Ok(Self {
1972 selectable_groups,
1973 always_on_groups,
1974 top_k,
1975 coefficient_scale,
1976 })
1977 }
1978
1979 pub const fn selectable_groups(self) -> i32 {
1981 self.selectable_groups
1982 }
1983
1984 pub const fn always_on_groups(self) -> i32 {
1986 self.always_on_groups
1987 }
1988
1989 pub const fn top_k(self) -> i32 {
1991 self.top_k
1992 }
1993
1994 pub const fn coefficient_scale(self) -> f32 {
1996 self.coefficient_scale
1997 }
1998}
1999
2000#[derive(Debug, Clone, Copy)]
2003pub struct JointGroupSelectionInput<'a, T> {
2004 hidden: &'a T,
2006 weight: &'a T,
2008 correction_bias: &'a T,
2010 global_scale: &'a T,
2012 selection: JointGroupSelectionSpec,
2014}
2015
2016impl<'a, T: Tensor> JointGroupSelectionInput<'a, T> {
2017 pub fn new(
2019 hidden: &'a T,
2020 weight: &'a T,
2021 correction_bias: &'a T,
2022 global_scale: &'a T,
2023 selection: JointGroupSelectionSpec,
2024 ) -> Result<Self, Error> {
2025 let input = Self {
2026 hidden,
2027 weight,
2028 correction_bias,
2029 global_scale,
2030 selection,
2031 };
2032 input.validate()?;
2033 Ok(input)
2034 }
2035 pub const fn hidden(&self) -> &'a T {
2037 self.hidden
2038 }
2039 pub const fn weight(&self) -> &'a T {
2041 self.weight
2042 }
2043 pub const fn correction_bias(&self) -> &'a T {
2045 self.correction_bias
2046 }
2047 pub const fn global_scale(&self) -> &'a T {
2049 self.global_scale
2050 }
2051 pub const fn selectable_groups(&self) -> i32 {
2053 self.selection.selectable_groups()
2054 }
2055 pub const fn always_on_groups(&self) -> i32 {
2057 self.selection.always_on_groups()
2058 }
2059 pub const fn top_k(&self) -> i32 {
2061 self.selection.top_k()
2062 }
2063 pub const fn coefficient_scale(&self) -> f32 {
2065 self.selection.coefficient_scale()
2066 }
2067}
2068
2069impl<T: Tensor> JointGroupSelectionInput<'_, T> {
2070 pub fn validate(&self) -> Result<(), Error> {
2072 let hidden = self.hidden.shape();
2073 let weight = self.weight.shape();
2074 let bias = self.correction_bias.shape();
2075 let scale = self.global_scale.shape();
2076 let hidden_width = hidden.last().copied().unwrap_or(0);
2077 if hidden.len() < 2
2078 || weight
2079 != [
2080 self.selectable_groups() + self.always_on_groups(),
2081 hidden_width,
2082 ]
2083 || bias != [self.selectable_groups()]
2084 || scale != [1]
2085 {
2086 return Err(Error::backend(format!(
2087 "invalid joint group selection tensors hidden={hidden:?} weight={weight:?} bias={bias:?} scale={scale:?} selectable={} always_on={} top_k={}",
2088 self.selectable_groups(),
2089 self.always_on_groups(),
2090 self.top_k(),
2091 )));
2092 }
2093 Ok(())
2094 }
2095}
2096
2097#[derive(Debug, Clone)]
2099pub struct JointGroupSelection<T> {
2100 primary_indices: T,
2103 primary_coefficients: T,
2105 always_on_coefficients: T,
2107}
2108
2109impl<T> JointGroupSelection<T> {
2110 pub fn new(primary_indices: T, primary_coefficients: T, always_on_coefficients: T) -> Self {
2112 Self {
2113 primary_indices,
2114 primary_coefficients,
2115 always_on_coefficients,
2116 }
2117 }
2118 pub const fn primary_indices(&self) -> &T {
2120 &self.primary_indices
2121 }
2122 pub const fn primary_coefficients(&self) -> &T {
2124 &self.primary_coefficients
2125 }
2126 pub const fn always_on_coefficients(&self) -> &T {
2128 &self.always_on_coefficients
2129 }
2130}
2131
2132#[derive(Debug, Clone, Copy, Eq, PartialEq)]
2134#[non_exhaustive]
2135pub enum GatedProductActivation {
2136 Silu,
2138 GeluApproximate,
2140}
2141
2142#[derive(Debug, Clone, Copy, PartialEq)]
2144pub struct GatedProductPolicy {
2145 activation: GatedProductActivation,
2146 gate_upper_bound: Option<f32>,
2147 up_absolute_bound: Option<f32>,
2148 sigmoid_multiplier: f32,
2149 up_offset: f32,
2150}
2151
2152impl GatedProductPolicy {
2153 pub fn new(
2155 activation: GatedProductActivation,
2156 gate_upper_bound: Option<f32>,
2157 up_absolute_bound: Option<f32>,
2158 sigmoid_multiplier: f32,
2159 up_offset: f32,
2160 ) -> Result<Self, Error> {
2161 let policy = Self {
2162 activation,
2163 gate_upper_bound,
2164 up_absolute_bound,
2165 sigmoid_multiplier,
2166 up_offset,
2167 };
2168 policy.validate()?;
2169 Ok(policy)
2170 }
2171
2172 pub const fn ordinary_silu() -> Self {
2174 Self {
2175 activation: GatedProductActivation::Silu,
2176 gate_upper_bound: None,
2177 up_absolute_bound: None,
2178 sigmoid_multiplier: 1.0,
2179 up_offset: 0.0,
2180 }
2181 }
2182
2183 pub const fn ordinary_gelu_approximate() -> Self {
2185 Self {
2186 activation: GatedProductActivation::GeluApproximate,
2187 ..Self::ordinary_silu()
2188 }
2189 }
2190
2191 pub fn bounded_silu(bound: f32) -> Result<Self, Error> {
2193 Self::new(
2194 GatedProductActivation::Silu,
2195 Some(bound),
2196 Some(bound),
2197 1.0,
2198 0.0,
2199 )
2200 }
2201
2202 pub fn validate(self) -> Result<(), Error> {
2204 if self
2205 .gate_upper_bound
2206 .is_some_and(|bound| !bound.is_finite() || bound <= 0.0)
2207 || self
2208 .up_absolute_bound
2209 .is_some_and(|bound| !bound.is_finite() || bound <= 0.0)
2210 || !self.sigmoid_multiplier.is_finite()
2211 || self.sigmoid_multiplier <= 0.0
2212 || !self.up_offset.is_finite()
2213 {
2214 return Err(Error::backend(format!(
2215 "invalid gated-product policy: {self:?}"
2216 )));
2217 }
2218 Ok(())
2219 }
2220
2221 pub const fn activation(self) -> GatedProductActivation {
2223 self.activation
2224 }
2225
2226 pub const fn gate_upper_bound(self) -> Option<f32> {
2228 self.gate_upper_bound
2229 }
2230
2231 pub const fn up_absolute_bound(self) -> Option<f32> {
2233 self.up_absolute_bound
2234 }
2235
2236 pub const fn sigmoid_multiplier(self) -> f32 {
2238 self.sigmoid_multiplier
2239 }
2240
2241 pub const fn up_offset(self) -> f32 {
2243 self.up_offset
2244 }
2245}
2246
2247impl Default for GatedProductPolicy {
2248 fn default() -> Self {
2249 Self::ordinary_silu()
2250 }
2251}
2252
2253pub trait GroupSelectionOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
2255 fn select(&mut self, logits: &T, context: &T::Context) -> Result<GroupSelection<T>, Error>;
2257
2258 fn select_indices(
2260 &mut self,
2261 input: &T,
2262 group_indices: &T,
2263 context: &T::Context,
2264 ) -> Result<GroupSelection<T>, Error>;
2265}
2266
2267#[derive(Debug, Clone, Eq, PartialEq)]
2269pub struct GatedProductGroupParameters {
2270 gate: GroupedProjectionSpec,
2272 up: GroupedProjectionSpec,
2274 down: GroupedProjectionSpec,
2276}
2277
2278impl GatedProductGroupParameters {
2279 pub fn new(
2281 gate: GroupedProjectionSpec,
2282 up: GroupedProjectionSpec,
2283 down: GroupedProjectionSpec,
2284 ) -> Self {
2285 Self { gate, up, down }
2286 }
2287 pub const fn gate(&self) -> &GroupedProjectionSpec {
2289 &self.gate
2290 }
2291 pub const fn up(&self) -> &GroupedProjectionSpec {
2293 &self.up
2294 }
2295 pub const fn down(&self) -> &GroupedProjectionSpec {
2297 &self.down
2298 }
2299}
2300
2301#[derive(Debug, Clone, Eq, PartialEq)]
2303pub struct GroupedProjectionSpec {
2304 weight: ParameterSpec,
2306 bias: Option<ParameterSpec>,
2308 format: LinearFormatSpec,
2310}
2311
2312impl GroupedProjectionSpec {
2313 pub fn new(
2315 weight: ParameterSpec,
2316 bias: Option<ParameterSpec>,
2317 format: LinearFormatSpec,
2318 ) -> Result<Self, Error> {
2319 let spec = Self {
2320 weight,
2321 bias,
2322 format,
2323 };
2324 spec.validate()?;
2325 Ok(spec)
2326 }
2327 pub const fn weight(&self) -> &ParameterSpec {
2329 &self.weight
2330 }
2331 pub const fn bias(&self) -> Option<&ParameterSpec> {
2333 self.bias.as_ref()
2334 }
2335 pub const fn format(&self) -> &LinearFormatSpec {
2337 &self.format
2338 }
2339 fn validate(&self) -> Result<(), Error> {
2340 self.format.validate_for_weight(&self.weight)?;
2341 let parameters = self.parameters();
2342 for (index, parameter) in parameters.iter().enumerate() {
2343 if parameters[index + 1..]
2344 .iter()
2345 .any(|candidate| candidate.id == parameter.id)
2346 {
2347 return Err(Error::backend(format!(
2348 "grouped projection reuses parameter identity {:?}",
2349 parameter.id
2350 )));
2351 }
2352 }
2353 Ok(())
2354 }
2355
2356 pub fn parameters(&self) -> Vec<&ParameterSpec> {
2358 let mut parameters = vec![&self.weight];
2359 parameters.extend(self.bias.as_ref());
2360 parameters.extend(self.format.scale());
2361 parameters.extend(self.format.affine_bias());
2362 parameters
2363 }
2364}
2365
2366#[derive(Debug, Clone, PartialEq)]
2368#[allow(clippy::large_enum_variant)] #[non_exhaustive]
2370pub enum GatedProductGroupLayout {
2371 Packed {
2374 gate_up: GroupedProjectionSpec,
2376 down: GroupedProjectionSpec,
2378 },
2379 Independent(Vec<GatedProductGroupParameters>),
2381}
2382
2383#[derive(Debug, Clone, PartialEq)]
2385pub struct GroupedGatedProductSpec {
2386 group_count: i32,
2388 input_dimensions: i32,
2390 intermediate_dimensions: i32,
2392 output_dimensions: i32,
2394 policy: GatedProductPolicy,
2396 layout: GatedProductGroupLayout,
2398}
2399
2400impl GroupedGatedProductSpec {
2401 pub fn new(
2403 group_count: i32,
2404 input_dimensions: i32,
2405 intermediate_dimensions: i32,
2406 output_dimensions: i32,
2407 policy: GatedProductPolicy,
2408 layout: GatedProductGroupLayout,
2409 ) -> Result<Self, Error> {
2410 let spec = Self {
2411 group_count,
2412 input_dimensions,
2413 intermediate_dimensions,
2414 output_dimensions,
2415 policy,
2416 layout,
2417 };
2418 spec.validate()?;
2419 Ok(spec)
2420 }
2421 pub fn with_group_geometry(
2423 mut self,
2424 group_count: i32,
2425 intermediate_dimensions: i32,
2426 ) -> Result<Self, Error> {
2427 self.group_count = group_count;
2428 self.intermediate_dimensions = intermediate_dimensions;
2429 self.validate()?;
2430 Ok(self)
2431 }
2432 pub const fn group_count(&self) -> i32 {
2434 self.group_count
2435 }
2436 pub const fn input_dimensions(&self) -> i32 {
2438 self.input_dimensions
2439 }
2440 pub const fn intermediate_dimensions(&self) -> i32 {
2442 self.intermediate_dimensions
2443 }
2444 pub const fn output_dimensions(&self) -> i32 {
2446 self.output_dimensions
2447 }
2448 pub const fn policy(&self) -> GatedProductPolicy {
2450 self.policy
2451 }
2452 pub const fn layout(&self) -> &GatedProductGroupLayout {
2454 &self.layout
2455 }
2456 pub fn validate(&self) -> Result<(), Error> {
2458 for (name, value) in [
2459 ("group_count", self.group_count),
2460 ("input_dimensions", self.input_dimensions),
2461 ("intermediate_dimensions", self.intermediate_dimensions),
2462 ("output_dimensions", self.output_dimensions),
2463 ] {
2464 if value <= 0 {
2465 return Err(Error::backend(format!(
2466 "gated-product group-bank {name} must be positive, got {value}"
2467 )));
2468 }
2469 }
2470 self.policy.validate()?;
2471 if let GatedProductGroupLayout::Independent(groups) = &self.layout {
2472 let expected = usize::try_from(self.group_count).map_err(Error::backend)?;
2473 if groups.len() != expected {
2474 return Err(Error::backend(format!(
2475 "independent gated-product bank has {} groups, expected {expected}",
2476 groups.len()
2477 )));
2478 }
2479 }
2480 let projections = match &self.layout {
2481 GatedProductGroupLayout::Packed { gate_up, down } => vec![gate_up, down],
2482 GatedProductGroupLayout::Independent(groups) => groups
2483 .iter()
2484 .flat_map(|group| [&group.gate, &group.up, &group.down])
2485 .collect(),
2486 };
2487 let mut identities = std::collections::BTreeSet::new();
2488 for projection in projections {
2489 projection.validate()?;
2490 for parameter in projection.parameters() {
2491 let identity = ¶meter.id;
2492 if !identities.insert(identity) {
2493 return Err(Error::backend(format!(
2494 "gated-product group parameter identity {identity} is duplicated"
2495 )));
2496 }
2497 }
2498 }
2499 Ok(())
2500 }
2501}
2502
2503#[derive(Debug, Clone)]
2505pub struct TensorParallelGroupedOutput<T> {
2506 reducible: T,
2508 post_reduce: Option<T>,
2510}
2511
2512impl<T> TensorParallelGroupedOutput<T> {
2513 pub fn new(reducible: T, post_reduce: Option<T>) -> Self {
2515 Self {
2516 reducible,
2517 post_reduce,
2518 }
2519 }
2520 pub const fn reducible(&self) -> &T {
2522 &self.reducible
2523 }
2524 pub const fn post_reduce(&self) -> Option<&T> {
2526 self.post_reduce.as_ref()
2527 }
2528 pub fn into_parts(self) -> (T, Option<T>) {
2530 (self.reducible, self.post_reduce)
2531 }
2532}
2533
2534pub trait GroupedGatedProductOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
2536 fn spec(&self) -> &GroupedGatedProductSpec;
2542
2543 fn forward_grouped(
2545 &mut self,
2546 input: &T,
2547 selections: &GroupSelection<T>,
2548 context: &T::Context,
2549 ) -> Result<T, Error>;
2550}
2551
2552pub trait TensorParallelGroupedGatedProductOperator<T: Tensor>:
2554 GroupedGatedProductOperator<T>
2555{
2556 fn forward_grouped_tensor_parallel(
2559 &mut self,
2560 input: &T,
2561 selections: &GroupSelection<T>,
2562 partitions: usize,
2563 context: &T::Context,
2564 ) -> Result<TensorParallelGroupedOutput<T>, Error>;
2565}
2566
2567#[derive(Debug, Clone, Eq, PartialEq)]
2569pub struct GroupedRelu2Spec {
2570 group_count: i32,
2572 hidden_dimensions: i32,
2574 intermediate_dimensions: i32,
2576 up: GroupedProjectionSpec,
2578 down: GroupedProjectionSpec,
2580}
2581
2582impl GroupedRelu2Spec {
2583 pub fn new(
2585 group_count: i32,
2586 hidden_dimensions: i32,
2587 intermediate_dimensions: i32,
2588 up: GroupedProjectionSpec,
2589 down: GroupedProjectionSpec,
2590 ) -> Result<Self, Error> {
2591 let spec = Self {
2592 group_count,
2593 hidden_dimensions,
2594 intermediate_dimensions,
2595 up,
2596 down,
2597 };
2598 spec.validate()?;
2599 Ok(spec)
2600 }
2601 pub fn with_group_count(mut self, group_count: i32) -> Result<Self, Error> {
2603 self.group_count = group_count;
2604 self.validate()?;
2605 Ok(self)
2606 }
2607 pub const fn group_count(&self) -> i32 {
2609 self.group_count
2610 }
2611 pub const fn hidden_dimensions(&self) -> i32 {
2613 self.hidden_dimensions
2614 }
2615 pub const fn intermediate_dimensions(&self) -> i32 {
2617 self.intermediate_dimensions
2618 }
2619 pub const fn up(&self) -> &GroupedProjectionSpec {
2621 &self.up
2622 }
2623 pub const fn down(&self) -> &GroupedProjectionSpec {
2625 &self.down
2626 }
2627 pub fn validate(&self) -> Result<(), Error> {
2629 if self.group_count <= 0 || self.hidden_dimensions <= 0 || self.intermediate_dimensions <= 0
2630 {
2631 return Err(Error::backend("invalid ReLU2 group-bank geometry"));
2632 }
2633 self.up.validate()?;
2634 self.down.validate()?;
2635 let mut identities = std::collections::BTreeSet::new();
2636 for projection in [&self.up, &self.down] {
2637 for parameter in projection.parameters() {
2638 if !identities.insert(¶meter.id) {
2639 return Err(Error::backend(format!(
2640 "ReLU2 group parameter identity {} is duplicated",
2641 parameter.id
2642 )));
2643 }
2644 }
2645 }
2646 Ok(())
2647 }
2648}
2649
2650pub trait GroupedRelu2Operator<T: Tensor>: Clone + Debug + Parameterized<T> {
2652 fn spec(&self) -> &GroupedRelu2Spec;
2654
2655 fn forward_grouped(
2657 &mut self,
2658 input: &T,
2659 selections: &GroupSelection<T>,
2660 context: &T::Context,
2661 ) -> Result<T, Error>;
2662}
2663
2664pub trait TensorParallelGroupedRelu2Operator<T: Tensor>: GroupedRelu2Operator<T> {
2666 fn forward_grouped_tensor_parallel(
2669 &mut self,
2670 input: &T,
2671 selections: &GroupSelection<T>,
2672 partitions: usize,
2673 context: &T::Context,
2674 ) -> Result<TensorParallelGroupedOutput<T>, Error>;
2675}
2676
2677pub trait GroupedNeuralBackend: NeuralBackend {
2679 type Selector: GroupSelectionOperator<Self::Tensor>;
2681 type GatedProductGroups: GroupedGatedProductOperator<Self::Tensor>;
2683 type Relu2Groups: GroupedRelu2Operator<Self::Tensor>;
2685
2686 fn grouped_linear(
2689 linear: &mut Self::Linear,
2690 input: &Self::Tensor,
2691 groups: i32,
2692 output_per_group: i32,
2693 context: &<Self::Tensor as Tensor>::Context,
2694 ) -> Result<Self::Tensor, Error>;
2695
2696 fn top_k_group_selector(
2698 spec: TopKGroupSelectorSpec,
2699 context: &<Self::Tensor as Tensor>::Context,
2700 ) -> Result<Self::Selector, Error>;
2701
2702 fn grouped_gated_product(
2704 spec: GroupedGatedProductSpec,
2705 context: &<Self::Tensor as Tensor>::Context,
2706 ) -> Result<Self::GatedProductGroups, Error>;
2707
2708 fn grouped_relu2(
2710 spec: GroupedRelu2Spec,
2711 context: &<Self::Tensor as Tensor>::Context,
2712 ) -> Result<Self::Relu2Groups, Error>;
2713
2714 fn joint_group_selection(
2716 input: JointGroupSelectionInput<'_, Self::Tensor>,
2717 context: &<Self::Tensor as Tensor>::Context,
2718 ) -> Result<JointGroupSelection<Self::Tensor>, Error>;
2719}
2720
2721pub trait TensorParallelGroupedNeuralBackend: GroupedNeuralBackend {
2724 fn gated_product_groups_tensor_parallel(
2726 groups: &mut Self::GatedProductGroups,
2727 input: &Self::Tensor,
2728 selections: &GroupSelection<Self::Tensor>,
2729 partitions: usize,
2730 context: &<Self::Tensor as Tensor>::Context,
2731 ) -> Result<TensorParallelGroupedOutput<Self::Tensor>, Error>;
2732
2733 fn relu2_groups_tensor_parallel(
2735 groups: &mut Self::Relu2Groups,
2736 input: &Self::Tensor,
2737 selections: &GroupSelection<Self::Tensor>,
2738 partitions: usize,
2739 context: &<Self::Tensor as Tensor>::Context,
2740 ) -> Result<TensorParallelGroupedOutput<Self::Tensor>, Error>;
2741}
2742
2743impl<B> TensorParallelGroupedNeuralBackend for B
2744where
2745 B: GroupedNeuralBackend,
2746 B::GatedProductGroups: TensorParallelGroupedGatedProductOperator<B::Tensor>,
2747 B::Relu2Groups: TensorParallelGroupedRelu2Operator<B::Tensor>,
2748{
2749 fn gated_product_groups_tensor_parallel(
2750 groups: &mut Self::GatedProductGroups,
2751 input: &Self::Tensor,
2752 selections: &GroupSelection<Self::Tensor>,
2753 partitions: usize,
2754 context: &<Self::Tensor as Tensor>::Context,
2755 ) -> Result<TensorParallelGroupedOutput<Self::Tensor>, Error> {
2756 groups.forward_grouped_tensor_parallel(input, selections, partitions, context)
2757 }
2758
2759 fn relu2_groups_tensor_parallel(
2760 groups: &mut Self::Relu2Groups,
2761 input: &Self::Tensor,
2762 selections: &GroupSelection<Self::Tensor>,
2763 partitions: usize,
2764 context: &<Self::Tensor as Tensor>::Context,
2765 ) -> Result<TensorParallelGroupedOutput<Self::Tensor>, Error> {
2766 groups.forward_grouped_tensor_parallel(input, selections, partitions, context)
2767 }
2768}
2769
2770#[derive(Debug, Clone)]
2772pub struct HyperConnectionSpec {
2773 pub streams: i32,
2775 pub hidden_size: i32,
2777 pub sinkhorn_iterations: usize,
2779 pub epsilon: f32,
2781 pub function: ParameterSpec,
2783 pub base: ParameterSpec,
2785 pub scale: ParameterSpec,
2787}
2788
2789impl HyperConnectionSpec {
2790 pub fn validate(&self) -> Result<(), Error> {
2792 if self.streams <= 0 || self.hidden_size <= 0 {
2793 return Err(Error::backend(
2794 "hyper-connection streams and hidden size must be positive",
2795 ));
2796 }
2797 if self.sinkhorn_iterations == 0 {
2798 return Err(Error::backend(
2799 "hyper-connection Sinkhorn iteration count must be positive",
2800 ));
2801 }
2802 if !self.epsilon.is_finite() || self.epsilon <= 0.0 {
2803 return Err(Error::backend(
2804 "hyper-connection epsilon must be finite and positive",
2805 ));
2806 }
2807 Ok(())
2808 }
2809}
2810
2811#[derive(Debug, Clone)]
2813pub struct HyperHeadSpec {
2814 pub streams: i32,
2816 pub hidden_size: i32,
2818 pub norm_epsilon: f32,
2820 pub epsilon: f32,
2822 pub function: ParameterSpec,
2824 pub base: ParameterSpec,
2826 pub scale: ParameterSpec,
2828}
2829
2830impl HyperHeadSpec {
2831 pub fn validate(&self) -> Result<(), Error> {
2833 if self.streams <= 0 || self.hidden_size <= 0 {
2834 return Err(Error::backend(
2835 "hyper-head streams and hidden size must be positive",
2836 ));
2837 }
2838 if !self.norm_epsilon.is_finite()
2839 || self.norm_epsilon <= 0.0
2840 || !self.epsilon.is_finite()
2841 || self.epsilon <= 0.0
2842 {
2843 return Err(Error::backend(
2844 "hyper-head epsilons must be finite and positive",
2845 ));
2846 }
2847 Ok(())
2848 }
2849}
2850
2851#[derive(Debug, Clone)]
2853pub struct HyperConnectionState<T> {
2854 pub collapsed: T,
2856 pub pre: T,
2858 pub post: T,
2860 pub combination: T,
2862}
2863
2864pub trait HyperConnectionOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
2866 fn collapse(
2869 &mut self,
2870 residual: &T,
2871 norm_epsilon: f32,
2872 context: &T::Context,
2873 ) -> Result<HyperConnectionState<T>, Error>;
2874
2875 fn expand(
2877 &mut self,
2878 sublayer: &T,
2879 residual: &T,
2880 state: &HyperConnectionState<T>,
2881 context: &T::Context,
2882 ) -> Result<T, Error>;
2883}
2884
2885pub trait HyperHeadOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
2887 fn forward(&mut self, residual: &T, context: &T::Context) -> Result<T, Error>;
2889}
2890
2891pub trait HyperNeuralBackend: NeuralBackend {
2893 type HyperConnection: HyperConnectionOperator<Self::Tensor>;
2895 type HyperHead: HyperHeadOperator<Self::Tensor>;
2897
2898 fn hyper_connection(
2900 spec: HyperConnectionSpec,
2901 context: &<Self::Tensor as Tensor>::Context,
2902 ) -> Result<Self::HyperConnection, Error>;
2903
2904 fn hyper_head(
2906 spec: HyperHeadSpec,
2907 context: &<Self::Tensor as Tensor>::Context,
2908 ) -> Result<Self::HyperHead, Error>;
2909}
2910
2911#[derive(Debug, Clone, Parameterized)]
2913#[parameterized(tensor = "B::Tensor")]
2914pub struct HyperConnection<B: HyperNeuralBackend> {
2915 operator: B::HyperConnection,
2916}
2917
2918impl<B: HyperNeuralBackend> HyperConnection<B> {
2919 pub fn new(
2921 spec: HyperConnectionSpec,
2922 context: &<B::Tensor as Tensor>::Context,
2923 ) -> Result<Self, Error> {
2924 spec.validate()?;
2925 Ok(Self {
2926 operator: B::hyper_connection(spec, context)?,
2927 })
2928 }
2929
2930 pub fn collapse(
2932 &mut self,
2933 residual: &B::Tensor,
2934 norm_epsilon: f32,
2935 context: &<B::Tensor as Tensor>::Context,
2936 ) -> Result<HyperConnectionState<B::Tensor>, Error> {
2937 self.operator.collapse(residual, norm_epsilon, context)
2938 }
2939
2940 pub fn expand(
2942 &mut self,
2943 sublayer: &B::Tensor,
2944 residual: &B::Tensor,
2945 state: &HyperConnectionState<B::Tensor>,
2946 context: &<B::Tensor as Tensor>::Context,
2947 ) -> Result<B::Tensor, Error> {
2948 self.operator.expand(sublayer, residual, state, context)
2949 }
2950}
2951
2952#[derive(Debug, Parameterized)]
2954#[parameterized(tensor = "B::Tensor")]
2955pub struct HyperHead<B: HyperNeuralBackend> {
2956 operator: B::HyperHead,
2957}
2958
2959impl<B: HyperNeuralBackend> Clone for HyperHead<B> {
2960 fn clone(&self) -> Self {
2961 Self {
2962 operator: self.operator.clone(),
2963 }
2964 }
2965}
2966
2967impl<B: HyperNeuralBackend> HyperHead<B> {
2968 pub fn new(
2970 spec: HyperHeadSpec,
2971 context: &<B::Tensor as Tensor>::Context,
2972 ) -> Result<Self, Error> {
2973 spec.validate()?;
2974 Ok(Self {
2975 operator: B::hyper_head(spec, context)?,
2976 })
2977 }
2978
2979 pub fn forward(
2981 &mut self,
2982 residual: &B::Tensor,
2983 context: &<B::Tensor as Tensor>::Context,
2984 ) -> Result<B::Tensor, Error> {
2985 self.operator.forward(residual, context)
2986 }
2987}
2988
2989#[derive(Debug)]
2996pub struct AttentionRequest<'a, T> {
2997 pub queries: T,
2999 pub keys: T,
3001 pub values: T,
3003 pub scale: f32,
3005 pub mask: Option<&'a T>,
3007 pub sinks: Option<&'a T>,
3009}
3010
3011impl<T: Tensor> AttentionRequest<'_, T> {
3012 pub fn validate(&self) -> Result<(), Error> {
3014 let queries = self.queries.shape();
3015 let keys = self.keys.shape();
3016 let values = self.values.shape();
3017 if queries.len() != 4
3018 || keys.len() != 4
3019 || values.len() != 4
3020 || queries[0] != keys[0]
3021 || keys[..3] != values[..3]
3022 || queries[3] != keys[3]
3023 || queries[1] <= 0
3024 || keys[1] <= 0
3025 || queries[1] % keys[1] != 0
3026 || queries[2] <= 0
3027 || keys[2] <= 0
3028 || values[3] <= 0
3029 || !self.scale.is_finite()
3030 || self.scale <= 0.0
3031 {
3032 return Err(Error::backend(format!(
3033 "invalid attention request geometry queries={queries:?} keys={keys:?} values={values:?} scale={}",
3034 self.scale
3035 )));
3036 }
3037 if let Some(sinks) = self.sinks {
3038 if sinks.shape() != [queries[1]] {
3039 return Err(Error::backend(format!(
3040 "attention sinks require shape [{}], got {:?}",
3041 queries[1],
3042 sinks.shape()
3043 )));
3044 }
3045 }
3046 Ok(())
3047 }
3048}
3049
3050pub trait AttentionCache<T: Tensor> {
3052 fn offset(&self) -> i32;
3054 fn max_size(&self) -> Option<i32>;
3056 fn update_for_attention(
3058 &mut self,
3059 keys: T,
3060 values: T,
3061 context: &T::Context,
3062 ) -> Result<(T, T), Error>;
3063 fn attention(
3065 &mut self,
3066 request: AttentionRequest<'_, T>,
3067 context: &T::Context,
3068 ) -> Result<T, Error>;
3069}
3070
3071pub trait AuxiliaryConvolutionState<T: Tensor>: AttentionCache<T> {
3076 fn convolution_state(&mut self, slot: u32) -> Result<&mut Option<T>, Error>;
3078}
3079
3080#[derive(Debug, Clone)]
3082pub struct CompressedAttentionState<T> {
3083 pub latent: T,
3085 pub rotary: T,
3087}
3088
3089#[derive(Debug, Clone)]
3091pub enum CompressedAttentionView<T> {
3092 Resident(CompressedAttentionState<T>),
3094 Paged {
3097 appended: CompressedAttentionState<T>,
3099 },
3100}
3101
3102impl<T> CompressedAttentionView<T> {
3103 pub const fn resident(&self) -> Option<&CompressedAttentionState<T>> {
3105 match self {
3106 Self::Resident(state) => Some(state),
3107 Self::Paged { .. } => None,
3108 }
3109 }
3110
3111 pub const fn observable(&self) -> &CompressedAttentionState<T> {
3113 match self {
3114 Self::Resident(state) | Self::Paged { appended: state } => state,
3115 }
3116 }
3117
3118 pub const fn is_paged(&self) -> bool {
3120 matches!(self, Self::Paged { .. })
3121 }
3122}
3123
3124#[derive(Debug, Clone)]
3126pub struct CompressedAttentionBlock<T> {
3127 pub start: i64,
3129 pub end: i64,
3131 pub state: CompressedAttentionState<T>,
3133}
3134
3135#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
3137pub struct CompressedAttentionScan {
3138 pub blocks: u64,
3140 pub bytes: u64,
3142 pub reconstruction_scratch_bytes: u64,
3144}
3145
3146#[derive(Debug, Clone, Copy)]
3148pub struct BlockwiseAttentionSpec<'a, T> {
3149 pub queries: &'a T,
3151 pub scale: f32,
3153 pub mask: Option<&'a T>,
3155 pub query_start: i64,
3157 pub context_end: i64,
3159 pub sliding_window: Option<i32>,
3161 pub prefix_tokens: i64,
3163 pub sinks: Option<&'a T>,
3165}
3166
3167pub trait BlockwiseAttentionBackend: NeuralBackend {
3171 type BlockwiseAccumulator;
3173
3174 fn begin_blockwise_attention(
3176 spec: BlockwiseAttentionSpec<'_, Self::Tensor>,
3177 context: &<Self::Tensor as Tensor>::Context,
3178 ) -> Result<Self::BlockwiseAccumulator, Error>;
3179
3180 fn accumulate_blockwise_attention(
3182 accumulator: &mut Self::BlockwiseAccumulator,
3183 start: i64,
3184 end: i64,
3185 keys: Self::Tensor,
3186 values: Self::Tensor,
3187 context: &<Self::Tensor as Tensor>::Context,
3188 ) -> Result<u64, Error>;
3189
3190 fn finish_blockwise_attention(
3192 accumulator: Self::BlockwiseAccumulator,
3193 context: &<Self::Tensor as Tensor>::Context,
3194 ) -> Result<Self::Tensor, Error>;
3195}
3196
3197pub trait CompressedAttentionCache<T: Tensor>: Debug {
3202 type Checkpoint: Clone + Debug;
3204
3205 fn offset(&self) -> i32;
3207 fn is_paged(&self) -> bool;
3209 fn append(
3211 &mut self,
3212 state: CompressedAttentionState<T>,
3213 context: &T::Context,
3214 ) -> Result<CompressedAttentionView<T>, Error>;
3215 fn visit_blocks<F>(
3218 &mut self,
3219 query_tokens: i32,
3220 context: &T::Context,
3221 visitor: F,
3222 ) -> Result<CompressedAttentionScan, Error>
3223 where
3224 F: FnMut(CompressedAttentionBlock<T>) -> Result<u64, Error>;
3225 fn checkpoint(&self) -> Self::Checkpoint;
3227 fn restore(&mut self, checkpoint: &Self::Checkpoint, context: &T::Context)
3229 -> Result<(), Error>;
3230 fn finalize(&mut self) -> Result<(), Error>;
3232 fn clear(&mut self) -> Result<(), Error>;
3234}
3235
3236#[derive(Debug, Clone)]
3238pub struct PoolingWindows<T> {
3239 pub values: T,
3241 pub gates: T,
3243 pub base_position: i32,
3245}
3246
3247#[derive(Debug, Clone)]
3249pub struct PoolingOverlap<T> {
3250 pub values: Option<T>,
3252 pub gates: Option<T>,
3254}
3255
3256pub trait PoolingAttentionCache<T: Tensor>: Debug {
3263 type Checkpoint: Clone + Debug;
3265
3266 fn offset(&self) -> i32;
3268 fn pooling_ratio(&self, stream: u32) -> Option<i32>;
3270 fn append_local(&mut self, keys: T, context: &T::Context) -> Result<T, Error>;
3273 fn local_mask(&self, query_tokens: i32, offset: i32, context: &T::Context) -> Result<T, Error>;
3275 fn accumulate_pooling_windows(
3277 &mut self,
3278 stream: u32,
3279 values: T,
3280 gates: T,
3281 absolute_offset: i32,
3282 context: &T::Context,
3283 ) -> Result<PoolingWindows<T>, Error>;
3284 fn replace_pooling_overlap(
3286 &mut self,
3287 stream: u32,
3288 values: T,
3289 gates: T,
3290 ) -> Result<PoolingOverlap<T>, Error>;
3291 fn append_pooled(&mut self, stream: u32, values: T, context: &T::Context) -> Result<T, Error>;
3293 fn pooling_mask(
3295 &self,
3296 stream: u32,
3297 query_tokens: i32,
3298 offset: i32,
3299 context: &T::Context,
3300 ) -> Result<Option<T>, Error>;
3301 fn checkpoint(&self) -> Self::Checkpoint;
3303 fn restore(&mut self, checkpoint: &Self::Checkpoint, context: &T::Context)
3305 -> Result<(), Error>;
3306 fn finalize(&mut self) -> Result<(), Error>;
3308 fn clear(&mut self) -> Result<(), Error>;
3310}
3311
3312#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
3319pub struct NeuralOperatorCapabilities(u64);
3320
3321impl NeuralOperatorCapabilities {
3322 pub const NONE: Self = Self(0);
3324 pub const GELU_APPROXIMATE: Self = Self(1 << 0);
3326 pub const SIGMOID: Self = Self(1 << 1);
3328 pub const SOFTPLUS: Self = Self(1 << 2);
3330 pub const EXP: Self = Self(1 << 3);
3332 pub const GATED_GROUP_RMS_NORM: Self = Self(1 << 4);
3334 pub const L2_NORMALIZE: Self = Self(1 << 5);
3336 pub const SILU_GATED_GROUP_RMS_NORM: Self = Self(1 << 6);
3338 pub const SEGMENTED_ATTENTION: Self = Self(1 << 7);
3340 pub const GATED_DELTA_SCAN: Self = Self(1 << 8);
3342 pub const SELECTIVE_STATE_SPACE_SCAN: Self = Self(1 << 9);
3344 pub const INDEXED_ATTENTION: Self = Self(1 << 10);
3346 pub const POOLED_ATTENTION: Self = Self(1 << 11);
3348 pub const POOLED_POSITION_SELECTION: Self = Self(1 << 12);
3350 pub const POOLED_MASK_GATHER: Self = Self(1 << 13);
3352 pub const ATTENTION_SINKS: Self = Self(1 << 14);
3354 pub const RELATIVE_ATTENTION: Self = Self(1 << 15);
3356 pub const JOINT_GROUP_SELECTION: Self = Self(1 << 16);
3358 pub const RMS_NORM_WITHOUT_WEIGHT: Self = Self(1 << 17);
3360 pub const GROUPED_LINEAR: Self = Self(1 << 18);
3362 pub const SUM_PARALLEL: Self = Self(1 << 19);
3364 pub const UNLOADED_I32: Self = Self(1 << 20);
3366 pub const FROM_I32_SLICE: Self = Self(1 << 21);
3368 pub const TO_F32_VEC: Self = Self(1 << 22);
3370 pub const TO_I32_VEC: Self = Self(1 << 23);
3372 pub const FULL_F32: Self = Self(1 << 24);
3374 pub const FULL_I32: Self = Self(1 << 25);
3376 pub const TANH: Self = Self(1 << 26);
3378 pub const CLIP: Self = Self(1 << 27);
3380 pub const SOFTMAX_AXIS: Self = Self(1 << 28);
3382 pub const BROADCAST_TO: Self = Self(1 << 29);
3384 pub const ZEROS_LIKE: Self = Self(1 << 30);
3386 pub const EQUAL_I32: Self = Self(1 << 31);
3388 pub const LOGICAL_OR: Self = Self(1 << 32);
3390 pub const WHERE_CONDITION: Self = Self(1 << 33);
3392 pub const MASKED_SCATTER: Self = Self(1 << 34);
3394 pub const ROPE_WITH_FREQUENCIES: Self = Self(1 << 35);
3396 pub const CONV2D: Self = Self(1 << 36);
3398 pub const MULTI_AXIS_ROTARY_EMBEDDINGS: Self = Self(1 << 37);
3400 pub const MASKED_OUTPUT_PROJECTION: Self = Self(1 << 38);
3402 pub const ALL: Self = Self((1 << 39) - 1);
3404
3405 pub const fn union(self, other: Self) -> Self {
3407 Self(self.0 | other.0)
3408 }
3409
3410 pub const fn contains(self, required: Self) -> bool {
3412 self.0 & required.0 == required.0
3413 }
3414
3415 pub fn missing_capability_names(self, required: Self) -> Vec<&'static str> {
3417 const NAMES: &[(NeuralOperatorCapabilities, &str)] = &[
3418 (
3419 NeuralOperatorCapabilities::GELU_APPROXIMATE,
3420 "gelu_approximate",
3421 ),
3422 (NeuralOperatorCapabilities::SIGMOID, "sigmoid"),
3423 (NeuralOperatorCapabilities::SOFTPLUS, "softplus"),
3424 (NeuralOperatorCapabilities::EXP, "exp"),
3425 (
3426 NeuralOperatorCapabilities::GATED_GROUP_RMS_NORM,
3427 "gated_group_rms_norm",
3428 ),
3429 (NeuralOperatorCapabilities::L2_NORMALIZE, "l2_normalize"),
3430 (
3431 NeuralOperatorCapabilities::SILU_GATED_GROUP_RMS_NORM,
3432 "silu_gated_group_rms_norm",
3433 ),
3434 (
3435 NeuralOperatorCapabilities::SEGMENTED_ATTENTION,
3436 "segmented_attention",
3437 ),
3438 (
3439 NeuralOperatorCapabilities::GATED_DELTA_SCAN,
3440 "gated_delta_scan",
3441 ),
3442 (
3443 NeuralOperatorCapabilities::SELECTIVE_STATE_SPACE_SCAN,
3444 "selective_state_space_scan",
3445 ),
3446 (
3447 NeuralOperatorCapabilities::INDEXED_ATTENTION,
3448 "indexed_attention",
3449 ),
3450 (
3451 NeuralOperatorCapabilities::POOLED_ATTENTION,
3452 "pooled_attention",
3453 ),
3454 (
3455 NeuralOperatorCapabilities::POOLED_POSITION_SELECTION,
3456 "select_pooled_positions",
3457 ),
3458 (
3459 NeuralOperatorCapabilities::POOLED_MASK_GATHER,
3460 "gather_pooled_mask",
3461 ),
3462 (
3463 NeuralOperatorCapabilities::ATTENTION_SINKS,
3464 "attention_sinks",
3465 ),
3466 (
3467 NeuralOperatorCapabilities::RELATIVE_ATTENTION,
3468 "relative_attention",
3469 ),
3470 (
3471 NeuralOperatorCapabilities::JOINT_GROUP_SELECTION,
3472 "joint_group_selection",
3473 ),
3474 (
3475 NeuralOperatorCapabilities::RMS_NORM_WITHOUT_WEIGHT,
3476 "rms_norm_without_weight",
3477 ),
3478 (NeuralOperatorCapabilities::GROUPED_LINEAR, "grouped_linear"),
3479 (NeuralOperatorCapabilities::SUM_PARALLEL, "sum_parallel"),
3480 (NeuralOperatorCapabilities::UNLOADED_I32, "unloaded_i32"),
3481 (NeuralOperatorCapabilities::FROM_I32_SLICE, "from_i32_slice"),
3482 (NeuralOperatorCapabilities::TO_F32_VEC, "to_f32_vec"),
3483 (NeuralOperatorCapabilities::TO_I32_VEC, "to_i32_vec"),
3484 (NeuralOperatorCapabilities::FULL_F32, "full_f32"),
3485 (NeuralOperatorCapabilities::FULL_I32, "full_i32"),
3486 (NeuralOperatorCapabilities::TANH, "tanh"),
3487 (NeuralOperatorCapabilities::CLIP, "clip"),
3488 (NeuralOperatorCapabilities::SOFTMAX_AXIS, "softmax_axis"),
3489 (NeuralOperatorCapabilities::BROADCAST_TO, "broadcast_to"),
3490 (NeuralOperatorCapabilities::ZEROS_LIKE, "zeros_like"),
3491 (NeuralOperatorCapabilities::EQUAL_I32, "equal_i32"),
3492 (NeuralOperatorCapabilities::LOGICAL_OR, "logical_or"),
3493 (
3494 NeuralOperatorCapabilities::WHERE_CONDITION,
3495 "where_condition",
3496 ),
3497 (NeuralOperatorCapabilities::MASKED_SCATTER, "masked_scatter"),
3498 (
3499 NeuralOperatorCapabilities::ROPE_WITH_FREQUENCIES,
3500 "rope_with_frequencies",
3501 ),
3502 (NeuralOperatorCapabilities::CONV2D, "conv2d"),
3503 (
3504 NeuralOperatorCapabilities::MULTI_AXIS_ROTARY_EMBEDDINGS,
3505 "multi_axis_rotary_embeddings",
3506 ),
3507 (
3508 NeuralOperatorCapabilities::MASKED_OUTPUT_PROJECTION,
3509 "masked_output_projection",
3510 ),
3511 ];
3512 NAMES
3513 .iter()
3514 .filter_map(|(capability, name)| {
3515 (required.contains(*capability) && !self.contains(*capability)).then_some(*name)
3516 })
3517 .collect()
3518 }
3519}
3520
3521#[cfg(test)]
3522mod neural_operator_capability_tests {
3523 use super::NeuralOperatorCapabilities as C;
3524
3525 #[test]
3526 fn all_includes_every_fail_closed_tensor_operation() {
3527 for (capability, name) in [
3528 (C::UNLOADED_I32, "unloaded_i32"),
3529 (C::FROM_I32_SLICE, "from_i32_slice"),
3530 (C::TO_F32_VEC, "to_f32_vec"),
3531 (C::TO_I32_VEC, "to_i32_vec"),
3532 (C::FULL_F32, "full_f32"),
3533 (C::FULL_I32, "full_i32"),
3534 (C::TANH, "tanh"),
3535 (C::CLIP, "clip"),
3536 (C::SOFTMAX_AXIS, "softmax_axis"),
3537 (C::BROADCAST_TO, "broadcast_to"),
3538 (C::ZEROS_LIKE, "zeros_like"),
3539 (C::EQUAL_I32, "equal_i32"),
3540 (C::LOGICAL_OR, "logical_or"),
3541 (C::WHERE_CONDITION, "where_condition"),
3542 (C::MASKED_SCATTER, "masked_scatter"),
3543 (C::ROPE_WITH_FREQUENCIES, "rope_with_frequencies"),
3544 (C::CONV2D, "conv2d"),
3545 (
3546 C::MULTI_AXIS_ROTARY_EMBEDDINGS,
3547 "multi_axis_rotary_embeddings",
3548 ),
3549 (C::MASKED_OUTPUT_PROJECTION, "masked_output_projection"),
3550 ] {
3551 assert!(C::ALL.contains(capability));
3552 assert_eq!(C::NONE.missing_capability_names(capability), [name]);
3553 }
3554 }
3555}
3556
3557pub trait NeuralBackend: Sized + 'static {
3562 const OPERATOR_CAPABILITIES: NeuralOperatorCapabilities = NeuralOperatorCapabilities::NONE;
3564
3565 type Tensor: Tensor;
3567 type Linear: LinearOperator<Self::Tensor>;
3569 type Embedding: EmbeddingOperator<Self::Tensor>;
3571 type Normalization: NormalizationOperator<Self::Tensor>;
3573 type Rotary: RotaryOperator<Self::Tensor>;
3575 type ParallelContext: ?Sized;
3577
3578 fn require_operator_capabilities(
3581 architecture: &'static str,
3582 required: NeuralOperatorCapabilities,
3583 ) -> Result<(), Error> {
3584 let available = Self::OPERATOR_CAPABILITIES;
3585 if available.contains(required) {
3586 return Ok(());
3587 }
3588 Err(Error::backend(format!(
3589 "{architecture} requires unsupported backend operators: {}",
3590 available.missing_capability_names(required).join(", ")
3591 )))
3592 }
3593
3594 fn linear(
3596 spec: LinearSpec,
3597 context: &<Self::Tensor as Tensor>::Context,
3598 ) -> Result<Self::Linear, Error>;
3599 fn embedding(
3601 spec: EmbeddingSpec,
3602 context: &<Self::Tensor as Tensor>::Context,
3603 ) -> Result<Self::Embedding, Error>;
3604 fn normalization(
3606 spec: NormalizationConstructionSpec,
3607 context: &<Self::Tensor as Tensor>::Context,
3608 ) -> Result<Self::Normalization, Error>;
3609 fn rotary(
3611 spec: RotarySpec,
3612 context: &<Self::Tensor as Tensor>::Context,
3613 ) -> Result<Self::Rotary, Error>;
3614 fn silu(
3616 input: Self::Tensor,
3617 context: &<Self::Tensor as Tensor>::Context,
3618 ) -> Result<Self::Tensor, Error>;
3619 fn gelu_approximate(
3621 input: Self::Tensor,
3622 context: &<Self::Tensor as Tensor>::Context,
3623 ) -> Result<Self::Tensor, Error> {
3624 let _ = (input, context);
3625 Err(Error::backend(
3626 "approximate GELU is not implemented by this backend",
3627 ))
3628 }
3629 fn sigmoid(
3631 input: Self::Tensor,
3632 context: &<Self::Tensor as Tensor>::Context,
3633 ) -> Result<Self::Tensor, Error> {
3634 let _ = (input, context);
3635 Err(Error::backend("sigmoid is not implemented by this backend"))
3636 }
3637 fn softplus(
3639 input: Self::Tensor,
3640 context: &<Self::Tensor as Tensor>::Context,
3641 ) -> Result<Self::Tensor, Error> {
3642 let _ = (input, context);
3643 Err(Error::backend(
3644 "softplus is not implemented by this backend",
3645 ))
3646 }
3647 fn exp(
3649 input: Self::Tensor,
3650 context: &<Self::Tensor as Tensor>::Context,
3651 ) -> Result<Self::Tensor, Error> {
3652 let _ = (input, context);
3653 Err(Error::backend(
3654 "exponential is not implemented by this backend",
3655 ))
3656 }
3657 fn gated_group_rms_norm(
3661 input: &Self::Tensor,
3662 gate: &Self::Tensor,
3663 weight: &Self::Tensor,
3664 groups: i32,
3665 epsilon: f32,
3666 context: &<Self::Tensor as Tensor>::Context,
3667 ) -> Result<Self::Tensor, Error> {
3668 let _ = (input, gate, weight, groups, epsilon, context);
3669 Err(Error::backend(
3670 "gated grouped RMS normalization is not implemented by this backend",
3671 ))
3672 }
3673 fn l2_normalize(
3676 input: &Self::Tensor,
3677 epsilon: f32,
3678 context: &<Self::Tensor as Tensor>::Context,
3679 ) -> Result<Self::Tensor, Error> {
3680 let _ = (input, epsilon, context);
3681 Err(Error::backend(
3682 "L2 normalization is not implemented by this backend",
3683 ))
3684 }
3685 fn silu_gated_group_rms_norm(
3690 input: &Self::Tensor,
3691 gate: &Self::Tensor,
3692 weight: &Self::Tensor,
3693 groups: i32,
3694 epsilon: f32,
3695 context: &<Self::Tensor as Tensor>::Context,
3696 ) -> Result<Self::Tensor, Error> {
3697 let _ = (input, gate, weight, groups, epsilon, context);
3698 Err(Error::backend(
3699 "SiLU-gated grouped RMS normalization is not implemented by this backend",
3700 ))
3701 }
3702 fn expand_heads(
3705 input: &Self::Tensor,
3706 expansion: HeadExpansion,
3707 context: &<Self::Tensor as Tensor>::Context,
3708 ) -> Result<Self::Tensor, Error> {
3709 expansion.validate(input)?;
3710 if expansion.source_heads == expansion.target_heads {
3711 return Ok(input.clone());
3712 }
3713 let mut expanded_shape = input.shape().to_vec();
3714 expanded_shape.insert(expansion.axis + 1, 1);
3715 let expanded = input.reshape(&expanded_shape, context)?;
3716 expanded_shape[expansion.axis + 1] = expansion.repeats();
3717 let expanded = expanded.broadcast_to(&expanded_shape, context)?;
3718 expanded_shape[expansion.axis] = expansion.target_heads;
3719 expanded_shape.remove(expansion.axis + 1);
3720 expanded.reshape(&expanded_shape, context)
3721 }
3722 fn segmented_attention(
3725 input: SegmentedAttentionInput<'_, Self::Tensor>,
3726 context: &<Self::Tensor as Tensor>::Context,
3727 ) -> Result<Self::Tensor, Error> {
3728 input.validate()?;
3729 let _ = context;
3730 Err(Error::backend(
3731 "segmented attention is not implemented by this backend",
3732 ))
3733 }
3734 fn add_residual(
3736 residual: &Self::Tensor,
3737 branch: &Self::Tensor,
3738 fp32: bool,
3739 context: &<Self::Tensor as Tensor>::Context,
3740 ) -> Result<Self::Tensor, Error> {
3741 let _ = fp32;
3742 residual.add(branch, context)
3743 }
3744 fn gated_delta_scan(
3750 input: GatedDeltaScanInput<'_, Self::Tensor>,
3751 context: &<Self::Tensor as Tensor>::Context,
3752 ) -> Result<GatedDeltaScanOutput<Self::Tensor>, Error> {
3753 let _ = (input, context);
3754 Err(Error::backend(
3755 "gated-delta scan is not implemented by this backend",
3756 ))
3757 }
3758 fn selective_state_space_scan(
3760 input: SelectiveStateSpaceScanInput<'_, Self::Tensor>,
3761 context: &<Self::Tensor as Tensor>::Context,
3762 ) -> Result<SelectiveStateSpaceScanOutput<Self::Tensor>, Error> {
3763 let _ = (input, context);
3764 Err(Error::backend(
3765 "selective state-space scan is not implemented by this backend",
3766 ))
3767 }
3768 fn indexed_attention(
3772 input: IndexedAttentionInput<'_, Self::Tensor>,
3773 context: &<Self::Tensor as Tensor>::Context,
3774 ) -> Result<Self::Tensor, Error> {
3775 let _ = (input, context);
3776 Err(Error::backend(
3777 "indexed attention is not implemented by this backend",
3778 ))
3779 }
3780 fn pooled_attention(
3782 input: PooledAttentionInput<'_, Self::Tensor>,
3783 context: &<Self::Tensor as Tensor>::Context,
3784 ) -> Result<Self::Tensor, Error> {
3785 let _ = (input, context);
3786 Err(Error::backend(
3787 "pooled attention is not implemented by this backend",
3788 ))
3789 }
3790 fn select_pooled_positions(
3792 input: PooledPositionInput<'_, Self::Tensor>,
3793 context: &<Self::Tensor as Tensor>::Context,
3794 ) -> Result<Self::Tensor, Error> {
3795 let _ = (input, context);
3796 Err(Error::backend(
3797 "pooled-position selection is not implemented by this backend",
3798 ))
3799 }
3800 fn gather_pooled_mask(
3802 mask: &Self::Tensor,
3803 selected_positions: &Self::Tensor,
3804 context: &<Self::Tensor as Tensor>::Context,
3805 ) -> Result<Self::Tensor, Error> {
3806 let _ = (mask, selected_positions, context);
3807 Err(Error::backend(
3808 "pooled-mask gathering is not implemented by this backend",
3809 ))
3810 }
3811 fn attention_with_sinks(
3813 request: AttentionRequest<'_, Self::Tensor>,
3814 context: &<Self::Tensor as Tensor>::Context,
3815 ) -> Result<Self::Tensor, Error> {
3816 request.validate()?;
3817 if request.sinks.is_some() {
3818 return Err(Error::backend(
3819 "attention sinks are not implemented by this backend",
3820 ));
3821 }
3822 Self::attention(
3823 request.queries,
3824 request.keys,
3825 request.values,
3826 request.scale,
3827 request.mask,
3828 context,
3829 )
3830 }
3831 fn sliding_window_attention_with_sinks(
3833 request: AttentionRequest<'_, Self::Tensor>,
3834 window: i32,
3835 position_offset: i32,
3836 context: &<Self::Tensor as Tensor>::Context,
3837 ) -> Result<Self::Tensor, Error> {
3838 request.validate()?;
3839 if request.sinks.is_some() {
3840 return Err(Error::backend(
3841 "sliding-window attention sinks are not implemented by this backend",
3842 ));
3843 }
3844 Self::sliding_window_attention(
3845 request.queries,
3846 request.keys,
3847 request.values,
3848 request.scale,
3849 window,
3850 position_offset,
3851 context,
3852 )
3853 }
3854 fn relative_attention(
3856 input: RelativeAttentionInput<'_, Self::Tensor>,
3857 context: &<Self::Tensor as Tensor>::Context,
3858 ) -> Result<Self::Tensor, Error> {
3859 let _ = (input, context);
3860 Err(Error::backend(
3861 "relative-profile attention is not implemented by this backend",
3862 ))
3863 }
3864 fn rms_norm_without_weight(
3867 input: &Self::Tensor,
3868 epsilon: f32,
3869 context: &<Self::Tensor as Tensor>::Context,
3870 ) -> Result<Self::Tensor, Error> {
3871 let _ = (input, epsilon, context);
3872 Err(Error::backend(
3873 "weightless RMS normalization is not implemented by this backend",
3874 ))
3875 }
3876 fn rms_norm_with_weight(
3883 input: &Self::Tensor,
3884 weight: &Self::Tensor,
3885 epsilon: f32,
3886 context: &<Self::Tensor as Tensor>::Context,
3887 ) -> Result<Self::Tensor, Error> {
3888 Self::rms_norm_without_weight(input, epsilon, context)?.multiply(weight, context)
3889 }
3890 fn gated_product(
3892 gate: Self::Tensor,
3893 up: Self::Tensor,
3894 policy: GatedProductPolicy,
3895 context: &<Self::Tensor as Tensor>::Context,
3896 ) -> Result<Self::Tensor, Error>;
3897 fn attention(
3899 queries: Self::Tensor,
3900 keys: Self::Tensor,
3901 values: Self::Tensor,
3902 scale: f32,
3903 mask: Option<&Self::Tensor>,
3904 context: &<Self::Tensor as Tensor>::Context,
3905 ) -> Result<Self::Tensor, Error>;
3906 #[allow(clippy::too_many_arguments)]
3908 fn sliding_window_attention(
3909 queries: Self::Tensor,
3910 keys: Self::Tensor,
3911 values: Self::Tensor,
3912 scale: f32,
3913 window: i32,
3914 position_offset: i32,
3915 context: &<Self::Tensor as Tensor>::Context,
3916 ) -> Result<Self::Tensor, Error>;
3917 fn causal_mask(
3924 sequence: i32,
3925 offset: i32,
3926 window: Option<i32>,
3927 context: &<Self::Tensor as Tensor>::Context,
3928 ) -> Result<Self::Tensor, Error>;
3929 fn row_parallel_linear(
3931 linear: &mut Self::Linear,
3932 input: &Self::Tensor,
3933 parallel: &Self::ParallelContext,
3934 context: &<Self::Tensor as Tensor>::Context,
3935 ) -> Result<Self::Tensor, Error>;
3936 fn parallel_size(_parallel: &Self::ParallelContext) -> usize {
3938 1
3939 }
3940}
3941
3942pub trait DistributedNeuralBackend: NeuralBackend {
3948 fn vocabulary_parallel_embedding(
3950 spec: EmbeddingSpec,
3951 range: VocabularyParallelRange,
3952 context: &<Self::Tensor as Tensor>::Context,
3953 ) -> Result<Self::Embedding, Error>;
3954 fn vocabulary_parallel_linear(
3956 spec: LinearSpec,
3957 range: VocabularyParallelRange,
3958 context: &<Self::Tensor as Tensor>::Context,
3959 ) -> Result<Self::Linear, Error>;
3960 fn vocabulary_parallel_lookup(
3962 embedding: &mut Self::Embedding,
3963 input: &Self::Tensor,
3964 policy: EmbeddingLookupPolicy,
3965 parallel: &Self::ParallelContext,
3966 context: &<Self::Tensor as Tensor>::Context,
3967 ) -> Result<Self::Tensor, Error>;
3968 fn vocabulary_parallel_project(
3970 linear: &mut Self::Linear,
3971 input: &Self::Tensor,
3972 parallel: &Self::ParallelContext,
3973 context: &<Self::Tensor as Tensor>::Context,
3974 ) -> Result<Self::Tensor, Error>;
3975 fn vocabulary_parallel_embedding_project(
3977 embedding: &mut Self::Embedding,
3978 input: &Self::Tensor,
3979 parallel: &Self::ParallelContext,
3980 context: &<Self::Tensor as Tensor>::Context,
3981 ) -> Result<Self::Tensor, Error>;
3982 fn sum_parallel(
3984 value: Self::Tensor,
3985 parallel: &Self::ParallelContext,
3986 context: &<Self::Tensor as Tensor>::Context,
3987 ) -> Result<Self::Tensor, Error>;
3988}
3989
3990#[derive(Debug, Clone, Copy)]
3992pub struct GatedDeltaScanInput<'a, T> {
3993 pub query: &'a T,
3995 pub key: &'a T,
3997 pub value: &'a T,
3999 pub log_decay: &'a T,
4001 pub beta: &'a T,
4003 pub initial_state: Option<&'a T>,
4005}
4006
4007#[derive(Debug, Clone)]
4009pub struct GatedDeltaScanOutput<T> {
4010 pub state: T,
4012 pub output: T,
4014}
4015
4016#[derive(Debug, Clone, Copy)]
4018pub struct SelectiveStateSpaceScanInput<'a, T> {
4019 pub values: &'a T,
4021 pub input_state: &'a T,
4023 pub output_state: &'a T,
4025 pub time_step: &'a T,
4027 pub time_step_bias: &'a T,
4029 pub transition_log: &'a T,
4031 pub skip: &'a T,
4033 pub initial_state: Option<&'a T>,
4035 pub time_step_floor: f32,
4037 pub chunk_size: usize,
4039}
4040
4041#[derive(Debug, Clone)]
4043pub struct SelectiveStateSpaceScanOutput<T> {
4044 pub state: T,
4046 pub output: T,
4048}
4049
4050#[allow(clippy::too_many_arguments)]
4052pub fn reference_selective_state_space_scan(
4053 batch: usize,
4054 sequence: usize,
4055 heads: usize,
4056 head_dimensions: usize,
4057 state_dimensions: usize,
4058 values: &[f32],
4059 input_state: &[f32],
4060 output_state: &[f32],
4061 time_step: &[f32],
4062 time_step_bias: &[f32],
4063 transition_log: &[f32],
4064 skip: &[f32],
4065 time_step_floor: f32,
4066 initial_state: Option<&[f32]>,
4067) -> Result<(Vec<f32>, Vec<f32>), Error> {
4068 let groups = batch * sequence * heads;
4069 let values_len = groups * head_dimensions;
4070 let vectors_len = groups * state_dimensions;
4071 let state_len = batch * heads * head_dimensions * state_dimensions;
4072 if values.len() != values_len
4073 || input_state.len() != vectors_len
4074 || output_state.len() != vectors_len
4075 || time_step.len() != groups
4076 || time_step_bias.len() != heads
4077 || transition_log.len() != heads
4078 || skip.len() != heads
4079 || initial_state.is_some_and(|state| state.len() != state_len)
4080 || !time_step_floor.is_finite()
4081 || time_step_floor < 0.0
4082 {
4083 return Err(Error::backend(
4084 "invalid selective state-space reference geometry",
4085 ));
4086 }
4087 let mut state = initial_state.map_or_else(|| vec![0.0; state_len], <[f32]>::to_vec);
4088 let mut output = vec![0.0; values_len];
4089 for batch_index in 0..batch {
4090 for token in 0..sequence {
4091 for head in 0..heads {
4092 let group = (batch_index * sequence + token) * heads + head;
4093 let dt =
4094 ((time_step[group] + time_step_bias[head]).exp().ln_1p()).max(time_step_floor);
4095 let transition = (-transition_log[head].exp() * dt).exp();
4096 let vector_base = group * state_dimensions;
4097 for dimension in 0..head_dimensions {
4098 let value_index = group * head_dimensions + dimension;
4099 let state_base =
4100 (batch_index * heads + head) * head_dimensions * state_dimensions
4101 + dimension * state_dimensions;
4102 let value = values[value_index];
4103 let mut projected = 0.0f32;
4104 for state_dimension in 0..state_dimensions {
4105 let state_index = state_base + state_dimension;
4106 state[state_index] = state[state_index] * transition
4107 + dt * input_state[vector_base + state_dimension] * value;
4108 projected +=
4109 state[state_index] * output_state[vector_base + state_dimension];
4110 }
4111 output[value_index] = projected + value * skip[head];
4112 }
4113 }
4114 }
4115 }
4116 Ok((state, output))
4117}
4118
4119#[allow(clippy::too_many_arguments)]
4125pub fn reference_gated_delta_scan(
4126 batch: usize,
4127 sequence: usize,
4128 heads: usize,
4129 key_dim: usize,
4130 value_dim: usize,
4131 query: &[f32],
4132 key: &[f32],
4133 value: &[f32],
4134 log_decay: &[f32],
4135 vector_decay: bool,
4136 beta: &[f32],
4137 initial_state: Option<&[f32]>,
4138) -> Result<(Vec<f32>, Vec<f32>), Error> {
4139 let key_values = batch * sequence * heads * key_dim;
4140 let values = batch * sequence * heads * value_dim;
4141 let groups = batch * sequence * heads;
4142 let state_values = batch * heads * key_dim * value_dim;
4143 if query.len() != key_values
4144 || key.len() != key_values
4145 || value.len() != values
4146 || beta.len() != groups
4147 || log_decay.len() != if vector_decay { key_values } else { groups }
4148 || initial_state.is_some_and(|state| state.len() != state_values)
4149 {
4150 return Err(Error::backend("invalid gated-delta reference geometry"));
4151 }
4152 let mut state = initial_state.map_or_else(|| vec![0.0; state_values], <[f32]>::to_vec);
4153 let mut output = vec![0.0; values];
4154 for batch_index in 0..batch {
4155 for token in 0..sequence {
4156 for head in 0..heads {
4157 let group = (batch_index * sequence + token) * heads + head;
4158 let state_group = (batch_index * heads + head) * key_dim * value_dim;
4159 for value_index in 0..value_dim {
4160 let mut memory = 0.0f32;
4161 for key_index in 0..key_dim {
4162 let vector_index = group * key_dim + key_index;
4163 let decay = if vector_decay {
4164 log_decay[vector_index]
4165 } else {
4166 log_decay[group]
4167 }
4168 .exp();
4169 let state_index = state_group + key_index * value_dim + value_index;
4170 state[state_index] *= decay;
4171 memory += state[state_index] * key[vector_index];
4172 }
4173 let value_index_flat = group * value_dim + value_index;
4174 let delta = (value[value_index_flat] - memory) * beta[group];
4175 let mut accumulated = 0.0f32;
4176 for key_index in 0..key_dim {
4177 let vector_index = group * key_dim + key_index;
4178 let state_index = state_group + key_index * value_dim + value_index;
4179 state[state_index] += key[vector_index] * delta;
4180 accumulated += state[state_index] * query[vector_index];
4181 }
4182 output[value_index_flat] = accumulated;
4183 }
4184 }
4185 }
4186 }
4187 Ok((state, output))
4188}
4189
4190#[cfg(test)]
4191mod gated_delta_reference_tests {
4192 use super::reference_gated_delta_scan;
4193
4194 #[test]
4195 fn chunked_continuation_matches_one_scan() {
4196 let query = [0.5, -0.25, 0.1, 0.2, -0.4, 0.8];
4197 let key = [0.3, 0.4, -0.2, 0.7, 0.6, -0.1];
4198 let value = [1.0, -0.5, 0.25, 0.75, -0.3, 0.9];
4199 let decay = [-0.2, -0.1, -0.4, -0.3, -0.5, -0.25];
4200 let beta = [0.8, 0.6, 0.4];
4201 let (expected_state, expected) = reference_gated_delta_scan(
4202 1, 3, 1, 2, 2, &query, &key, &value, &decay, true, &beta, None,
4203 )
4204 .unwrap();
4205 let (state, mut actual) = reference_gated_delta_scan(
4206 1,
4207 2,
4208 1,
4209 2,
4210 2,
4211 &query[..4],
4212 &key[..4],
4213 &value[..4],
4214 &decay[..4],
4215 true,
4216 &beta[..2],
4217 None,
4218 )
4219 .unwrap();
4220 let (actual_state, tail) = reference_gated_delta_scan(
4221 1,
4222 1,
4223 1,
4224 2,
4225 2,
4226 &query[4..],
4227 &key[4..],
4228 &value[4..],
4229 &decay[4..],
4230 true,
4231 &beta[2..],
4232 Some(&state),
4233 )
4234 .unwrap();
4235 actual.extend(tail);
4236 assert!(expected
4237 .iter()
4238 .zip(actual)
4239 .all(|(left, right)| (left - right).abs() < 1e-6));
4240 assert!(expected_state
4241 .iter()
4242 .zip(actual_state)
4243 .all(|(left, right)| (left - right).abs() < 1e-6));
4244 }
4245}
4246
4247#[cfg(test)]
4248mod selective_state_space_reference_tests {
4249 use super::reference_selective_state_space_scan;
4250
4251 #[test]
4252 fn continuation_matches_one_scan() {
4253 let values = [0.2, -0.4, 0.8, 0.5, -0.3, 0.7];
4254 let input_state = [0.1, 0.3, -0.2, 0.4, 0.6, -0.5];
4255 let output_state = [0.7, -0.1, 0.2, 0.5, -0.4, 0.9];
4256 let time_step = [-0.3, 0.1, -0.2];
4257 let bias = [0.05];
4258 let transition = [-0.4];
4259 let skip = [0.25];
4260 let (expected_state, expected) = reference_selective_state_space_scan(
4261 1,
4262 3,
4263 1,
4264 2,
4265 2,
4266 &values,
4267 &input_state,
4268 &output_state,
4269 &time_step,
4270 &bias,
4271 &transition,
4272 &skip,
4273 0.001,
4274 None,
4275 )
4276 .unwrap();
4277 let (state, mut actual) = reference_selective_state_space_scan(
4278 1,
4279 2,
4280 1,
4281 2,
4282 2,
4283 &values[..4],
4284 &input_state[..4],
4285 &output_state[..4],
4286 &time_step[..2],
4287 &bias,
4288 &transition,
4289 &skip,
4290 0.001,
4291 None,
4292 )
4293 .unwrap();
4294 let (actual_state, tail) = reference_selective_state_space_scan(
4295 1,
4296 1,
4297 1,
4298 2,
4299 2,
4300 &values[4..],
4301 &input_state[4..],
4302 &output_state[4..],
4303 &time_step[2..],
4304 &bias,
4305 &transition,
4306 &skip,
4307 0.001,
4308 Some(&state),
4309 )
4310 .unwrap();
4311 actual.extend(tail);
4312 assert!(expected
4313 .iter()
4314 .zip(actual)
4315 .all(|(left, right)| (left - right).abs() < 1e-6));
4316 assert!(expected_state
4317 .iter()
4318 .zip(actual_state)
4319 .all(|(left, right)| (left - right).abs() < 1e-6));
4320 }
4321}
4322
4323pub trait Tensor: Clone + Debug + Sized + 'static {
4329 type Context: ?Sized;
4331
4332 fn shape(&self) -> &[i32];
4334
4335 fn dim(&self, axis: usize) -> i32 {
4337 self.shape()[axis]
4338 }
4339
4340 fn unloaded_f32(shape: &[i32], context: &Self::Context) -> Result<Self, Error>;
4342 fn unloaded_i32(shape: &[i32], context: &Self::Context) -> Result<Self, Error> {
4344 let _ = (shape, context);
4345 Err(Error::backend(
4346 "I32 parameter allocation is not implemented by this backend",
4347 ))
4348 }
4349 fn from_f32_slice(
4351 values: &[f32],
4352 shape: &[i32],
4353 context: &Self::Context,
4354 ) -> Result<Self, Error>;
4355 fn from_i32_slice(
4357 values: &[i32],
4358 shape: &[i32],
4359 context: &Self::Context,
4360 ) -> Result<Self, Error> {
4361 let _ = (values, shape, context);
4362 Err(Error::backend(
4363 "I32 tensor construction is not implemented by this backend",
4364 ))
4365 }
4366 fn to_f32_vec(&self, context: &Self::Context) -> Result<Vec<f32>, Error> {
4368 let _ = context;
4369 Err(Error::backend(
4370 "F32 host materialization is not implemented by this backend",
4371 ))
4372 }
4373 fn to_i32_vec(&self, context: &Self::Context) -> Result<Vec<i32>, Error> {
4375 let _ = context;
4376 Err(Error::backend(
4377 "I32 host materialization is not implemented by this backend",
4378 ))
4379 }
4380 fn full_f32(value: f32, shape: &[i32], context: &Self::Context) -> Result<Self, Error> {
4382 let _ = (value, shape, context);
4383 Err(Error::backend(
4384 "filled tensor construction is not implemented by this backend",
4385 ))
4386 }
4387 fn full_i32(value: i32, shape: &[i32], context: &Self::Context) -> Result<Self, Error> {
4389 let _ = (value, shape, context);
4390 Err(Error::backend(
4391 "filled I32 tensor construction is not implemented by this backend",
4392 ))
4393 }
4394 fn add(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4396 fn subtract(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4398 fn multiply(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4400 fn multiply_scalar(&self, rhs: f32, context: &Self::Context) -> Result<Self, Error>;
4402 fn divide(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4404 fn square(&self, context: &Self::Context) -> Result<Self, Error>;
4406 fn tanh(&self, context: &Self::Context) -> Result<Self, Error> {
4408 let _ = context;
4409 Err(Error::backend(
4410 "tanh is not implemented by this tensor backend",
4411 ))
4412 }
4413 fn maximum_scalar(&self, rhs: f32, context: &Self::Context) -> Result<Self, Error>;
4415 fn maximum_i32(&self, rhs: i32, context: &Self::Context) -> Result<Self, Error> {
4418 self.maximum_scalar(rhs as f32, context)
4419 }
4420 fn clip(&self, minimum: &Self, maximum: &Self, context: &Self::Context) -> Result<Self, Error> {
4422 let _ = (minimum, maximum, context);
4423 Err(Error::backend(
4424 "clip is not implemented by this tensor backend",
4425 ))
4426 }
4427
4428 fn softmax_axis(
4430 &self,
4431 axis: i32,
4432 precise: bool,
4433 context: &Self::Context,
4434 ) -> Result<Self, Error> {
4435 let _ = (axis, precise, context);
4436 Err(Error::backend(
4437 "softmax is not implemented by this tensor backend",
4438 ))
4439 }
4440
4441 fn reshape(&self, shape: &[i32], context: &Self::Context) -> Result<Self, Error>;
4443 fn broadcast_to(&self, shape: &[i32], context: &Self::Context) -> Result<Self, Error> {
4445 let _ = (shape, context);
4446 Err(Error::backend(
4447 "broadcasting is not implemented by this tensor backend",
4448 ))
4449 }
4450 fn transpose_axes(&self, axes: &[i32], context: &Self::Context) -> Result<Self, Error>;
4452 fn swap_axes(&self, left: i32, right: i32, context: &Self::Context) -> Result<Self, Error>;
4454 fn transpose(&self, context: &Self::Context) -> Result<Self, Error>;
4456 fn expand_dims(&self, axis: i32, context: &Self::Context) -> Result<Self, Error>;
4458 fn squeeze_axes(&self, axes: &[i32], context: &Self::Context) -> Result<Self, Error>;
4460 fn index(&self, indexes: &[Index], context: &Self::Context) -> Result<Self, Error>;
4462 fn take_axis(&self, indexes: &Self, axis: i32, context: &Self::Context) -> Result<Self, Error>;
4464 fn zeros_like(&self, context: &Self::Context) -> Result<Self, Error> {
4466 let _ = context;
4467 Err(Error::backend(
4468 "dtype-preserving zero allocation is not implemented by this tensor backend",
4469 ))
4470 }
4471 fn equal_i32(&self, value: i32, context: &Self::Context) -> Result<Self, Error> {
4473 let _ = (value, context);
4474 Err(Error::backend(
4475 "integer scalar comparison is not implemented by this tensor backend",
4476 ))
4477 }
4478 fn logical_or(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error> {
4480 let _ = (rhs, context);
4481 Err(Error::backend(
4482 "logical disjunction is not implemented by this tensor backend",
4483 ))
4484 }
4485 fn where_condition(
4487 condition: &Self,
4488 when_true: &Self,
4489 when_false: &Self,
4490 context: &Self::Context,
4491 ) -> Result<Self, Error> {
4492 let _ = (condition, when_true, when_false, context);
4493 Err(Error::backend(
4494 "conditional selection is not implemented by this tensor backend",
4495 ))
4496 }
4497 fn masked_scatter(
4499 &self,
4500 mask: &Self,
4501 source: &Self,
4502 context: &Self::Context,
4503 ) -> Result<Self, Error> {
4504 let _ = (mask, source, context);
4505 Err(Error::backend(
4506 "masked scatter is not implemented by this tensor backend",
4507 ))
4508 }
4509
4510 fn rope_with_frequencies(
4512 &self,
4513 dimensions: i32,
4514 traditional: bool,
4515 offset: i32,
4516 frequencies: &Self,
4517 context: &Self::Context,
4518 ) -> Result<Self, Error> {
4519 let _ = (dimensions, traditional, offset, frequencies, context);
4520 Err(Error::backend(
4521 "explicit-frequency rotary positions are not implemented by this tensor backend",
4522 ))
4523 }
4524
4525 fn concatenate(values: &[Self], axis: i32, context: &Self::Context) -> Result<Self, Error>;
4527 fn stack(values: &[Self], axis: i32, context: &Self::Context) -> Result<Self, Error>;
4529 fn matmul(lhs: &Self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4531 fn sum_axis(
4533 value: &Self,
4534 axis: i32,
4535 keep_dims: bool,
4536 context: &Self::Context,
4537 ) -> Result<Self, Error>;
4538 fn mean_axis(
4540 value: &Self,
4541 axis: i32,
4542 keep_dims: bool,
4543 context: &Self::Context,
4544 ) -> Result<Self, Error> {
4545 let width = value
4546 .shape()
4547 .get(if axis < 0 {
4548 usize::try_from(value.shape().len() as i32 + axis).unwrap_or(usize::MAX)
4549 } else {
4550 usize::try_from(axis).unwrap_or(usize::MAX)
4551 })
4552 .copied()
4553 .ok_or_else(|| Error::backend(format!("mean axis {axis} is out of range")))?;
4554 Self::sum_axis(value, axis, keep_dims, context)?
4555 .multiply_scalar(1.0 / width as f32, context)
4556 }
4557 fn argmin_axis(
4559 value: &Self,
4560 axis: i32,
4561 keep_dims: bool,
4562 context: &Self::Context,
4563 ) -> Result<Self, Error>;
4564 fn pad(
4566 value: &Self,
4567 widths: &[(i32, i32)],
4568 mode: PadMode,
4569 context: &Self::Context,
4570 ) -> Result<Self, Error>;
4571
4572 #[allow(clippy::too_many_arguments)]
4574 fn conv1d(
4575 input: &Self,
4576 weight: &Self,
4577 stride: i32,
4578 padding: i32,
4579 dilation: i32,
4580 groups: i32,
4581 context: &Self::Context,
4582 ) -> Result<Self, Error>;
4583 #[allow(clippy::too_many_arguments)]
4585 fn conv2d(
4586 input: &Self,
4587 weight: &Self,
4588 stride: (i32, i32),
4589 padding: (i32, i32),
4590 dilation: (i32, i32),
4591 groups: i32,
4592 context: &Self::Context,
4593 ) -> Result<Self, Error> {
4594 let _ = (input, weight, stride, padding, dilation, groups, context);
4595 Err(Error::backend(
4596 "two-dimensional convolution is not implemented by this backend",
4597 ))
4598 }
4599 #[allow(clippy::too_many_arguments)]
4601 fn conv_transpose1d(
4602 input: &Self,
4603 weight: &Self,
4604 stride: i32,
4605 padding: i32,
4606 dilation: i32,
4607 output_padding: i32,
4608 groups: i32,
4609 context: &Self::Context,
4610 ) -> Result<Self, Error>;
4611 fn linear(
4613 input: &Self,
4614 weight: &Self,
4615 bias: Option<&Self>,
4616 context: &Self::Context,
4617 ) -> Result<Self, Error>;
4618 fn layer_norm(
4620 input: &Self,
4621 weight: Option<&Self>,
4622 bias: Option<&Self>,
4623 epsilon: f32,
4624 context: &Self::Context,
4625 ) -> Result<Self, Error>;
4626 fn gelu(input: &Self, context: &Self::Context) -> Result<Self, Error>;
4628 fn elu(input: &Self, alpha: f32, context: &Self::Context) -> Result<Self, Error>;
4630 #[allow(clippy::too_many_arguments)]
4632 fn rope(
4633 input: &Self,
4634 dimensions: i32,
4635 traditional: bool,
4636 base: f32,
4637 scale: f32,
4638 offset: i32,
4639 context: &Self::Context,
4640 ) -> Result<Self, Error>;
4641 fn multi_axis_rotary_embeddings(
4643 position_ids: &Self,
4644 spec: &multimodal::MultiAxisRotarySpec,
4645 context: &Self::Context,
4646 ) -> Result<(Self, Self), Error> {
4647 let _ = (position_ids, spec, context);
4648 Err(Error::backend(
4649 "multi-axis rotary embeddings are not implemented by this backend",
4650 ))
4651 }
4652 fn masked_output_projection(
4654 input: multimodal::MaskedOutputProjectionInput<'_, Self>,
4655 context: &Self::Context,
4656 ) -> Result<Self, Error> {
4657 let _ = (input, context);
4658 Err(Error::backend(
4659 "masked output projection is not implemented by this backend",
4660 ))
4661 }
4662 fn scaled_dot_product_attention(
4664 queries: &Self,
4665 keys: &Self,
4666 values: &Self,
4667 scale: f32,
4668 mask: AttentionMask<'_, Self>,
4669 context: &Self::Context,
4670 ) -> Result<Self, Error>;
4671}
4672
4673#[derive(Debug, Clone)]
4675pub struct Parameter<T> {
4676 spec: ParameterSpec,
4677 trainable: bool,
4678 value: T,
4679}
4680
4681impl<T> Parameter<T> {
4682 pub fn new(spec: ParameterSpec, value: T) -> Self {
4684 let trainable = spec.trainable;
4685 Self {
4686 spec,
4687 trainable,
4688 value,
4689 }
4690 }
4691 pub const fn as_ref(&self) -> &T {
4693 &self.value
4694 }
4695 pub fn replace(&mut self, value: T) {
4697 self.value = value;
4698 }
4699}
4700
4701impl<T: Tensor> Parameter<T> {
4702 pub fn unloaded(
4704 spec: ParameterSpec,
4705 shape: &[i32],
4706 context: &T::Context,
4707 ) -> Result<Self, Error> {
4708 Ok(Self::new(spec, T::unloaded_f32(shape, context)?))
4709 }
4710
4711 pub fn unloaded_i32(
4713 spec: ParameterSpec,
4714 shape: &[i32],
4715 context: &T::Context,
4716 ) -> Result<Self, Error> {
4717 Ok(Self::new(spec, T::unloaded_i32(shape, context)?))
4718 }
4719}
4720
4721impl<T: 'static> Parameterized<T> for Parameter<T> {
4722 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
4723 where
4724 V: ParameterVisitor<'a, T>,
4725 {
4726 visitor.visit(
4727 ParameterMetadata::from_spec(&self.spec, self.trainable),
4728 &self.value,
4729 );
4730 }
4731
4732 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
4733 where
4734 V: ParameterVisitorMut<'a, T>,
4735 {
4736 visitor.visit_mut(
4737 ParameterMetadata::from_spec(&self.spec, self.trainable),
4738 &mut self.value,
4739 );
4740 }
4741
4742 fn set_trainable(&mut self, trainable: bool) {
4743 self.trainable = trainable;
4744 }
4745}
4746
4747impl<T: 'static, M: Parameterized<T>> Parameterized<T> for Vec<M> {
4748 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
4749 where
4750 V: ParameterVisitor<'a, T>,
4751 {
4752 for module in self {
4753 module.visit_parameters(visitor);
4754 }
4755 }
4756
4757 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
4758 where
4759 V: ParameterVisitorMut<'a, T>,
4760 {
4761 for module in self {
4762 module.visit_parameters_mut(visitor);
4763 }
4764 }
4765
4766 fn set_trainable(&mut self, trainable: bool) {
4767 for module in self {
4768 module.set_trainable(trainable);
4769 }
4770 }
4771}
4772
4773impl<T: 'static, M: Parameterized<T>> Parameterized<T> for Option<M> {
4774 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
4775 where
4776 V: ParameterVisitor<'a, T>,
4777 {
4778 if let Some(module) = self {
4779 module.visit_parameters(visitor);
4780 }
4781 }
4782
4783 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
4784 where
4785 V: ParameterVisitorMut<'a, T>,
4786 {
4787 if let Some(module) = self {
4788 module.visit_parameters_mut(visitor);
4789 }
4790 }
4791
4792 fn set_trainable(&mut self, trainable: bool) {
4793 if let Some(module) = self {
4794 module.set_trainable(trainable);
4795 }
4796 }
4797}
4798
4799#[derive(Debug, Clone, Copy, Eq, PartialEq)]
4801pub enum ConvolutionActivation {
4802 Identity,
4804 Silu,
4806}
4807
4808#[derive(Debug, Clone)]
4810pub struct CausalDepthwiseConvolutionSpec {
4811 pub channels: i32,
4813 pub kernel_size: i32,
4815 pub weight: ParameterSpec,
4817 pub bias: Option<ParameterSpec>,
4819 pub activation: ConvolutionActivation,
4821}
4822
4823impl CausalDepthwiseConvolutionSpec {
4824 pub fn validate(&self) -> Result<(), Error> {
4826 if self.channels <= 0 {
4827 return Err(Error::backend(format!(
4828 "causal depthwise convolution channels must be positive, got {}",
4829 self.channels
4830 )));
4831 }
4832 if self.kernel_size <= 0 {
4833 return Err(Error::backend(format!(
4834 "causal depthwise convolution kernel size must be positive, got {}",
4835 self.kernel_size
4836 )));
4837 }
4838 Ok(())
4839 }
4840}
4841
4842#[derive(Debug, Clone)]
4844pub struct CausalDepthwiseConvolutionOutput<T> {
4845 pub output: T,
4847 pub history: Option<T>,
4849}
4850
4851#[derive(Debug, Clone, Parameterized)]
4856#[parameterized(tensor = "B::Tensor")]
4857pub struct CausalDepthwiseConvolution<B: NeuralBackend> {
4858 pub weight: Parameter<B::Tensor>,
4860 pub bias: Option<Parameter<B::Tensor>>,
4862 #[parameter(skip)]
4863 channels: i32,
4864 #[parameter(skip)]
4865 kernel_size: i32,
4866 #[parameter(skip)]
4867 activation: ConvolutionActivation,
4868}
4869
4870impl<B: NeuralBackend> CausalDepthwiseConvolution<B> {
4871 pub fn new(
4873 spec: CausalDepthwiseConvolutionSpec,
4874 context: &<B::Tensor as Tensor>::Context,
4875 ) -> Result<Self, Error> {
4876 spec.validate()?;
4877 Ok(Self {
4878 weight: Parameter::unloaded(
4879 spec.weight,
4880 &[spec.channels, 1, spec.kernel_size],
4881 context,
4882 )?,
4883 bias: spec
4884 .bias
4885 .map(|bias| Parameter::unloaded(bias, &[spec.channels], context))
4886 .transpose()?,
4887 channels: spec.channels,
4888 kernel_size: spec.kernel_size,
4889 activation: spec.activation,
4890 })
4891 }
4892
4893 pub const fn history_len(&self) -> i32 {
4895 self.kernel_size - 1
4896 }
4897
4898 pub fn forward(
4900 &self,
4901 input: &B::Tensor,
4902 history: Option<&B::Tensor>,
4903 context: &<B::Tensor as Tensor>::Context,
4904 ) -> Result<CausalDepthwiseConvolutionOutput<B::Tensor>, Error> {
4905 let shape = input.shape();
4906 if shape.len() != 3 || shape[0] <= 0 || shape[1] <= 0 || shape[2] != self.channels {
4907 return Err(Error::backend(format!(
4908 "causal depthwise convolution expects [batch, sequence, {}], got {shape:?}",
4909 self.channels
4910 )));
4911 }
4912 let history_len = self.history_len();
4913 let padded = if history_len == 0 {
4914 if history.is_some() {
4915 return Err(Error::backend(
4916 "width-one causal convolution does not accept history",
4917 ));
4918 }
4919 input.clone()
4920 } else if let Some(history) = history {
4921 let expected = [shape[0], history_len, self.channels];
4922 if history.shape() != expected {
4923 return Err(Error::backend(format!(
4924 "causal depthwise convolution history must have shape {expected:?}, got {:?}",
4925 history.shape()
4926 )));
4927 }
4928 B::Tensor::concatenate(&[history.clone(), input.clone()], 1, context)?
4929 } else {
4930 B::Tensor::pad(
4931 input,
4932 &[(0, 0), (history_len, 0), (0, 0)],
4933 PadMode::Constant,
4934 context,
4935 )?
4936 };
4937 let execution_weight = self.weight.as_ref().swap_axes(1, 2, context)?;
4938 let mut output =
4939 B::Tensor::conv1d(&padded, &execution_weight, 1, 0, 1, self.channels, context)?;
4940 if output.shape() != shape {
4941 return Err(Error::backend(format!(
4942 "causal depthwise convolution backend returned shape {:?}, expected {shape:?}",
4943 output.shape()
4944 )));
4945 }
4946 if let Some(bias) = &self.bias {
4947 let bias = bias
4948 .as_ref()
4949 .reshape(&[1, 1, self.channels], context)?
4950 .broadcast_to(shape, context)?;
4951 output = output.add(&bias, context)?;
4952 }
4953 if self.activation == ConvolutionActivation::Silu {
4954 output = B::silu(output, context)?;
4955 }
4956 let history = (history_len > 0)
4957 .then(|| {
4958 padded.index(
4959 &[
4960 Index::Full,
4961 Index::Range(shape[1], shape[1] + history_len),
4962 Index::Full,
4963 ],
4964 context,
4965 )
4966 })
4967 .transpose()?;
4968 Ok(CausalDepthwiseConvolutionOutput { output, history })
4969 }
4970}
4971
4972#[derive(Debug, Clone)]
4974pub struct GatedShortConvolutionSpec {
4975 pub input_dimensions: i32,
4977 pub channels: i32,
4979 pub output_dimensions: i32,
4981 pub input_projection: LinearSpec,
4983 pub output_projection: LinearSpec,
4985 pub convolution: CausalDepthwiseConvolutionSpec,
4987}
4988
4989impl GatedShortConvolutionSpec {
4990 pub fn validate(&self) -> Result<(), Error> {
4992 self.convolution.validate()?;
4993 let fused = self
4994 .channels
4995 .checked_mul(3)
4996 .ok_or_else(|| Error::backend("gated short-convolution width overflowed"))?;
4997 if self.input_dimensions <= 0
4998 || self.channels <= 0
4999 || self.output_dimensions <= 0
5000 || self.convolution.channels != self.channels
5001 || self.input_projection.input != self.input_dimensions
5002 || self.input_projection.output != fused
5003 || self.output_projection.input != self.channels
5004 || self.output_projection.output != self.output_dimensions
5005 {
5006 return Err(Error::backend(format!(
5007 "invalid gated short-convolution geometry input={} channels={} output={} fused_projection={}x{} output_projection={}x{} convolution_channels={}",
5008 self.input_dimensions,
5009 self.channels,
5010 self.output_dimensions,
5011 self.input_projection.input,
5012 self.input_projection.output,
5013 self.output_projection.input,
5014 self.output_projection.output,
5015 self.convolution.channels,
5016 )));
5017 }
5018 Ok(())
5019 }
5020}
5021
5022#[derive(Debug, Clone)]
5024pub struct GatedShortConvolutionOutput<T> {
5025 pub output: T,
5027 pub history: Option<T>,
5029}
5030
5031#[derive(Debug, Clone, Parameterized)]
5033#[parameterized(tensor = "B::Tensor")]
5034pub struct GatedShortConvolution<B: NeuralBackend> {
5035 pub input_projection: B::Linear,
5037 pub convolution: CausalDepthwiseConvolution<B>,
5039 pub output_projection: B::Linear,
5041 #[parameter(skip)]
5042 channels: i32,
5043}
5044
5045impl<B: NeuralBackend> GatedShortConvolution<B> {
5046 pub fn new(
5048 spec: GatedShortConvolutionSpec,
5049 context: &<B::Tensor as Tensor>::Context,
5050 ) -> Result<Self, Error> {
5051 spec.validate()?;
5052 Ok(Self {
5053 input_projection: B::linear(spec.input_projection, context)?,
5054 convolution: CausalDepthwiseConvolution::new(spec.convolution, context)?,
5055 output_projection: B::linear(spec.output_projection, context)?,
5056 channels: spec.channels,
5057 })
5058 }
5059
5060 fn hidden(
5061 &mut self,
5062 input: &B::Tensor,
5063 history: Option<&B::Tensor>,
5064 context: &<B::Tensor as Tensor>::Context,
5065 ) -> Result<(B::Tensor, Option<B::Tensor>), Error> {
5066 let projected = self.input_projection.forward(input, context)?;
5067 let rank = projected.shape().len();
5068 if rank == 0 || projected.shape()[rank - 1] != 3 * self.channels {
5069 return Err(Error::backend(format!(
5070 "gated short-convolution projection returned shape {:?}, expected final width {}",
5071 projected.shape(),
5072 3 * self.channels
5073 )));
5074 }
5075 let mut segment = vec![Index::Full; rank];
5076 segment[rank - 1] = Index::Range(0, self.channels);
5077 let b = projected.index(&segment, context)?;
5078 segment[rank - 1] = Index::Range(self.channels, 2 * self.channels);
5079 let c = projected.index(&segment, context)?;
5080 segment[rank - 1] = Index::Range(2 * self.channels, 3 * self.channels);
5081 let x = projected.index(&segment, context)?;
5082 let convolution = self
5083 .convolution
5084 .forward(&b.multiply(&x, context)?, history, context)?;
5085 Ok((
5086 c.multiply(&convolution.output, context)?,
5087 convolution.history,
5088 ))
5089 }
5090
5091 pub fn forward(
5093 &mut self,
5094 input: &B::Tensor,
5095 history: Option<&B::Tensor>,
5096 context: &<B::Tensor as Tensor>::Context,
5097 ) -> Result<GatedShortConvolutionOutput<B::Tensor>, Error> {
5098 let (hidden, history) = self.hidden(input, history, context)?;
5099 Ok(GatedShortConvolutionOutput {
5100 output: self.output_projection.forward(&hidden, context)?,
5101 history,
5102 })
5103 }
5104
5105 pub fn forward_parallel(
5107 &mut self,
5108 input: &B::Tensor,
5109 history: Option<&B::Tensor>,
5110 parallel: &B::ParallelContext,
5111 context: &<B::Tensor as Tensor>::Context,
5112 ) -> Result<GatedShortConvolutionOutput<B::Tensor>, Error> {
5113 let (hidden, history) = self.hidden(input, history, context)?;
5114 Ok(GatedShortConvolutionOutput {
5115 output: B::row_parallel_linear(
5116 &mut self.output_projection,
5117 &hidden,
5118 parallel,
5119 context,
5120 )?,
5121 history,
5122 })
5123 }
5124}
5125
5126#[cfg(test)]
5127mod grouped_contract_tests {
5128 use super::*;
5129
5130 fn dense_format() -> LinearFormatSpec {
5131 LinearFormatSpec::unscaled(LinearFormat::Dense).unwrap()
5132 }
5133
5134 fn parameters(prefix: &str) -> GatedProductGroupParameters {
5135 let projection = |name| {
5136 GroupedProjectionSpec::new(
5137 ParameterSpec::trainable(name).unwrap(),
5138 None,
5139 dense_format(),
5140 )
5141 .unwrap()
5142 };
5143 GatedProductGroupParameters::new(
5144 projection(format!("{prefix}.gate.weight")),
5145 projection(format!("{prefix}.up.weight")),
5146 projection(format!("{prefix}.down.weight")),
5147 )
5148 }
5149
5150 #[test]
5151 fn top_k_selection_policy_rejects_invalid_counts() {
5152 assert!(TopKGroupSelectionSpec::new(8, 2, GroupScoring::Softmax, true).is_ok());
5153 assert!(TopKGroupSelectionSpec::new(0, 1, GroupScoring::Softmax, false).is_err());
5154 assert!(TopKGroupSelectionSpec::new(8, 9, GroupScoring::Softmax, false).is_err());
5155 }
5156
5157 #[test]
5158 fn gated_product_policy_rejects_malformed_scalars() {
5159 assert!(
5160 GatedProductPolicy::new(GatedProductActivation::Silu, Some(0.0), None, 1.0, 0.0,)
5161 .is_err()
5162 );
5163 assert!(GatedProductPolicy::new(
5164 GatedProductActivation::Silu,
5165 None,
5166 Some(f32::NAN),
5167 1.0,
5168 0.0,
5169 )
5170 .is_err());
5171 assert!(
5172 GatedProductPolicy::new(GatedProductActivation::Silu, None, None, 0.0, 0.0,).is_err()
5173 );
5174 assert!(GatedProductPolicy::new(
5175 GatedProductActivation::Silu,
5176 None,
5177 None,
5178 1.0,
5179 f32::INFINITY,
5180 )
5181 .is_err());
5182 }
5183
5184 #[test]
5185 fn selector_projection_and_correction_biases_require_distinct_identities() {
5186 let shared_bias = ParameterSpec::trainable("selector.bias").unwrap();
5187 let spec = TopKGroupSelectorSpec::new(
5188 4,
5189 ParameterSpec::trainable("selector.weight").unwrap(),
5190 dense_format(),
5191 TopKGroupSelectionSpec::new(2, 1, GroupScoring::SelectedSoftmax, false).unwrap(),
5192 )
5193 .unwrap()
5194 .with_bias(shared_bias.clone())
5195 .unwrap();
5196
5197 assert!(spec.with_correction_bias(shared_bias).is_err());
5198 }
5199
5200 #[test]
5201 fn independent_group_layout_requires_exact_cardinality() {
5202 assert!(GroupedGatedProductSpec::new(
5203 2,
5204 16,
5205 8,
5206 16,
5207 eredu_nn::GatedProductPolicy::ordinary_silu(),
5208 GatedProductGroupLayout::Independent(vec![parameters("e0"), parameters("e1")]),
5209 )
5210 .is_ok());
5211 assert!(GroupedGatedProductSpec::new(
5212 2,
5213 16,
5214 8,
5215 16,
5216 eredu_nn::GatedProductPolicy::ordinary_silu(),
5217 GatedProductGroupLayout::Independent(vec![parameters("e0")]),
5218 )
5219 .is_err());
5220 }
5221
5222 #[test]
5223 fn gated_product_bank_rejects_reused_projection_bias_identity() {
5224 let shared = ParameterSpec::trainable("groups.gate_up").unwrap();
5225 let gate_up = GroupedProjectionSpec::new(shared.clone(), Some(shared), dense_format());
5226 assert!(gate_up.is_err());
5227 }
5228
5229 #[test]
5230 fn quantized_group_projection_requires_explicit_companion_identities() {
5231 let format =
5232 LinearFormat::Affine(eredu_checkpoint::AffineQuantization::new(32, 4).unwrap());
5233 let projection = |format| {
5234 GroupedProjectionSpec::new(
5235 ParameterSpec::trainable("arbitrary.group.matrix").unwrap(),
5236 None,
5237 format,
5238 )
5239 };
5240 assert!(LinearFormatSpec::unscaled(format).is_err());
5241 assert!(projection(
5242 LinearFormatSpec::affine(
5243 format,
5244 ParameterSpec::trainable("unrelated.scale.identity").unwrap(),
5245 ParameterSpec::trainable("unrelated.affine.identity").unwrap(),
5246 )
5247 .unwrap()
5248 )
5249 .is_ok());
5250 }
5251}
5252
5253#[derive(Debug, Clone)]
5255pub struct Linear<T> {
5256 pub weight: Parameter<T>,
5258 pub bias: Option<Parameter<T>>,
5260}
5261
5262impl<T: Tensor> Linear<T> {
5263 pub fn unloaded(spec: LinearSpec, context: &T::Context) -> Result<Self, Error> {
5265 Ok(Self {
5266 weight: Parameter::unloaded(spec.weight, &[spec.output, spec.input], context)?,
5267 bias: spec
5268 .bias
5269 .map(|bias| Parameter::unloaded(bias, &[spec.output], context))
5270 .transpose()?,
5271 })
5272 }
5273
5274 pub fn forward(&self, input: &T, context: &T::Context) -> Result<T, Error> {
5276 T::linear(
5277 input,
5278 self.weight.as_ref(),
5279 self.bias.as_ref().map(Parameter::as_ref),
5280 context,
5281 )
5282 }
5283}
5284
5285impl<T: 'static> Parameterized<T> for Linear<T> {
5286 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
5287 where
5288 V: ParameterVisitor<'a, T>,
5289 {
5290 self.weight.visit_parameters(visitor);
5291 if let Some(bias) = &self.bias {
5292 bias.visit_parameters(visitor);
5293 }
5294 }
5295
5296 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
5297 where
5298 V: ParameterVisitorMut<'a, T>,
5299 {
5300 self.weight.visit_parameters_mut(visitor);
5301 if let Some(bias) = &mut self.bias {
5302 bias.visit_parameters_mut(visitor);
5303 }
5304 }
5305
5306 fn set_trainable(&mut self, trainable: bool) {
5307 self.weight.set_trainable(trainable);
5308 if let Some(bias) = &mut self.bias {
5309 bias.set_trainable(trainable);
5310 }
5311 }
5312}
5313
5314#[derive(Debug, Clone)]
5316pub struct LayerNorm<T> {
5317 pub epsilon: f32,
5319 pub weight: Option<Parameter<T>>,
5321 pub bias: Option<Parameter<T>>,
5323}
5324
5325impl<T: Tensor> LayerNorm<T> {
5326 pub fn unloaded(
5328 dimensions: i32,
5329 epsilon: f32,
5330 weight: Option<ParameterSpec>,
5331 bias: Option<ParameterSpec>,
5332 context: &T::Context,
5333 ) -> Result<Self, Error> {
5334 Ok(Self {
5335 epsilon,
5336 weight: weight
5337 .map(|weight| Parameter::unloaded(weight, &[dimensions], context))
5338 .transpose()?,
5339 bias: bias
5340 .map(|bias| Parameter::unloaded(bias, &[dimensions], context))
5341 .transpose()?,
5342 })
5343 }
5344
5345 pub fn forward(&self, input: &T, context: &T::Context) -> Result<T, Error> {
5347 T::layer_norm(
5348 input,
5349 self.weight.as_ref().map(Parameter::as_ref),
5350 self.bias.as_ref().map(Parameter::as_ref),
5351 self.epsilon,
5352 context,
5353 )
5354 }
5355}
5356
5357impl<T: 'static> Parameterized<T> for LayerNorm<T> {
5358 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
5359 where
5360 V: ParameterVisitor<'a, T>,
5361 {
5362 if let Some(weight) = &self.weight {
5363 weight.visit_parameters(visitor);
5364 }
5365 if let Some(bias) = &self.bias {
5366 bias.visit_parameters(visitor);
5367 }
5368 }
5369
5370 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
5371 where
5372 V: ParameterVisitorMut<'a, T>,
5373 {
5374 if let Some(weight) = &mut self.weight {
5375 weight.visit_parameters_mut(visitor);
5376 }
5377 if let Some(bias) = &mut self.bias {
5378 bias.visit_parameters_mut(visitor);
5379 }
5380 }
5381
5382 fn set_trainable(&mut self, trainable: bool) {
5383 if let Some(weight) = &mut self.weight {
5384 weight.set_trainable(trainable);
5385 }
5386 if let Some(bias) = &mut self.bias {
5387 bias.set_trainable(trainable);
5388 }
5389 }
5390}
5391
5392#[cfg(test)]
5393mod parameter_topology_tests {
5394 use super::*;
5395
5396 #[derive(Parameterized)]
5397 #[parameterized(tensor = "i32")]
5398 struct DerivedModule {
5399 first: Parameter<i32>,
5400 second: Option<Parameter<i32>>,
5401 #[parameter(skip)]
5402 label: &'static str,
5403 }
5404
5405 #[derive(Parameterized)]
5406 #[parameterized(tensor = "i32")]
5407 enum DerivedChoice {
5408 Present(Parameter<i32>),
5409 Empty,
5410 }
5411
5412 fn parameter(id: &str, value: i32) -> Parameter<i32> {
5413 Parameter::new(ParameterSpec::trainable(id).unwrap(), value)
5414 }
5415
5416 #[test]
5417 fn derive_recurses_through_structs_options_and_enums() {
5418 let mut module = DerivedModule {
5419 first: parameter("first.weight", 1),
5420 second: Some(parameter("second.weight", 2)),
5421 label: "not a parameter",
5422 };
5423 assert_eq!(module.label, "not a parameter");
5424 let metadata = validate_parameter_topology::<i32, _>(&module).unwrap();
5425 assert_eq!(
5426 metadata
5427 .iter()
5428 .map(|entry| entry.id.as_str())
5429 .collect::<Vec<_>>(),
5430 ["first.weight", "second.weight"]
5431 );
5432
5433 module.set_trainable(false);
5434 assert!(validate_parameter_topology::<i32, _>(&module)
5435 .unwrap()
5436 .iter()
5437 .all(|entry| !entry.trainable));
5438
5439 let choice = DerivedChoice::Present(parameter("choice.weight", 3));
5440 assert_eq!(
5441 validate_parameter_topology::<i32, _>(&choice).unwrap()[0]
5442 .id
5443 .as_str(),
5444 "choice.weight"
5445 );
5446 assert!(validate_parameter_topology::<i32, _>(&DerivedChoice::Empty)
5447 .unwrap()
5448 .is_empty());
5449 }
5450
5451 #[test]
5452 fn validation_rejects_duplicates_and_invalid_aliases() {
5453 let duplicate = vec![parameter("same.weight", 1), parameter("same.weight", 2)];
5454 assert!(matches!(
5455 validate_parameter_topology::<i32, _>(&duplicate),
5456 Err(ParameterTopologyError::DuplicateId(id)) if id.as_str() == "same.weight"
5457 ));
5458
5459 let alias = Parameter::new(
5460 ParameterSpec {
5461 id: ParameterId::new("alias.weight").unwrap(),
5462 trainable: true,
5463 alias_of: Some(ParameterId::new("missing.weight").unwrap()),
5464 group: None,
5465 linear_companion: None,
5466 linear_companion_of: None,
5467 },
5468 1,
5469 );
5470 assert!(matches!(
5471 validate_parameter_topology::<i32, _>(&alias),
5472 Err(ParameterTopologyError::MissingAliasDestination { .. })
5473 ));
5474 }
5475}
5476
5477#[cfg(test)]
5478mod fused_projection_layout_tests {
5479 use super::*;
5480
5481 #[test]
5482 fn component_major_layout_is_checked_and_stable() {
5483 let layout = FusedProjectionLayout::new([
5484 FusedProjectionSegment::new("query", 8).unwrap(),
5485 FusedProjectionSegment::new("key", 4).unwrap(),
5486 FusedProjectionSegment::new("value", 4).unwrap(),
5487 ])
5488 .unwrap();
5489 assert_eq!(layout.output_width(), 16);
5490 assert_eq!(
5491 layout
5492 .segments()
5493 .iter()
5494 .map(|segment| (segment.name(), segment.width()))
5495 .collect::<Vec<_>>(),
5496 [("query", 8), ("key", 4), ("value", 4)]
5497 );
5498 assert!(FusedProjectionLayout::new(Vec::new()).is_err());
5499 assert!(FusedProjectionLayout::new([
5500 FusedProjectionSegment::new("same", 1).unwrap(),
5501 FusedProjectionSegment::new("same", 1).unwrap(),
5502 ])
5503 .is_err());
5504 assert!(FusedProjectionSegment::new("", 1).is_err());
5505 assert!(FusedProjectionSegment::new("bad", 0).is_err());
5506 }
5507
5508 #[test]
5509 fn zero_sentinel_cannot_alias_an_embedding_row() {
5510 EmbeddingLookupPolicy::Strict.validate().unwrap();
5511 EmbeddingLookupPolicy::ZeroSentinel(-1).validate().unwrap();
5512 assert!(EmbeddingLookupPolicy::ZeroSentinel(0).validate().is_err());
5513 }
5514
5515 #[test]
5516 fn vocabulary_parallel_ownership_requires_exact_global_rows() {
5517 let range = VocabularyParallelRange {
5518 global_vocabulary: 5,
5519 local: 0..3,
5520 };
5521 range.validate_global_rows(5).unwrap();
5522 assert!(range.validate_global_rows(4).is_err());
5523 assert!(range.validate_global_rows(-1).is_err());
5524 }
5525}
5526
5527#[derive(Debug, Clone, Copy)]
5529pub struct Rope {
5530 dimensions: i32,
5531 traditional: bool,
5532 base: f32,
5533 scale: f32,
5534}
5535
5536impl Rope {
5537 pub const fn new(dimensions: i32, traditional: bool, base: f32, scale: f32) -> Self {
5539 Self {
5540 dimensions,
5541 traditional,
5542 base,
5543 scale,
5544 }
5545 }
5546
5547 pub fn forward<T: Tensor>(
5549 &self,
5550 input: &T,
5551 offset: i32,
5552 context: &T::Context,
5553 ) -> Result<T, Error> {
5554 T::rope(
5555 input,
5556 self.dimensions,
5557 self.traditional,
5558 self.base,
5559 self.scale,
5560 offset,
5561 context,
5562 )
5563 }
5564}