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 sequence_layout;
21
22#[derive(Debug, Clone, thiserror::Error)]
24#[error("{message}")]
25pub struct Error {
26 message: String,
27}
28
29impl Error {
30 pub fn backend(error: impl std::fmt::Display) -> Self {
33 Self {
34 message: error.to_string(),
35 }
36 }
37}
38
39#[derive(Debug, Clone, Copy, Eq, PartialEq)]
41pub enum Index {
42 Full,
44 At(i32),
46 Range(i32, i32),
48}
49
50#[derive(Debug, Clone, Copy, Eq, PartialEq)]
52pub enum PadMode {
53 Constant,
55 Edge,
57}
58
59#[derive(Debug, Clone, Copy)]
61pub enum AttentionMask<'a, T> {
62 None,
64 Causal,
66 Tensor(&'a T),
68}
69
70#[derive(Debug, Clone, Copy, Eq, PartialEq)]
74pub struct HeadExpansion {
75 pub axis: usize,
77 pub source_heads: i32,
79 pub target_heads: i32,
81}
82
83impl HeadExpansion {
84 pub fn validate<T: Tensor>(&self, input: &T) -> Result<(), Error> {
86 let shape = input.shape();
87 if self.source_heads <= 0
88 || self.target_heads <= 0
89 || self.target_heads % self.source_heads != 0
90 || shape.get(self.axis).copied() != Some(self.source_heads)
91 {
92 return Err(Error::backend(format!(
93 "invalid head expansion axis={} source={} target={} shape={shape:?}",
94 self.axis, self.source_heads, self.target_heads
95 )));
96 }
97 Ok(())
98 }
99
100 pub const fn repeats(self) -> i32 {
102 self.target_heads / self.source_heads
103 }
104}
105
106#[derive(Debug, Clone, Copy)]
108pub struct SegmentedAttentionInput<'a, T> {
109 pub queries: &'a T,
111 pub keys: &'a T,
113 pub values: &'a T,
115 pub segment_lengths: &'a [i32],
117 pub scale: f32,
119}
120
121impl<T: Tensor> SegmentedAttentionInput<'_, T> {
122 pub fn validate(&self) -> Result<(), Error> {
124 let query = self.queries.shape();
125 let key = self.keys.shape();
126 let value = self.values.shape();
127 if query.len() != 3
128 || key.len() != 3
129 || value.len() != 3
130 || query[0] <= 0
131 || query[1] <= 0
132 || query[2] <= 0
133 || query[0] != key[0]
134 || query[0] != value[0]
135 || query[1] != key[1]
136 || query[1] != value[1]
137 || query[2] != key[2]
138 || value[2] <= 0
139 || !self.scale.is_finite()
140 || self.scale <= 0.0
141 {
142 return Err(Error::backend(format!(
143 "invalid segmented attention geometry q={query:?} k={key:?} v={value:?} scale={}",
144 self.scale
145 )));
146 }
147 validate_segment_lengths(query[0], self.segment_lengths)
148 }
149}
150
151pub fn validate_segment_lengths(total: i32, segment_lengths: &[i32]) -> Result<(), Error> {
153 if total <= 0 || segment_lengths.is_empty() {
154 return Err(Error::backend(format!(
155 "segmented attention requires a positive total and at least one segment, got total={total} segments={segment_lengths:?}"
156 )));
157 }
158 let mut sum = 0i32;
159 for &length in segment_lengths {
160 if length <= 0 {
161 return Err(Error::backend(format!(
162 "segmented attention lengths must be positive, got {segment_lengths:?}"
163 )));
164 }
165 sum = sum.checked_add(length).ok_or_else(|| {
166 Error::backend("segmented attention length total overflowed signed 32-bit geometry")
167 })?;
168 if sum > total {
169 return Err(Error::backend(format!(
170 "segmented attention lengths exceed total {total}: {segment_lengths:?}"
171 )));
172 }
173 }
174 if sum != total {
175 return Err(Error::backend(format!(
176 "segmented attention lengths sum to {sum}, expected {total}"
177 )));
178 }
179 Ok(())
180}
181
182pub fn reference_expand_heads(
184 values: &[f32],
185 shape: &[usize],
186 axis: usize,
187 target_heads: usize,
188) -> Result<(Vec<f32>, Vec<usize>), Error> {
189 let source_heads = shape.get(axis).copied().unwrap_or(0);
190 if source_heads == 0 || target_heads == 0 || !target_heads.is_multiple_of(source_heads) {
191 return Err(Error::backend(format!(
192 "invalid reference head expansion axis={axis} target={target_heads} shape={shape:?}"
193 )));
194 }
195 let elements = shape.iter().try_fold(1usize, |total, width| {
196 total
197 .checked_mul(*width)
198 .ok_or_else(|| Error::backend("reference head expansion element count overflowed"))
199 })?;
200 if elements != values.len() {
201 return Err(Error::backend(format!(
202 "reference head expansion expected {elements} values, got {}",
203 values.len()
204 )));
205 }
206 let outer = shape[..axis].iter().product::<usize>();
207 let inner = shape[axis + 1..].iter().product::<usize>();
208 let repeats = target_heads / source_heads;
209 let mut output = Vec::with_capacity(outer * target_heads * inner);
210 for outer_index in 0..outer {
211 for source in 0..source_heads {
212 let start = (outer_index * source_heads + source) * inner;
213 for _ in 0..repeats {
214 output.extend_from_slice(&values[start..start + inner]);
215 }
216 }
217 }
218 let mut output_shape = shape.to_vec();
219 output_shape[axis] = target_heads;
220 Ok((output, output_shape))
221}
222
223#[allow(clippy::too_many_arguments)]
227pub fn reference_segmented_attention(
228 tokens: usize,
229 heads: usize,
230 dimensions: usize,
231 value_dimensions: usize,
232 queries: &[f32],
233 keys: &[f32],
234 values: &[f32],
235 segment_lengths: &[i32],
236 scale: f32,
237) -> Result<Vec<f32>, Error> {
238 let tokens_i32 = i32::try_from(tokens)
239 .map_err(|_| Error::backend("reference segmented attention token count exceeds i32"))?;
240 validate_segment_lengths(tokens_i32, segment_lengths)?;
241 if heads == 0
242 || dimensions == 0
243 || value_dimensions == 0
244 || !scale.is_finite()
245 || scale <= 0.0
246 || queries.len() != tokens * heads * dimensions
247 || keys.len() != tokens * heads * dimensions
248 || values.len() != tokens * heads * value_dimensions
249 {
250 return Err(Error::backend(
251 "invalid reference segmented attention geometry",
252 ));
253 }
254 let mut output = vec![0.0f32; tokens * heads * value_dimensions];
255 let mut segment_start = 0usize;
256 for &length in segment_lengths {
257 let length = usize::try_from(length).expect("validated positive segment length");
258 let segment_end = segment_start + length;
259 for query_token in segment_start..segment_end {
260 for head in 0..heads {
261 let mut scores = Vec::with_capacity(length);
262 for key_token in segment_start..segment_end {
263 let mut score = 0.0f32;
264 for dimension in 0..dimensions {
265 let query_index = (query_token * heads + head) * dimensions + dimension;
266 let key_index = (key_token * heads + head) * dimensions + dimension;
267 score += queries[query_index] * keys[key_index];
268 }
269 scores.push(score * scale);
270 }
271 let maximum = scores.iter().copied().fold(f32::NEG_INFINITY, f32::max);
272 let denominator = scores
273 .iter_mut()
274 .map(|score| {
275 *score = (*score - maximum).exp();
276 *score
277 })
278 .sum::<f32>();
279 for value_dimension in 0..value_dimensions {
280 let mut result = 0.0f32;
281 for (relative, key_token) in (segment_start..segment_end).enumerate() {
282 let value_index =
283 (key_token * heads + head) * value_dimensions + value_dimension;
284 result += scores[relative] / denominator * values[value_index];
285 }
286 let output_index =
287 (query_token * heads + head) * value_dimensions + value_dimension;
288 output[output_index] = result;
289 }
290 }
291 }
292 segment_start = segment_end;
293 }
294 Ok(output)
295}
296
297#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
299pub enum AttentionValueSource {
300 Projected,
302 ReuseKey,
304}
305
306#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
308pub enum AttentionStateSource {
309 Local {
311 value: AttentionValueSource,
313 },
314 Publish {
316 value: AttentionValueSource,
318 },
319 Shared,
321}
322
323impl AttentionStateSource {
324 pub const fn owns_state(self) -> bool {
326 !matches!(self, Self::Shared)
327 }
328
329 pub const fn publishes_state(self) -> bool {
331 matches!(self, Self::Publish { .. })
332 }
333
334 pub const fn value(self) -> Option<AttentionValueSource> {
336 match self {
337 Self::Local { value } | Self::Publish { value } => Some(value),
338 Self::Shared => None,
339 }
340 }
341}
342
343#[cfg(test)]
344mod attention_state_source_tests {
345 use super::{AttentionStateSource, AttentionValueSource};
346
347 #[test]
348 fn ownership_publication_and_key_as_value_are_independent() {
349 let local = AttentionStateSource::Local {
350 value: AttentionValueSource::Projected,
351 };
352 let publisher = AttentionStateSource::Publish {
353 value: AttentionValueSource::ReuseKey,
354 };
355 assert!(local.owns_state());
356 assert!(!local.publishes_state());
357 assert_eq!(local.value(), Some(AttentionValueSource::Projected));
358 assert!(publisher.owns_state());
359 assert!(publisher.publishes_state());
360 assert_eq!(publisher.value(), Some(AttentionValueSource::ReuseKey));
361 assert!(!AttentionStateSource::Shared.owns_state());
362 assert_eq!(AttentionStateSource::Shared.value(), None);
363 }
364}
365
366#[cfg(test)]
367mod recurrent_encoder_contract_tests {
368 use super::{
369 reference_expand_heads, reference_segmented_attention, validate_segment_lengths,
370 NormalizationConstructionSpec, NormalizationScale,
371 };
372
373 #[test]
374 fn normalization_construction_rejects_invalid_geometry_and_scalars() {
375 assert!(NormalizationConstructionSpec {
376 dimensions: 8,
377 epsilon: 1e-6,
378 scale: NormalizationScale::Unit,
379 }
380 .validate()
381 .is_ok());
382 assert!(NormalizationConstructionSpec {
383 dimensions: 0,
384 epsilon: 1e-6,
385 scale: NormalizationScale::Unit,
386 }
387 .validate()
388 .is_err());
389 assert!(NormalizationConstructionSpec {
390 dimensions: 8,
391 epsilon: f32::NAN,
392 scale: NormalizationScale::Unit,
393 }
394 .validate()
395 .is_err());
396 }
397
398 #[test]
399 fn head_expansion_reference_preserves_grouped_row_order() {
400 let (values, shape) =
401 reference_expand_heads(&[1.0, 2.0, 3.0, 4.0], &[1, 2, 2], 1, 4).unwrap();
402 assert_eq!(shape, vec![1, 4, 2]);
403 assert_eq!(values, vec![1.0, 2.0, 1.0, 2.0, 3.0, 4.0, 3.0, 4.0]);
404 assert!(reference_expand_heads(&[1.0, 2.0], &[1, 2], 1, 3).is_err());
405 }
406
407 #[test]
408 fn segmented_attention_reference_is_independent_per_contiguous_segment() {
409 let output = reference_segmented_attention(
410 3,
411 1,
412 1,
413 1,
414 &[0.0, 0.0, 0.0],
415 &[0.0, 0.0, 0.0],
416 &[2.0, 4.0, 9.0],
417 &[2, 1],
418 1.0,
419 )
420 .unwrap();
421 assert_eq!(output, vec![3.0, 3.0, 9.0]);
422 assert!(validate_segment_lengths(3, &[]).is_err());
423 assert!(validate_segment_lengths(3, &[2, 0, 1]).is_err());
424 assert!(validate_segment_lengths(3, &[2]).is_err());
425 assert!(validate_segment_lengths(3, &[2, 2]).is_err());
426 assert!(validate_segment_lengths(i32::MAX, &[i32::MAX, 1]).is_err());
427 }
428}
429
430#[derive(Debug, Clone, Copy)]
437pub struct IndexedAttentionInput<'a, T> {
438 pub queries: &'a T,
440 pub local_keys: &'a T,
442 pub local_values: &'a T,
444 pub pooled_keys: &'a T,
446 pub pooled_values: &'a T,
448 pub selected_positions: &'a T,
450 pub scale: f32,
452 pub local_mask: Option<&'a T>,
454 pub pooled_mask: Option<&'a T>,
456 pub sinks: Option<&'a T>,
458}
459
460#[derive(Debug, Clone, Copy)]
462pub struct PooledAttentionInput<'a, T> {
463 pub queries: &'a T,
465 pub local: &'a T,
467 pub pooled: &'a T,
469 pub scale: f32,
471 pub local_mask: Option<&'a T>,
473 pub pooled_mask: Option<&'a T>,
475 pub sinks: Option<&'a T>,
477}
478
479#[derive(Debug, Clone, Copy)]
484pub struct PooledPositionInput<'a, T> {
485 pub queries: &'a T,
487 pub pooled_keys: &'a T,
489 pub head_weights: &'a T,
491 pub mask: Option<&'a T>,
494 pub top_k: i32,
496 pub scale: f32,
498 pub head_scale: f32,
500}
501
502#[derive(Debug, Clone, Copy)]
508pub struct RelativeAttentionInput<'a, T> {
509 pub queries: &'a T,
511 pub keys: &'a T,
513 pub values: &'a T,
515 pub profiles: &'a T,
517 pub query_offset: i32,
519 pub key_offset: i32,
521 pub window: Option<i32>,
523 pub log_scaling_floor: Option<i32>,
525 pub log_scaling_alpha: f32,
527}
528
529impl<T: Tensor> RelativeAttentionInput<'_, T> {
530 pub fn validate(&self) -> Result<(), Error> {
532 let query = self.queries.shape();
533 let key = self.keys.shape();
534 let value = self.values.shape();
535 let profiles = self.profiles.shape();
536 if query.len() != 4
537 || key.len() != 4
538 || value.len() != 4
539 || profiles.len() != 4
540 || query[0] != key[0]
541 || key != value
542 || query[2] != profiles[2]
543 || query[0] != profiles[0]
544 || query[1] != profiles[1]
545 || query[3] != key[3]
546 || query[1] % key[1] != 0
547 || profiles[3] <= 0
548 || self.window.is_some_and(|window| window <= 0)
549 || self.log_scaling_floor.is_some_and(|floor| floor <= 0)
550 || !self.log_scaling_alpha.is_finite()
551 {
552 return Err(Error::backend(format!(
553 "invalid relative attention geometry q={query:?} k={key:?} v={value:?} profiles={profiles:?} window={:?} floor={:?} alpha={}",
554 self.window, self.log_scaling_floor, self.log_scaling_alpha
555 )));
556 }
557 Ok(())
558 }
559}
560
561impl<T: Tensor> IndexedAttentionInput<'_, T> {
562 pub fn validate(&self) -> Result<(), Error> {
565 let query = self.queries.shape();
566 let local_keys = self.local_keys.shape();
567 let local_values = self.local_values.shape();
568 let pooled_keys = self.pooled_keys.shape();
569 let pooled_values = self.pooled_values.shape();
570 let selected = self.selected_positions.shape();
571 if query.len() != 4
572 || local_keys.len() != 3
573 || local_values.len() != 3
574 || pooled_keys.len() != 3
575 || pooled_values.len() != 3
576 || selected.len() != 3
577 || query[0] != local_keys[0]
578 || query[0] != local_values[0]
579 || query[0] != pooled_keys[0]
580 || query[0] != pooled_values[0]
581 || query[0] != selected[0]
582 || query[2] != selected[1]
583 || query[3] != local_keys[2]
584 || query[3] != pooled_keys[2]
585 || local_keys[1] != local_values[1]
586 || pooled_keys[1] != pooled_values[1]
587 || local_values[2] != pooled_values[2]
588 || selected[2] <= 0
589 || pooled_keys[1] <= 0
590 {
591 return Err(Error::backend(format!(
592 "invalid indexed-attention geometry: queries={query:?} local_keys={local_keys:?} local_values={local_values:?} pooled_keys={pooled_keys:?} pooled_values={pooled_values:?} selected={selected:?}"
593 )));
594 }
595 if !self.scale.is_finite() || self.scale <= 0.0 {
596 return Err(Error::backend(format!(
597 "indexed-attention scale must be finite and positive, got {}",
598 self.scale
599 )));
600 }
601 if let Some(sinks) = self.sinks {
602 if sinks.shape() != [query[1]] {
603 return Err(Error::backend(format!(
604 "indexed-attention sinks require shape [{}], got {:?}",
605 query[1],
606 sinks.shape()
607 )));
608 }
609 }
610 Ok(())
611 }
612}
613
614#[derive(Debug, Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
616pub struct ParameterId(String);
617
618impl ParameterId {
619 pub fn new(id: impl Into<String>) -> Result<Self, ParameterTopologyError> {
621 let id = id.into();
622 if id.trim().is_empty() {
623 return Err(ParameterTopologyError::EmptyId);
624 }
625 Ok(Self(id))
626 }
627
628 pub fn as_str(&self) -> &str {
630 &self.0
631 }
632}
633
634impl std::fmt::Display for ParameterId {
635 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636 formatter.write_str(&self.0)
637 }
638}
639
640#[derive(Debug, Clone, Eq, PartialEq)]
642pub struct ParameterSpec {
643 pub id: ParameterId,
645 pub trainable: bool,
647 pub alias_of: Option<ParameterId>,
649 pub group: Option<String>,
651 pub linear_companion: Option<LinearCompanionRole>,
653 pub linear_companion_of: Option<ParameterId>,
655}
656
657impl ParameterSpec {
658 pub fn trainable(id: impl Into<String>) -> Result<Self, ParameterTopologyError> {
660 Ok(Self {
661 id: ParameterId::new(id)?,
662 trainable: true,
663 alias_of: None,
664 group: None,
665 linear_companion: None,
666 linear_companion_of: None,
667 })
668 }
669}
670
671#[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
673pub enum LinearCompanionRole {
674 Scale,
676 AffineBias,
678}
679
680#[derive(Debug, Clone, Eq, PartialEq)]
682pub struct ParameterMetadata {
683 pub id: ParameterId,
685 pub trainable: bool,
687 pub alias_of: Option<ParameterId>,
689 pub group: Option<String>,
691 pub linear_companion: Option<LinearCompanionRole>,
693 pub linear_companion_of: Option<ParameterId>,
695}
696
697impl ParameterMetadata {
698 pub fn from_spec(spec: &ParameterSpec, trainable: bool) -> Self {
700 Self {
701 id: spec.id.clone(),
702 trainable,
703 alias_of: spec.alias_of.clone(),
704 group: spec.group.clone(),
705 linear_companion: spec.linear_companion,
706 linear_companion_of: spec.linear_companion_of.clone(),
707 }
708 }
709}
710
711#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
713pub enum ParameterTopologyError {
714 #[error("parameter identity must not be empty")]
716 EmptyId,
717 #[error("parameter identity {0} is duplicated")]
719 DuplicateId(ParameterId),
720 #[error("parameter alias {alias} points to missing destination {destination}")]
722 MissingAliasDestination {
723 alias: ParameterId,
725 destination: ParameterId,
727 },
728 #[error("parameter alias {alias} points to non-authoritative alias {destination}")]
730 AliasTargetsAlias {
731 alias: ParameterId,
733 destination: ParameterId,
735 },
736}
737
738pub trait ParameterVisitor<'a, T: 'a> {
740 fn visit(&mut self, metadata: ParameterMetadata, value: &'a T);
742}
743
744pub trait ParameterVisitorMut<'a, T: 'a> {
746 fn visit_mut(&mut self, metadata: ParameterMetadata, value: &'a mut T);
748}
749
750pub trait Parameterized<T: 'static> {
752 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
754 where
755 V: ParameterVisitor<'a, T>;
756
757 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
759 where
760 V: ParameterVisitorMut<'a, T>;
761
762 fn set_trainable(&mut self, trainable: bool);
764}
765
766pub fn validate_parameter_topology<T: 'static, M>(
768 module: &M,
769) -> Result<Vec<ParameterMetadata>, ParameterTopologyError>
770where
771 M: Parameterized<T>,
772{
773 struct Collector(Vec<ParameterMetadata>);
774 impl<'a, T: 'a> ParameterVisitor<'a, T> for Collector {
775 fn visit(&mut self, metadata: ParameterMetadata, _value: &'a T) {
776 self.0.push(metadata);
777 }
778 }
779
780 let mut collector = Collector(Vec::new());
781 module.visit_parameters(&mut collector);
782 let mut topology = std::collections::BTreeMap::new();
783 for metadata in &collector.0 {
784 if topology.insert(metadata.id.clone(), metadata).is_some() {
785 return Err(ParameterTopologyError::DuplicateId(metadata.id.clone()));
786 }
787 }
788 for metadata in &collector.0 {
789 let Some(destination) = &metadata.alias_of else {
790 continue;
791 };
792 let Some(target) = topology.get(destination) else {
793 return Err(ParameterTopologyError::MissingAliasDestination {
794 alias: metadata.id.clone(),
795 destination: destination.clone(),
796 });
797 };
798 if target.alias_of.is_some() {
799 return Err(ParameterTopologyError::AliasTargetsAlias {
800 alias: metadata.id.clone(),
801 destination: destination.clone(),
802 });
803 }
804 }
805 Ok(collector.0)
806}
807
808#[derive(Debug, Clone)]
810pub struct LinearSpec {
811 pub input: i32,
813 pub output: i32,
815 pub weight: ParameterSpec,
817 pub bias: Option<ParameterSpec>,
819 pub format: LinearFormatSpec,
821}
822
823#[derive(Debug, Clone)]
825pub struct EmbeddingSpec {
826 pub vocabulary: i32,
828 pub dimensions: i32,
830 pub weight: ParameterSpec,
832 pub format: LinearFormatSpec,
834}
835
836#[derive(Debug, Clone, Eq, PartialEq)]
841pub struct LinearFormatSpec {
842 format: LinearFormat,
843 scale: Option<ParameterSpec>,
844 affine_bias: Option<ParameterSpec>,
845}
846
847impl LinearFormatSpec {
848 pub fn unscaled(format: LinearFormat) -> Result<Self, Error> {
850 let spec = Self {
851 format,
852 scale: None,
853 affine_bias: None,
854 };
855 spec.validate()?;
856 Ok(spec)
857 }
858
859 pub fn scaled(format: LinearFormat, scale: ParameterSpec) -> Result<Self, Error> {
861 let mut scale = scale;
862 scale.linear_companion = Some(LinearCompanionRole::Scale);
863 scale.linear_companion_of = None;
864 let spec = Self {
865 format,
866 scale: Some(scale),
867 affine_bias: None,
868 };
869 spec.validate()?;
870 Ok(spec)
871 }
872
873 pub fn affine(
875 format: LinearFormat,
876 scale: ParameterSpec,
877 affine_bias: ParameterSpec,
878 ) -> Result<Self, Error> {
879 let mut scale = scale;
880 scale.linear_companion = Some(LinearCompanionRole::Scale);
881 scale.linear_companion_of = None;
882 let mut affine_bias = affine_bias;
883 affine_bias.linear_companion = Some(LinearCompanionRole::AffineBias);
884 affine_bias.linear_companion_of = None;
885 let spec = Self {
886 format,
887 scale: Some(scale),
888 affine_bias: Some(affine_bias),
889 };
890 spec.validate()?;
891 Ok(spec)
892 }
893
894 pub const fn encoding(&self) -> LinearFormat {
896 self.format
897 }
898
899 pub const fn scale(&self) -> Option<&ParameterSpec> {
901 self.scale.as_ref()
902 }
903
904 pub const fn affine_bias(&self) -> Option<&ParameterSpec> {
906 self.affine_bias.as_ref()
907 }
908
909 pub fn validate(&self) -> Result<(), Error> {
911 self.format.validate().map_err(Error::backend)?;
912 let expected = match self.format {
913 LinearFormat::Dense | LinearFormat::GgufIQuant { .. } => (false, false),
914 LinearFormat::MxFp4 | LinearFormat::E4M3BlockFp8(_) => (true, false),
915 LinearFormat::Affine(_) => (true, true),
916 };
917 if (self.scale.is_some(), self.affine_bias.is_some()) != expected {
918 return Err(Error::backend(format!(
919 "linear format {:?} requires scale/bias companions {:?}, got {:?}",
920 self.format,
921 expected,
922 (self.scale.is_some(), self.affine_bias.is_some())
923 )));
924 }
925 if self
926 .scale
927 .as_ref()
928 .zip(self.affine_bias.as_ref())
929 .is_some_and(|(scale, bias)| scale.id == bias.id)
930 {
931 return Err(Error::backend(
932 "linear scale and affine-bias companions require distinct identities",
933 ));
934 }
935 if self
936 .scale
937 .as_ref()
938 .is_some_and(|scale| scale.linear_companion != Some(LinearCompanionRole::Scale))
939 || self
940 .affine_bias
941 .as_ref()
942 .is_some_and(|bias| bias.linear_companion != Some(LinearCompanionRole::AffineBias))
943 {
944 return Err(Error::backend(
945 "linear format companions have invalid semantic roles",
946 ));
947 }
948 Ok(())
949 }
950
951 pub fn validate_for_weight(&self, weight: &ParameterSpec) -> Result<(), Error> {
953 self.validate()?;
954 if self
955 .scale
956 .as_ref()
957 .into_iter()
958 .chain(self.affine_bias.as_ref())
959 .any(|companion| companion.id == weight.id)
960 {
961 return Err(Error::backend(format!(
962 "linear format companion reuses primary weight identity {}",
963 weight.id
964 )));
965 }
966 Ok(())
967 }
968}
969
970#[derive(Debug, Clone, Eq, PartialEq)]
972pub struct VocabularyParallelRange {
973 pub global_vocabulary: usize,
975 pub local: std::ops::Range<usize>,
977}
978
979impl VocabularyParallelRange {
980 pub fn validate(&self) -> Result<(), Error> {
982 if self.global_vocabulary == 0
983 || self.local.is_empty()
984 || self.local.end > self.global_vocabulary
985 {
986 return Err(Error::backend(format!(
987 "invalid vocabulary-parallel range {:?} of {}",
988 self.local, self.global_vocabulary
989 )));
990 }
991 Ok(())
992 }
993
994 pub fn validate_global_rows(&self, rows: i32) -> Result<(), Error> {
997 self.validate()?;
998 if usize::try_from(rows).ok() != Some(self.global_vocabulary) {
999 return Err(Error::backend(format!(
1000 "vocabulary-parallel operator declares {rows} rows but ownership covers {}",
1001 self.global_vocabulary
1002 )));
1003 }
1004 Ok(())
1005 }
1006}
1007
1008#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1010pub enum EmbeddingLookupPolicy {
1011 Strict,
1013 ZeroSentinel(i32),
1016}
1017
1018impl EmbeddingLookupPolicy {
1019 pub fn validate(self) -> Result<(), Error> {
1021 if let Self::ZeroSentinel(sentinel) = self {
1022 if sentinel >= 0 {
1023 return Err(Error::backend(format!(
1024 "embedding zero sentinel must be negative, got {sentinel}"
1025 )));
1026 }
1027 }
1028 Ok(())
1029 }
1030}
1031
1032#[derive(Debug, Clone, Eq, PartialEq)]
1034pub struct FusedProjectionSegment {
1035 name: String,
1036 width: i32,
1037}
1038
1039impl FusedProjectionSegment {
1040 pub fn new(name: impl Into<String>, width: i32) -> Result<Self, Error> {
1042 let name = name.into();
1043 if name.trim().is_empty() || width <= 0 {
1044 return Err(Error::backend(format!(
1045 "fused projection segments require a name and positive width, got name={name:?} width={width}"
1046 )));
1047 }
1048 Ok(Self { name, width })
1049 }
1050
1051 pub fn name(&self) -> &str {
1053 &self.name
1054 }
1055
1056 pub const fn width(&self) -> i32 {
1058 self.width
1059 }
1060}
1061
1062#[derive(Debug, Clone, Eq, PartialEq)]
1064pub struct FusedProjectionLayout {
1065 segments: Vec<FusedProjectionSegment>,
1066 output_width: i32,
1067}
1068
1069impl FusedProjectionLayout {
1070 pub fn new(segments: impl IntoIterator<Item = FusedProjectionSegment>) -> Result<Self, Error> {
1072 let segments = segments.into_iter().collect::<Vec<_>>();
1073 if segments.is_empty() {
1074 return Err(Error::backend(
1075 "fused projection layout must contain at least one segment",
1076 ));
1077 }
1078 let mut names = std::collections::BTreeSet::new();
1079 let mut output_width = 0i32;
1080 for segment in &segments {
1081 if !names.insert(segment.name.clone()) {
1082 return Err(Error::backend(format!(
1083 "fused projection segment {:?} is duplicated",
1084 segment.name
1085 )));
1086 }
1087 output_width = output_width.checked_add(segment.width).ok_or_else(|| {
1088 Error::backend("fused projection output width overflowed signed 32-bit geometry")
1089 })?;
1090 }
1091 Ok(Self {
1092 segments,
1093 output_width,
1094 })
1095 }
1096
1097 pub fn segments(&self) -> &[FusedProjectionSegment] {
1099 &self.segments
1100 }
1101
1102 pub const fn output_width(&self) -> i32 {
1104 self.output_width
1105 }
1106
1107 pub fn split<T: Tensor>(&self, output: &T, context: &T::Context) -> Result<Vec<T>, Error> {
1109 let actual = output
1110 .shape()
1111 .last()
1112 .copied()
1113 .ok_or_else(|| Error::backend("fused projection output has no feature axis"))?;
1114 if actual != self.output_width {
1115 return Err(Error::backend(format!(
1116 "fused projection emitted width {actual}, expected {}",
1117 self.output_width
1118 )));
1119 }
1120 let mut start = 0i32;
1121 let mut indexes = vec![Index::Full; output.shape().len()];
1122 self.segments
1123 .iter()
1124 .map(|segment| {
1125 let end = start + segment.width;
1126 let last = indexes.len() - 1;
1127 indexes[last] = Index::Range(start, end);
1128 let selected = output.index(&indexes, context);
1129 start = end;
1130 selected
1131 })
1132 .collect()
1133 }
1134}
1135
1136#[derive(Debug, Clone)]
1142pub enum NormalizationScale {
1143 Learned(ParameterSpec),
1145 LearnedOffset {
1147 weight: ParameterSpec,
1149 offset: f32,
1151 },
1152 Unit,
1154}
1155
1156#[derive(Debug, Clone)]
1158pub struct NormalizationConstructionSpec {
1159 pub dimensions: i32,
1161 pub epsilon: f32,
1163 pub scale: NormalizationScale,
1165}
1166
1167impl NormalizationConstructionSpec {
1168 pub fn learned(dimensions: i32, epsilon: f32, weight: ParameterSpec) -> Self {
1170 Self {
1171 dimensions,
1172 epsilon,
1173 scale: NormalizationScale::Learned(weight),
1174 }
1175 }
1176
1177 pub fn validate(&self) -> Result<(), Error> {
1179 let offset = match &self.scale {
1180 NormalizationScale::LearnedOffset { offset, .. } => Some(*offset),
1181 NormalizationScale::Learned(_) | NormalizationScale::Unit => None,
1182 };
1183 if self.dimensions <= 0
1184 || !self.epsilon.is_finite()
1185 || self.epsilon <= 0.0
1186 || offset.is_some_and(|offset| !offset.is_finite())
1187 {
1188 return Err(Error::backend(format!(
1189 "invalid RMS normalization construction: dimensions={} epsilon={} offset={offset:?}",
1190 self.dimensions, self.epsilon
1191 )));
1192 }
1193 Ok(())
1194 }
1195}
1196
1197#[derive(Debug, Clone, Copy, PartialEq)]
1199pub enum RotaryAlgorithm {
1200 Default,
1202 Linear {
1204 factor: f32,
1206 },
1207 Llama3 {
1209 factor: f32,
1211 low_frequency_factor: f32,
1213 high_frequency_factor: f32,
1215 original_max_positions: i32,
1217 },
1218 Proportional {
1220 factor: f32,
1222 rotary_fraction: f32,
1224 },
1225 Yarn {
1227 factor: f32,
1229 original_max_positions: i32,
1231 beta_fast: f32,
1233 beta_slow: f32,
1235 concentration: f32,
1237 attention_factor: f32,
1239 truncate: bool,
1241 },
1242}
1243
1244impl RotaryAlgorithm {
1245 pub fn validate(self) -> Result<(), Error> {
1247 let positive = |value: f32| value.is_finite() && value > 0.0;
1248 let valid = match self {
1249 Self::Default => true,
1250 Self::Linear { factor } => positive(factor),
1251 Self::Llama3 {
1252 factor,
1253 low_frequency_factor,
1254 high_frequency_factor,
1255 original_max_positions,
1256 } => {
1257 positive(factor)
1258 && positive(low_frequency_factor)
1259 && positive(high_frequency_factor)
1260 && high_frequency_factor > low_frequency_factor
1261 && original_max_positions > 0
1262 }
1263 Self::Proportional {
1264 factor,
1265 rotary_fraction,
1266 } => positive(factor) && positive(rotary_fraction) && rotary_fraction <= 1.0,
1267 Self::Yarn {
1268 factor,
1269 original_max_positions,
1270 beta_fast,
1271 beta_slow,
1272 concentration,
1273 attention_factor,
1274 ..
1275 } => {
1276 positive(factor)
1277 && original_max_positions > 0
1278 && positive(beta_fast)
1279 && positive(beta_slow)
1280 && beta_fast > beta_slow
1281 && positive(concentration)
1282 && attention_factor.is_finite()
1283 && attention_factor >= 0.0
1284 }
1285 };
1286 if valid {
1287 Ok(())
1288 } else {
1289 Err(Error::backend(format!(
1290 "invalid normalized rotary algorithm: {self:?}"
1291 )))
1292 }
1293 }
1294}
1295
1296#[derive(Debug, Clone, Copy)]
1298pub struct RotarySpec {
1299 pub dimensions: i32,
1301 pub base: f32,
1303 pub traditional: bool,
1305 pub algorithm: RotaryAlgorithm,
1307}
1308
1309pub trait LinearOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
1311 fn forward(&mut self, input: &T, context: &T::Context) -> Result<T, Error>;
1313}
1314
1315pub trait EmbeddingOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
1317 fn forward(&mut self, input: &T, context: &T::Context) -> Result<T, Error>;
1319 fn lookup(
1321 &mut self,
1322 input: &T,
1323 policy: EmbeddingLookupPolicy,
1324 context: &T::Context,
1325 ) -> Result<T, Error> {
1326 policy.validate()?;
1327 match policy {
1328 EmbeddingLookupPolicy::Strict => self.forward(input, context),
1329 EmbeddingLookupPolicy::ZeroSentinel(sentinel) => Err(Error::backend(format!(
1330 "embedding backend does not implement zero sentinel {sentinel}"
1331 ))),
1332 }
1333 }
1334 fn as_linear(&mut self, input: &T, context: &T::Context) -> Result<T, Error>;
1336}
1337
1338pub trait NormalizationOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
1340 fn forward(&mut self, input: &T, context: &T::Context) -> Result<T, Error>;
1342}
1343
1344#[derive(Debug, Clone)]
1346pub struct LowRankProjectionSpec {
1347 pub first: Option<LinearSpec>,
1350 pub normalization: NormalizationConstructionSpec,
1352 pub second: LinearSpec,
1354}
1355
1356impl LowRankProjectionSpec {
1357 pub fn validate(&self) -> Result<(), Error> {
1359 let rank = self.normalization.dimensions;
1360 if rank <= 0 {
1361 return Err(Error::backend(format!(
1362 "low-rank normalization dimensions must be positive, got {rank}"
1363 )));
1364 }
1365 if self.second.input != rank {
1366 return Err(Error::backend(format!(
1367 "low-rank second projection expects {} inputs but rank width is {rank}",
1368 self.second.input
1369 )));
1370 }
1371 if let Some(first) = &self.first {
1372 if first.output != rank {
1373 return Err(Error::backend(format!(
1374 "low-rank first projection emits {} values but rank width is {rank}",
1375 first.output
1376 )));
1377 }
1378 }
1379 Ok(())
1380 }
1381}
1382
1383#[derive(Debug, Clone, Parameterized)]
1386#[parameterized(tensor = "B::Tensor")]
1387pub struct LowRankProjection<B: NeuralBackend> {
1388 pub first: Option<B::Linear>,
1390 pub normalization: B::Normalization,
1392 pub second: B::Linear,
1394}
1395
1396impl<B: NeuralBackend> LowRankProjection<B> {
1397 pub fn new(
1400 spec: LowRankProjectionSpec,
1401 context: &<B::Tensor as Tensor>::Context,
1402 ) -> Result<Self, Error> {
1403 spec.validate()?;
1404 Ok(Self {
1405 first: spec
1406 .first
1407 .map(|projection| B::linear(projection, context))
1408 .transpose()?,
1409 normalization: B::normalization(spec.normalization, context)?,
1410 second: B::linear(spec.second, context)?,
1411 })
1412 }
1413
1414 pub fn forward(
1417 &mut self,
1418 input: &B::Tensor,
1419 context: &<B::Tensor as Tensor>::Context,
1420 ) -> Result<B::Tensor, Error> {
1421 let rank = match &mut self.first {
1422 Some(first) => first.forward(input, context)?,
1423 None => input.clone(),
1424 };
1425 let rank = self.normalization.forward(&rank, context)?;
1426 self.second.forward(&rank, context)
1427 }
1428}
1429
1430pub trait RotaryOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
1432 fn forward(
1434 &mut self,
1435 input: &T,
1436 position: RotaryPosition<'_, T>,
1437 context: &T::Context,
1438 ) -> Result<T, Error>;
1439
1440 fn forward_subspace(
1443 &mut self,
1444 input: &T,
1445 subspace: RotarySubspace,
1446 position: RotaryPosition<'_, T>,
1447 context: &T::Context,
1448 ) -> Result<T, Error> {
1449 let width = *input
1450 .shape()
1451 .last()
1452 .ok_or_else(|| Error::backend("rotary input must have a feature axis"))?;
1453 let (start, dimensions) = subspace.resolve(width)?;
1454 if start == 0 && dimensions == width {
1455 return self.forward(input, position, context);
1456 }
1457 let end = start + dimensions;
1458 let mut indexes = vec![Index::Full; input.shape().len()];
1459 indexes[input.shape().len() - 1] = Index::Range(start, end);
1460 let selected = input.index(&indexes, context)?;
1461 let rotated = self.forward(&selected, position, context)?;
1462 let mut pieces = Vec::with_capacity(3);
1463 if start > 0 {
1464 indexes[input.shape().len() - 1] = Index::Range(0, start);
1465 pieces.push(input.index(&indexes, context)?);
1466 }
1467 pieces.push(rotated);
1468 if end < width {
1469 indexes[input.shape().len() - 1] = Index::Range(end, width);
1470 pieces.push(input.index(&indexes, context)?);
1471 }
1472 T::concatenate(&pieces, -1, context)
1473 }
1474}
1475
1476#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1478pub enum RotarySubspace {
1479 Full,
1481 Range {
1483 start: i32,
1485 dimensions: i32,
1487 },
1488}
1489
1490impl RotarySubspace {
1491 fn resolve(self, width: i32) -> Result<(i32, i32), Error> {
1492 let (start, dimensions) = match self {
1493 Self::Full => (0, width),
1494 Self::Range { start, dimensions } => (start, dimensions),
1495 };
1496 if width <= 0
1497 || start < 0
1498 || dimensions <= 0
1499 || dimensions % 2 != 0
1500 || start > width - dimensions
1501 {
1502 return Err(Error::backend(format!(
1503 "rotary subspace start={start} dimensions={dimensions} is invalid for width {width}"
1504 )));
1505 }
1506 Ok((start, dimensions))
1507 }
1508}
1509
1510#[derive(Debug)]
1512pub enum RotaryPosition<'a, T> {
1513 Offset(i32),
1515 Embeddings {
1517 cosine: &'a T,
1519 sine: &'a T,
1521 },
1522}
1523
1524impl<T> Copy for RotaryPosition<'_, T> {}
1525
1526impl<T> Clone for RotaryPosition<'_, T> {
1527 fn clone(&self) -> Self {
1528 *self
1529 }
1530}
1531
1532#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1534#[non_exhaustive]
1535pub enum GroupScoring {
1536 Softmax,
1538 SelectedSoftmax,
1540 Sigmoid,
1542 SqrtSoftplus,
1544}
1545
1546#[derive(Debug, Clone, Copy, PartialEq)]
1548pub struct TopKGroupSelectionSpec {
1549 group_count: i32,
1550 top_k: i32,
1551 scoring: GroupScoring,
1552 normalize_selected: bool,
1553 normalization_epsilon: f32,
1554 coefficient_scale: f32,
1555 selection_partitions: i32,
1556 selected_groups: i32,
1557}
1558
1559#[derive(Debug, Clone)]
1561pub struct TopKGroupSelectorSpec {
1562 input_dimensions: i32,
1564 weight: ParameterSpec,
1566 bias: Option<ParameterSpec>,
1569 correction_bias: Option<ParameterSpec>,
1572 input_transform: Option<SelectorInputTransformSpec>,
1575 coefficient_scale: Option<ParameterSpec>,
1577 format: LinearFormatSpec,
1579 selection: TopKGroupSelectionSpec,
1581}
1582
1583#[derive(Debug, Clone)]
1585pub struct SelectorInputTransformSpec {
1586 epsilon: f32,
1588 scale: ParameterSpec,
1590 inverse_sqrt_dimensions: bool,
1592}
1593
1594impl SelectorInputTransformSpec {
1595 pub fn new(
1597 epsilon: f32,
1598 scale: ParameterSpec,
1599 inverse_sqrt_dimensions: bool,
1600 ) -> Result<Self, Error> {
1601 if !epsilon.is_finite() || epsilon < 0.0 {
1602 return Err(Error::backend(
1603 "selector input RMS epsilon must be finite and nonnegative",
1604 ));
1605 }
1606 Ok(Self {
1607 epsilon,
1608 scale,
1609 inverse_sqrt_dimensions,
1610 })
1611 }
1612
1613 pub const fn epsilon(&self) -> f32 {
1615 self.epsilon
1616 }
1617 pub const fn scale(&self) -> &ParameterSpec {
1619 &self.scale
1620 }
1621 pub const fn inverse_sqrt_dimensions(&self) -> bool {
1623 self.inverse_sqrt_dimensions
1624 }
1625}
1626
1627impl TopKGroupSelectorSpec {
1628 pub fn new(
1630 input_dimensions: i32,
1631 weight: ParameterSpec,
1632 format: LinearFormatSpec,
1633 selection: TopKGroupSelectionSpec,
1634 ) -> Result<Self, Error> {
1635 let spec = Self {
1636 input_dimensions,
1637 weight,
1638 bias: None,
1639 correction_bias: None,
1640 input_transform: None,
1641 coefficient_scale: None,
1642 format,
1643 selection,
1644 };
1645 spec.validate()?;
1646 Ok(spec)
1647 }
1648
1649 pub fn with_bias(mut self, bias: ParameterSpec) -> Result<Self, Error> {
1651 self.bias = Some(bias);
1652 self.validate()?;
1653 Ok(self)
1654 }
1655 pub fn with_correction_bias(mut self, bias: ParameterSpec) -> Result<Self, Error> {
1657 self.correction_bias = Some(bias);
1658 self.validate()?;
1659 Ok(self)
1660 }
1661 pub fn with_input_transform(mut self, transform: SelectorInputTransformSpec) -> Self {
1663 self.input_transform = Some(transform);
1664 self
1665 }
1666 pub fn with_coefficient_scale(mut self, scale: ParameterSpec) -> Self {
1668 self.coefficient_scale = Some(scale);
1669 self
1670 }
1671 pub const fn input_dimensions(&self) -> i32 {
1673 self.input_dimensions
1674 }
1675 pub const fn weight(&self) -> &ParameterSpec {
1677 &self.weight
1678 }
1679 pub const fn bias(&self) -> Option<&ParameterSpec> {
1681 self.bias.as_ref()
1682 }
1683 pub const fn correction_bias(&self) -> Option<&ParameterSpec> {
1685 self.correction_bias.as_ref()
1686 }
1687 pub const fn input_transform(&self) -> Option<&SelectorInputTransformSpec> {
1689 self.input_transform.as_ref()
1690 }
1691 pub const fn coefficient_scale(&self) -> Option<&ParameterSpec> {
1693 self.coefficient_scale.as_ref()
1694 }
1695 pub const fn format(&self) -> &LinearFormatSpec {
1697 &self.format
1698 }
1699 pub const fn selection(&self) -> TopKGroupSelectionSpec {
1701 self.selection
1702 }
1703
1704 pub fn validate(&self) -> Result<(), Error> {
1706 self.format.validate_for_weight(&self.weight)?;
1707 if self.input_dimensions <= 0 {
1708 return Err(Error::backend(format!(
1709 "selector input dimensions must be positive, got {}",
1710 self.input_dimensions
1711 )));
1712 }
1713 if self
1714 .input_transform
1715 .as_ref()
1716 .is_some_and(|transform| !transform.epsilon.is_finite() || transform.epsilon < 0.0)
1717 {
1718 return Err(Error::backend(
1719 "selector input RMS epsilon must be finite and nonnegative",
1720 ));
1721 }
1722 if self
1723 .bias
1724 .as_ref()
1725 .zip(self.correction_bias.as_ref())
1726 .is_some_and(|(bias, correction_bias)| bias.id == correction_bias.id)
1727 {
1728 return Err(Error::backend(
1729 "selector projection bias and correction bias require distinct parameter identities",
1730 ));
1731 }
1732 Ok(())
1733 }
1734}
1735
1736impl TopKGroupSelectionSpec {
1737 pub fn new(
1739 group_count: i32,
1740 top_k: i32,
1741 scoring: GroupScoring,
1742 normalize_selected: bool,
1743 ) -> Result<Self, Error> {
1744 if group_count <= 0 {
1745 return Err(Error::backend(format!(
1746 "group count must be positive, got {group_count}"
1747 )));
1748 }
1749 if top_k <= 0 || top_k > group_count {
1750 return Err(Error::backend(format!(
1751 "top-k selection count must be in 1..={group_count}, got {top_k}"
1752 )));
1753 }
1754 Ok(Self {
1755 group_count,
1756 top_k,
1757 scoring,
1758 normalize_selected,
1759 normalization_epsilon: 0.0,
1760 coefficient_scale: 1.0,
1761 selection_partitions: 1,
1762 selected_groups: 1,
1763 })
1764 }
1765
1766 pub fn with_groups(
1768 mut self,
1769 selection_partitions: i32,
1770 selected_groups: i32,
1771 ) -> Result<Self, Error> {
1772 if selection_partitions <= 0
1773 || selected_groups <= 0
1774 || selected_groups > selection_partitions
1775 || self.group_count % selection_partitions != 0
1776 || self.top_k > selected_groups * (self.group_count / selection_partitions)
1777 {
1778 return Err(Error::backend(format!(
1779 "invalid grouped selection geometry: group_count={} top_k={} partitions={selection_partitions} selected_partitions={selected_groups}",
1780 self.group_count, self.top_k
1781 )));
1782 }
1783 self.selection_partitions = selection_partitions;
1784 self.selected_groups = selected_groups;
1785 Ok(self)
1786 }
1787
1788 pub fn with_weight_policy(
1790 mut self,
1791 normalization_epsilon: f32,
1792 coefficient_scale: f32,
1793 ) -> Result<Self, Error> {
1794 if !normalization_epsilon.is_finite()
1795 || normalization_epsilon < 0.0
1796 || !coefficient_scale.is_finite()
1797 || coefficient_scale <= 0.0
1798 {
1799 return Err(Error::backend(
1800 "selection normalization epsilon must be finite and nonnegative and grouped scaling must be finite and positive",
1801 ));
1802 }
1803 self.normalization_epsilon = normalization_epsilon;
1804 self.coefficient_scale = coefficient_scale;
1805 Ok(self)
1806 }
1807
1808 pub const fn group_count(self) -> i32 {
1810 self.group_count
1811 }
1812
1813 pub const fn top_k(self) -> i32 {
1815 self.top_k
1816 }
1817
1818 pub const fn scoring(self) -> GroupScoring {
1820 self.scoring
1821 }
1822
1823 pub const fn normalize_selected(self) -> bool {
1825 self.normalize_selected
1826 }
1827
1828 pub const fn normalization_epsilon(self) -> f32 {
1830 self.normalization_epsilon
1831 }
1832
1833 pub const fn coefficient_scale(self) -> f32 {
1835 self.coefficient_scale
1836 }
1837
1838 pub const fn selection_partitions(self) -> i32 {
1840 self.selection_partitions
1841 }
1842
1843 pub const fn selected_groups(self) -> i32 {
1845 self.selected_groups
1846 }
1847}
1848
1849#[derive(Debug, Clone)]
1851pub struct GroupSelection<T> {
1852 group_indices: T,
1854 selected_scores: T,
1856 coefficients: T,
1858}
1859
1860impl<T> GroupSelection<T> {
1861 pub fn new(group_indices: T, selected_scores: T, coefficients: T) -> Self {
1863 Self {
1864 group_indices,
1865 selected_scores,
1866 coefficients,
1867 }
1868 }
1869 pub const fn group_indices(&self) -> &T {
1871 &self.group_indices
1872 }
1873 pub const fn selected_scores(&self) -> &T {
1875 &self.selected_scores
1876 }
1877 pub const fn coefficients(&self) -> &T {
1879 &self.coefficients
1880 }
1881}
1882
1883#[derive(Debug, Clone, Copy, PartialEq)]
1885pub struct JointGroupSelectionSpec {
1886 selectable_groups: i32,
1887 always_on_groups: i32,
1888 top_k: i32,
1889 coefficient_scale: f32,
1890}
1891
1892impl JointGroupSelectionSpec {
1893 pub fn new(
1895 selectable_groups: i32,
1896 always_on_groups: i32,
1897 top_k: i32,
1898 coefficient_scale: f32,
1899 ) -> Result<Self, Error> {
1900 if selectable_groups <= 0
1901 || always_on_groups <= 0
1902 || top_k <= 0
1903 || top_k > selectable_groups
1904 || !coefficient_scale.is_finite()
1905 || coefficient_scale <= 0.0
1906 {
1907 return Err(Error::backend(format!(
1908 "invalid joint group-selection geometry selectable={selectable_groups} always_on={always_on_groups} top_k={top_k} coefficient_scale={coefficient_scale}"
1909 )));
1910 }
1911 Ok(Self {
1912 selectable_groups,
1913 always_on_groups,
1914 top_k,
1915 coefficient_scale,
1916 })
1917 }
1918
1919 pub const fn selectable_groups(self) -> i32 {
1921 self.selectable_groups
1922 }
1923
1924 pub const fn always_on_groups(self) -> i32 {
1926 self.always_on_groups
1927 }
1928
1929 pub const fn top_k(self) -> i32 {
1931 self.top_k
1932 }
1933
1934 pub const fn coefficient_scale(self) -> f32 {
1936 self.coefficient_scale
1937 }
1938}
1939
1940#[derive(Debug, Clone, Copy)]
1943pub struct JointGroupSelectionInput<'a, T> {
1944 hidden: &'a T,
1946 weight: &'a T,
1948 correction_bias: &'a T,
1950 global_scale: &'a T,
1952 selection: JointGroupSelectionSpec,
1954}
1955
1956impl<'a, T: Tensor> JointGroupSelectionInput<'a, T> {
1957 pub fn new(
1959 hidden: &'a T,
1960 weight: &'a T,
1961 correction_bias: &'a T,
1962 global_scale: &'a T,
1963 selection: JointGroupSelectionSpec,
1964 ) -> Result<Self, Error> {
1965 let input = Self {
1966 hidden,
1967 weight,
1968 correction_bias,
1969 global_scale,
1970 selection,
1971 };
1972 input.validate()?;
1973 Ok(input)
1974 }
1975 pub const fn hidden(&self) -> &'a T {
1977 self.hidden
1978 }
1979 pub const fn weight(&self) -> &'a T {
1981 self.weight
1982 }
1983 pub const fn correction_bias(&self) -> &'a T {
1985 self.correction_bias
1986 }
1987 pub const fn global_scale(&self) -> &'a T {
1989 self.global_scale
1990 }
1991 pub const fn selectable_groups(&self) -> i32 {
1993 self.selection.selectable_groups()
1994 }
1995 pub const fn always_on_groups(&self) -> i32 {
1997 self.selection.always_on_groups()
1998 }
1999 pub const fn top_k(&self) -> i32 {
2001 self.selection.top_k()
2002 }
2003 pub const fn coefficient_scale(&self) -> f32 {
2005 self.selection.coefficient_scale()
2006 }
2007}
2008
2009impl<T: Tensor> JointGroupSelectionInput<'_, T> {
2010 pub fn validate(&self) -> Result<(), Error> {
2012 let hidden = self.hidden.shape();
2013 let weight = self.weight.shape();
2014 let bias = self.correction_bias.shape();
2015 let scale = self.global_scale.shape();
2016 let hidden_width = hidden.last().copied().unwrap_or(0);
2017 if hidden.len() < 2
2018 || weight
2019 != [
2020 self.selectable_groups() + self.always_on_groups(),
2021 hidden_width,
2022 ]
2023 || bias != [self.selectable_groups()]
2024 || scale != [1]
2025 {
2026 return Err(Error::backend(format!(
2027 "invalid joint group selection tensors hidden={hidden:?} weight={weight:?} bias={bias:?} scale={scale:?} selectable={} always_on={} top_k={}",
2028 self.selectable_groups(),
2029 self.always_on_groups(),
2030 self.top_k(),
2031 )));
2032 }
2033 Ok(())
2034 }
2035}
2036
2037#[derive(Debug, Clone)]
2039pub struct JointGroupSelection<T> {
2040 primary_indices: T,
2043 primary_coefficients: T,
2045 always_on_coefficients: T,
2047}
2048
2049impl<T> JointGroupSelection<T> {
2050 pub fn new(primary_indices: T, primary_coefficients: T, always_on_coefficients: T) -> Self {
2052 Self {
2053 primary_indices,
2054 primary_coefficients,
2055 always_on_coefficients,
2056 }
2057 }
2058 pub const fn primary_indices(&self) -> &T {
2060 &self.primary_indices
2061 }
2062 pub const fn primary_coefficients(&self) -> &T {
2064 &self.primary_coefficients
2065 }
2066 pub const fn always_on_coefficients(&self) -> &T {
2068 &self.always_on_coefficients
2069 }
2070}
2071
2072#[derive(Debug, Clone, Copy, Eq, PartialEq)]
2074#[non_exhaustive]
2075pub enum GatedProductActivation {
2076 Silu,
2078 GeluApproximate,
2080}
2081
2082#[derive(Debug, Clone, Copy, PartialEq)]
2084pub struct GatedProductPolicy {
2085 activation: GatedProductActivation,
2086 gate_upper_bound: Option<f32>,
2087 up_absolute_bound: Option<f32>,
2088 sigmoid_multiplier: f32,
2089 up_offset: f32,
2090}
2091
2092impl GatedProductPolicy {
2093 pub fn new(
2095 activation: GatedProductActivation,
2096 gate_upper_bound: Option<f32>,
2097 up_absolute_bound: Option<f32>,
2098 sigmoid_multiplier: f32,
2099 up_offset: f32,
2100 ) -> Result<Self, Error> {
2101 let policy = Self {
2102 activation,
2103 gate_upper_bound,
2104 up_absolute_bound,
2105 sigmoid_multiplier,
2106 up_offset,
2107 };
2108 policy.validate()?;
2109 Ok(policy)
2110 }
2111
2112 pub const fn ordinary_silu() -> Self {
2114 Self {
2115 activation: GatedProductActivation::Silu,
2116 gate_upper_bound: None,
2117 up_absolute_bound: None,
2118 sigmoid_multiplier: 1.0,
2119 up_offset: 0.0,
2120 }
2121 }
2122
2123 pub const fn ordinary_gelu_approximate() -> Self {
2125 Self {
2126 activation: GatedProductActivation::GeluApproximate,
2127 ..Self::ordinary_silu()
2128 }
2129 }
2130
2131 pub fn bounded_silu(bound: f32) -> Result<Self, Error> {
2133 Self::new(
2134 GatedProductActivation::Silu,
2135 Some(bound),
2136 Some(bound),
2137 1.0,
2138 0.0,
2139 )
2140 }
2141
2142 pub fn validate(self) -> Result<(), Error> {
2144 if self
2145 .gate_upper_bound
2146 .is_some_and(|bound| !bound.is_finite() || bound <= 0.0)
2147 || self
2148 .up_absolute_bound
2149 .is_some_and(|bound| !bound.is_finite() || bound <= 0.0)
2150 || !self.sigmoid_multiplier.is_finite()
2151 || self.sigmoid_multiplier <= 0.0
2152 || !self.up_offset.is_finite()
2153 {
2154 return Err(Error::backend(format!(
2155 "invalid gated-product policy: {self:?}"
2156 )));
2157 }
2158 Ok(())
2159 }
2160
2161 pub const fn activation(self) -> GatedProductActivation {
2163 self.activation
2164 }
2165
2166 pub const fn gate_upper_bound(self) -> Option<f32> {
2168 self.gate_upper_bound
2169 }
2170
2171 pub const fn up_absolute_bound(self) -> Option<f32> {
2173 self.up_absolute_bound
2174 }
2175
2176 pub const fn sigmoid_multiplier(self) -> f32 {
2178 self.sigmoid_multiplier
2179 }
2180
2181 pub const fn up_offset(self) -> f32 {
2183 self.up_offset
2184 }
2185}
2186
2187impl Default for GatedProductPolicy {
2188 fn default() -> Self {
2189 Self::ordinary_silu()
2190 }
2191}
2192
2193pub trait GroupSelectionOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
2195 fn select(&mut self, logits: &T, context: &T::Context) -> Result<GroupSelection<T>, Error>;
2197
2198 fn select_indices(
2200 &mut self,
2201 input: &T,
2202 group_indices: &T,
2203 context: &T::Context,
2204 ) -> Result<GroupSelection<T>, Error>;
2205}
2206
2207#[derive(Debug, Clone)]
2209pub struct GatedProductGroupParameters {
2210 gate: GroupedProjectionSpec,
2212 up: GroupedProjectionSpec,
2214 down: GroupedProjectionSpec,
2216}
2217
2218impl GatedProductGroupParameters {
2219 pub fn new(
2221 gate: GroupedProjectionSpec,
2222 up: GroupedProjectionSpec,
2223 down: GroupedProjectionSpec,
2224 ) -> Self {
2225 Self { gate, up, down }
2226 }
2227 pub const fn gate(&self) -> &GroupedProjectionSpec {
2229 &self.gate
2230 }
2231 pub const fn up(&self) -> &GroupedProjectionSpec {
2233 &self.up
2234 }
2235 pub const fn down(&self) -> &GroupedProjectionSpec {
2237 &self.down
2238 }
2239}
2240
2241#[derive(Debug, Clone)]
2243pub struct GroupedProjectionSpec {
2244 weight: ParameterSpec,
2246 bias: Option<ParameterSpec>,
2248 format: LinearFormatSpec,
2250}
2251
2252impl GroupedProjectionSpec {
2253 pub fn new(
2255 weight: ParameterSpec,
2256 bias: Option<ParameterSpec>,
2257 format: LinearFormatSpec,
2258 ) -> Result<Self, Error> {
2259 let spec = Self {
2260 weight,
2261 bias,
2262 format,
2263 };
2264 spec.validate()?;
2265 Ok(spec)
2266 }
2267 pub const fn weight(&self) -> &ParameterSpec {
2269 &self.weight
2270 }
2271 pub const fn bias(&self) -> Option<&ParameterSpec> {
2273 self.bias.as_ref()
2274 }
2275 pub const fn format(&self) -> &LinearFormatSpec {
2277 &self.format
2278 }
2279 fn validate(&self) -> Result<(), Error> {
2280 self.format.validate_for_weight(&self.weight)?;
2281 let parameters = self.parameters();
2282 for (index, parameter) in parameters.iter().enumerate() {
2283 if parameters[index + 1..]
2284 .iter()
2285 .any(|candidate| candidate.id == parameter.id)
2286 {
2287 return Err(Error::backend(format!(
2288 "grouped projection reuses parameter identity {:?}",
2289 parameter.id
2290 )));
2291 }
2292 }
2293 Ok(())
2294 }
2295
2296 fn parameters(&self) -> Vec<&ParameterSpec> {
2297 let mut parameters = vec![&self.weight];
2298 parameters.extend(self.bias.as_ref());
2299 parameters.extend(self.format.scale());
2300 parameters.extend(self.format.affine_bias());
2301 parameters
2302 }
2303}
2304
2305#[derive(Debug, Clone)]
2307#[allow(clippy::large_enum_variant)] #[non_exhaustive]
2309pub enum GatedProductGroupLayout {
2310 Packed {
2313 gate_up: GroupedProjectionSpec,
2315 down: GroupedProjectionSpec,
2317 },
2318 Independent(Vec<GatedProductGroupParameters>),
2320}
2321
2322#[derive(Debug, Clone)]
2324pub struct GroupedGatedProductSpec {
2325 group_count: i32,
2327 input_dimensions: i32,
2329 intermediate_dimensions: i32,
2331 output_dimensions: i32,
2333 policy: GatedProductPolicy,
2335 layout: GatedProductGroupLayout,
2337}
2338
2339impl GroupedGatedProductSpec {
2340 pub fn new(
2342 group_count: i32,
2343 input_dimensions: i32,
2344 intermediate_dimensions: i32,
2345 output_dimensions: i32,
2346 policy: GatedProductPolicy,
2347 layout: GatedProductGroupLayout,
2348 ) -> Result<Self, Error> {
2349 let spec = Self {
2350 group_count,
2351 input_dimensions,
2352 intermediate_dimensions,
2353 output_dimensions,
2354 policy,
2355 layout,
2356 };
2357 spec.validate()?;
2358 Ok(spec)
2359 }
2360 pub fn with_group_geometry(
2362 mut self,
2363 group_count: i32,
2364 intermediate_dimensions: i32,
2365 ) -> Result<Self, Error> {
2366 self.group_count = group_count;
2367 self.intermediate_dimensions = intermediate_dimensions;
2368 self.validate()?;
2369 Ok(self)
2370 }
2371 pub const fn group_count(&self) -> i32 {
2373 self.group_count
2374 }
2375 pub const fn input_dimensions(&self) -> i32 {
2377 self.input_dimensions
2378 }
2379 pub const fn intermediate_dimensions(&self) -> i32 {
2381 self.intermediate_dimensions
2382 }
2383 pub const fn output_dimensions(&self) -> i32 {
2385 self.output_dimensions
2386 }
2387 pub const fn policy(&self) -> GatedProductPolicy {
2389 self.policy
2390 }
2391 pub const fn layout(&self) -> &GatedProductGroupLayout {
2393 &self.layout
2394 }
2395 pub fn validate(&self) -> Result<(), Error> {
2397 for (name, value) in [
2398 ("group_count", self.group_count),
2399 ("input_dimensions", self.input_dimensions),
2400 ("intermediate_dimensions", self.intermediate_dimensions),
2401 ("output_dimensions", self.output_dimensions),
2402 ] {
2403 if value <= 0 {
2404 return Err(Error::backend(format!(
2405 "gated-product group-bank {name} must be positive, got {value}"
2406 )));
2407 }
2408 }
2409 self.policy.validate()?;
2410 if let GatedProductGroupLayout::Independent(groups) = &self.layout {
2411 let expected = usize::try_from(self.group_count).map_err(Error::backend)?;
2412 if groups.len() != expected {
2413 return Err(Error::backend(format!(
2414 "independent gated-product bank has {} groups, expected {expected}",
2415 groups.len()
2416 )));
2417 }
2418 }
2419 let projections = match &self.layout {
2420 GatedProductGroupLayout::Packed { gate_up, down } => vec![gate_up, down],
2421 GatedProductGroupLayout::Independent(groups) => groups
2422 .iter()
2423 .flat_map(|group| [&group.gate, &group.up, &group.down])
2424 .collect(),
2425 };
2426 let mut identities = std::collections::BTreeSet::new();
2427 for projection in projections {
2428 projection.validate()?;
2429 for parameter in projection.parameters() {
2430 let identity = ¶meter.id;
2431 if !identities.insert(identity) {
2432 return Err(Error::backend(format!(
2433 "gated-product group parameter identity {identity} is duplicated"
2434 )));
2435 }
2436 }
2437 }
2438 Ok(())
2439 }
2440}
2441
2442#[derive(Debug, Clone)]
2444pub struct TensorParallelGroupedOutput<T> {
2445 reducible: T,
2447 post_reduce: Option<T>,
2449}
2450
2451impl<T> TensorParallelGroupedOutput<T> {
2452 pub fn new(reducible: T, post_reduce: Option<T>) -> Self {
2454 Self {
2455 reducible,
2456 post_reduce,
2457 }
2458 }
2459 pub const fn reducible(&self) -> &T {
2461 &self.reducible
2462 }
2463 pub const fn post_reduce(&self) -> Option<&T> {
2465 self.post_reduce.as_ref()
2466 }
2467 pub fn into_parts(self) -> (T, Option<T>) {
2469 (self.reducible, self.post_reduce)
2470 }
2471}
2472
2473pub trait GroupedGatedProductOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
2475 fn spec(&self) -> &GroupedGatedProductSpec;
2481
2482 fn forward_grouped(
2484 &mut self,
2485 input: &T,
2486 selections: &GroupSelection<T>,
2487 context: &T::Context,
2488 ) -> Result<T, Error>;
2489}
2490
2491pub trait TensorParallelGroupedGatedProductOperator<T: Tensor>:
2493 GroupedGatedProductOperator<T>
2494{
2495 fn forward_grouped_tensor_parallel(
2498 &mut self,
2499 input: &T,
2500 selections: &GroupSelection<T>,
2501 partitions: usize,
2502 context: &T::Context,
2503 ) -> Result<TensorParallelGroupedOutput<T>, Error>;
2504}
2505
2506#[derive(Debug, Clone)]
2508pub struct GroupedRelu2Spec {
2509 group_count: i32,
2511 hidden_dimensions: i32,
2513 intermediate_dimensions: i32,
2515 up: GroupedProjectionSpec,
2517 down: GroupedProjectionSpec,
2519}
2520
2521impl GroupedRelu2Spec {
2522 pub fn new(
2524 group_count: i32,
2525 hidden_dimensions: i32,
2526 intermediate_dimensions: i32,
2527 up: GroupedProjectionSpec,
2528 down: GroupedProjectionSpec,
2529 ) -> Result<Self, Error> {
2530 let spec = Self {
2531 group_count,
2532 hidden_dimensions,
2533 intermediate_dimensions,
2534 up,
2535 down,
2536 };
2537 spec.validate()?;
2538 Ok(spec)
2539 }
2540 pub const fn group_count(&self) -> i32 {
2542 self.group_count
2543 }
2544 pub const fn hidden_dimensions(&self) -> i32 {
2546 self.hidden_dimensions
2547 }
2548 pub const fn intermediate_dimensions(&self) -> i32 {
2550 self.intermediate_dimensions
2551 }
2552 pub const fn up(&self) -> &GroupedProjectionSpec {
2554 &self.up
2555 }
2556 pub const fn down(&self) -> &GroupedProjectionSpec {
2558 &self.down
2559 }
2560 pub fn validate(&self) -> Result<(), Error> {
2562 if self.group_count <= 0 || self.hidden_dimensions <= 0 || self.intermediate_dimensions <= 0
2563 {
2564 return Err(Error::backend("invalid ReLU2 group-bank geometry"));
2565 }
2566 self.up.validate()?;
2567 self.down.validate()?;
2568 let mut identities = std::collections::BTreeSet::new();
2569 for projection in [&self.up, &self.down] {
2570 for parameter in projection.parameters() {
2571 if !identities.insert(¶meter.id) {
2572 return Err(Error::backend(format!(
2573 "ReLU2 group parameter identity {} is duplicated",
2574 parameter.id
2575 )));
2576 }
2577 }
2578 }
2579 Ok(())
2580 }
2581}
2582
2583pub trait GroupedRelu2Operator<T: Tensor>: Clone + Debug + Parameterized<T> {
2585 fn forward_grouped(
2587 &mut self,
2588 input: &T,
2589 selections: &GroupSelection<T>,
2590 context: &T::Context,
2591 ) -> Result<T, Error>;
2592}
2593
2594pub trait TensorParallelGroupedRelu2Operator<T: Tensor>: GroupedRelu2Operator<T> {
2596 fn forward_grouped_tensor_parallel(
2599 &mut self,
2600 input: &T,
2601 selections: &GroupSelection<T>,
2602 partitions: usize,
2603 context: &T::Context,
2604 ) -> Result<TensorParallelGroupedOutput<T>, Error>;
2605}
2606
2607pub trait GroupedNeuralBackend: NeuralBackend {
2609 type Selector: GroupSelectionOperator<Self::Tensor>;
2611 type GatedProductGroups: GroupedGatedProductOperator<Self::Tensor>;
2613 type Relu2Groups: GroupedRelu2Operator<Self::Tensor>;
2615
2616 fn grouped_linear(
2619 linear: &mut Self::Linear,
2620 input: &Self::Tensor,
2621 groups: i32,
2622 output_per_group: i32,
2623 context: &<Self::Tensor as Tensor>::Context,
2624 ) -> Result<Self::Tensor, Error>;
2625
2626 fn top_k_group_selector(
2628 spec: TopKGroupSelectorSpec,
2629 context: &<Self::Tensor as Tensor>::Context,
2630 ) -> Result<Self::Selector, Error>;
2631
2632 fn grouped_gated_product(
2634 spec: GroupedGatedProductSpec,
2635 context: &<Self::Tensor as Tensor>::Context,
2636 ) -> Result<Self::GatedProductGroups, Error>;
2637
2638 fn grouped_relu2(
2640 spec: GroupedRelu2Spec,
2641 context: &<Self::Tensor as Tensor>::Context,
2642 ) -> Result<Self::Relu2Groups, Error>;
2643
2644 fn joint_group_selection(
2646 input: JointGroupSelectionInput<'_, Self::Tensor>,
2647 context: &<Self::Tensor as Tensor>::Context,
2648 ) -> Result<JointGroupSelection<Self::Tensor>, Error>;
2649}
2650
2651pub trait TensorParallelGroupedNeuralBackend: GroupedNeuralBackend {
2654 fn gated_product_groups_tensor_parallel(
2656 groups: &mut Self::GatedProductGroups,
2657 input: &Self::Tensor,
2658 selections: &GroupSelection<Self::Tensor>,
2659 partitions: usize,
2660 context: &<Self::Tensor as Tensor>::Context,
2661 ) -> Result<TensorParallelGroupedOutput<Self::Tensor>, Error>;
2662
2663 fn relu2_groups_tensor_parallel(
2665 groups: &mut Self::Relu2Groups,
2666 input: &Self::Tensor,
2667 selections: &GroupSelection<Self::Tensor>,
2668 partitions: usize,
2669 context: &<Self::Tensor as Tensor>::Context,
2670 ) -> Result<TensorParallelGroupedOutput<Self::Tensor>, Error>;
2671}
2672
2673impl<B> TensorParallelGroupedNeuralBackend for B
2674where
2675 B: GroupedNeuralBackend,
2676 B::GatedProductGroups: TensorParallelGroupedGatedProductOperator<B::Tensor>,
2677 B::Relu2Groups: TensorParallelGroupedRelu2Operator<B::Tensor>,
2678{
2679 fn gated_product_groups_tensor_parallel(
2680 groups: &mut Self::GatedProductGroups,
2681 input: &Self::Tensor,
2682 selections: &GroupSelection<Self::Tensor>,
2683 partitions: usize,
2684 context: &<Self::Tensor as Tensor>::Context,
2685 ) -> Result<TensorParallelGroupedOutput<Self::Tensor>, Error> {
2686 groups.forward_grouped_tensor_parallel(input, selections, partitions, context)
2687 }
2688
2689 fn relu2_groups_tensor_parallel(
2690 groups: &mut Self::Relu2Groups,
2691 input: &Self::Tensor,
2692 selections: &GroupSelection<Self::Tensor>,
2693 partitions: usize,
2694 context: &<Self::Tensor as Tensor>::Context,
2695 ) -> Result<TensorParallelGroupedOutput<Self::Tensor>, Error> {
2696 groups.forward_grouped_tensor_parallel(input, selections, partitions, context)
2697 }
2698}
2699
2700#[derive(Debug, Clone)]
2702pub struct HyperConnectionSpec {
2703 pub streams: i32,
2705 pub hidden_size: i32,
2707 pub sinkhorn_iterations: usize,
2709 pub epsilon: f32,
2711 pub function: ParameterSpec,
2713 pub base: ParameterSpec,
2715 pub scale: ParameterSpec,
2717}
2718
2719impl HyperConnectionSpec {
2720 pub fn validate(&self) -> Result<(), Error> {
2722 if self.streams <= 0 || self.hidden_size <= 0 {
2723 return Err(Error::backend(
2724 "hyper-connection streams and hidden size must be positive",
2725 ));
2726 }
2727 if self.sinkhorn_iterations == 0 {
2728 return Err(Error::backend(
2729 "hyper-connection Sinkhorn iteration count must be positive",
2730 ));
2731 }
2732 if !self.epsilon.is_finite() || self.epsilon <= 0.0 {
2733 return Err(Error::backend(
2734 "hyper-connection epsilon must be finite and positive",
2735 ));
2736 }
2737 Ok(())
2738 }
2739}
2740
2741#[derive(Debug, Clone)]
2743pub struct HyperHeadSpec {
2744 pub streams: i32,
2746 pub hidden_size: i32,
2748 pub norm_epsilon: f32,
2750 pub epsilon: f32,
2752 pub function: ParameterSpec,
2754 pub base: ParameterSpec,
2756 pub scale: ParameterSpec,
2758}
2759
2760impl HyperHeadSpec {
2761 pub fn validate(&self) -> Result<(), Error> {
2763 if self.streams <= 0 || self.hidden_size <= 0 {
2764 return Err(Error::backend(
2765 "hyper-head streams and hidden size must be positive",
2766 ));
2767 }
2768 if !self.norm_epsilon.is_finite()
2769 || self.norm_epsilon <= 0.0
2770 || !self.epsilon.is_finite()
2771 || self.epsilon <= 0.0
2772 {
2773 return Err(Error::backend(
2774 "hyper-head epsilons must be finite and positive",
2775 ));
2776 }
2777 Ok(())
2778 }
2779}
2780
2781#[derive(Debug, Clone)]
2783pub struct HyperConnectionState<T> {
2784 pub collapsed: T,
2786 pub pre: T,
2788 pub post: T,
2790 pub combination: T,
2792}
2793
2794pub trait HyperConnectionOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
2796 fn collapse(
2799 &mut self,
2800 residual: &T,
2801 norm_epsilon: f32,
2802 context: &T::Context,
2803 ) -> Result<HyperConnectionState<T>, Error>;
2804
2805 fn expand(
2807 &mut self,
2808 sublayer: &T,
2809 residual: &T,
2810 state: &HyperConnectionState<T>,
2811 context: &T::Context,
2812 ) -> Result<T, Error>;
2813}
2814
2815pub trait HyperHeadOperator<T: Tensor>: Clone + Debug + Parameterized<T> {
2817 fn forward(&mut self, residual: &T, context: &T::Context) -> Result<T, Error>;
2819}
2820
2821pub trait HyperNeuralBackend: NeuralBackend {
2823 type HyperConnection: HyperConnectionOperator<Self::Tensor>;
2825 type HyperHead: HyperHeadOperator<Self::Tensor>;
2827
2828 fn hyper_connection(
2830 spec: HyperConnectionSpec,
2831 context: &<Self::Tensor as Tensor>::Context,
2832 ) -> Result<Self::HyperConnection, Error>;
2833
2834 fn hyper_head(
2836 spec: HyperHeadSpec,
2837 context: &<Self::Tensor as Tensor>::Context,
2838 ) -> Result<Self::HyperHead, Error>;
2839}
2840
2841#[derive(Debug, Clone, Parameterized)]
2843#[parameterized(tensor = "B::Tensor")]
2844pub struct HyperConnection<B: HyperNeuralBackend> {
2845 operator: B::HyperConnection,
2846}
2847
2848impl<B: HyperNeuralBackend> HyperConnection<B> {
2849 pub fn new(
2851 spec: HyperConnectionSpec,
2852 context: &<B::Tensor as Tensor>::Context,
2853 ) -> Result<Self, Error> {
2854 spec.validate()?;
2855 Ok(Self {
2856 operator: B::hyper_connection(spec, context)?,
2857 })
2858 }
2859
2860 pub fn collapse(
2862 &mut self,
2863 residual: &B::Tensor,
2864 norm_epsilon: f32,
2865 context: &<B::Tensor as Tensor>::Context,
2866 ) -> Result<HyperConnectionState<B::Tensor>, Error> {
2867 self.operator.collapse(residual, norm_epsilon, context)
2868 }
2869
2870 pub fn expand(
2872 &mut self,
2873 sublayer: &B::Tensor,
2874 residual: &B::Tensor,
2875 state: &HyperConnectionState<B::Tensor>,
2876 context: &<B::Tensor as Tensor>::Context,
2877 ) -> Result<B::Tensor, Error> {
2878 self.operator.expand(sublayer, residual, state, context)
2879 }
2880}
2881
2882#[derive(Debug, Clone, Parameterized)]
2884#[parameterized(tensor = "B::Tensor")]
2885pub struct HyperHead<B: HyperNeuralBackend> {
2886 operator: B::HyperHead,
2887}
2888
2889impl<B: HyperNeuralBackend> HyperHead<B> {
2890 pub fn new(
2892 spec: HyperHeadSpec,
2893 context: &<B::Tensor as Tensor>::Context,
2894 ) -> Result<Self, Error> {
2895 spec.validate()?;
2896 Ok(Self {
2897 operator: B::hyper_head(spec, context)?,
2898 })
2899 }
2900
2901 pub fn forward(
2903 &mut self,
2904 residual: &B::Tensor,
2905 context: &<B::Tensor as Tensor>::Context,
2906 ) -> Result<B::Tensor, Error> {
2907 self.operator.forward(residual, context)
2908 }
2909}
2910
2911#[derive(Debug)]
2918pub struct AttentionRequest<'a, T> {
2919 pub queries: T,
2921 pub keys: T,
2923 pub values: T,
2925 pub scale: f32,
2927 pub mask: Option<&'a T>,
2929 pub sinks: Option<&'a T>,
2931}
2932
2933impl<T: Tensor> AttentionRequest<'_, T> {
2934 pub fn validate(&self) -> Result<(), Error> {
2936 let queries = self.queries.shape();
2937 let keys = self.keys.shape();
2938 let values = self.values.shape();
2939 if queries.len() != 4
2940 || keys.len() != 4
2941 || values.len() != 4
2942 || queries[0] != keys[0]
2943 || keys[..3] != values[..3]
2944 || queries[3] != keys[3]
2945 || queries[1] <= 0
2946 || keys[1] <= 0
2947 || queries[1] % keys[1] != 0
2948 || queries[2] <= 0
2949 || keys[2] <= 0
2950 || values[3] <= 0
2951 || !self.scale.is_finite()
2952 || self.scale <= 0.0
2953 {
2954 return Err(Error::backend(format!(
2955 "invalid attention request geometry queries={queries:?} keys={keys:?} values={values:?} scale={}",
2956 self.scale
2957 )));
2958 }
2959 if let Some(sinks) = self.sinks {
2960 if sinks.shape() != [queries[1]] {
2961 return Err(Error::backend(format!(
2962 "attention sinks require shape [{}], got {:?}",
2963 queries[1],
2964 sinks.shape()
2965 )));
2966 }
2967 }
2968 Ok(())
2969 }
2970}
2971
2972pub trait AttentionCache<T: Tensor> {
2974 fn offset(&self) -> i32;
2976 fn max_size(&self) -> Option<i32>;
2978 fn update_for_attention(
2980 &mut self,
2981 keys: T,
2982 values: T,
2983 context: &T::Context,
2984 ) -> Result<(T, T), Error>;
2985 fn attention(
2987 &mut self,
2988 request: AttentionRequest<'_, T>,
2989 context: &T::Context,
2990 ) -> Result<T, Error>;
2991}
2992
2993pub trait AuxiliaryConvolutionState<T: Tensor>: AttentionCache<T> {
2998 fn convolution_state(&mut self, slot: u32) -> Result<&mut Option<T>, Error>;
3000}
3001
3002#[derive(Debug, Clone)]
3004pub struct CompressedAttentionState<T> {
3005 pub latent: T,
3007 pub rotary: T,
3009}
3010
3011#[derive(Debug, Clone)]
3013pub enum CompressedAttentionView<T> {
3014 Resident(CompressedAttentionState<T>),
3016 Paged {
3019 appended: CompressedAttentionState<T>,
3021 },
3022}
3023
3024impl<T> CompressedAttentionView<T> {
3025 pub const fn resident(&self) -> Option<&CompressedAttentionState<T>> {
3027 match self {
3028 Self::Resident(state) => Some(state),
3029 Self::Paged { .. } => None,
3030 }
3031 }
3032
3033 pub const fn observable(&self) -> &CompressedAttentionState<T> {
3035 match self {
3036 Self::Resident(state) | Self::Paged { appended: state } => state,
3037 }
3038 }
3039
3040 pub const fn is_paged(&self) -> bool {
3042 matches!(self, Self::Paged { .. })
3043 }
3044}
3045
3046#[derive(Debug, Clone)]
3048pub struct CompressedAttentionBlock<T> {
3049 pub start: i64,
3051 pub end: i64,
3053 pub state: CompressedAttentionState<T>,
3055}
3056
3057#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
3059pub struct CompressedAttentionScan {
3060 pub blocks: u64,
3062 pub bytes: u64,
3064 pub reconstruction_scratch_bytes: u64,
3066}
3067
3068#[derive(Debug, Clone, Copy)]
3070pub struct BlockwiseAttentionSpec<'a, T> {
3071 pub queries: &'a T,
3073 pub scale: f32,
3075 pub mask: Option<&'a T>,
3077 pub query_start: i64,
3079 pub context_end: i64,
3081 pub sliding_window: Option<i32>,
3083 pub prefix_tokens: i64,
3085 pub sinks: Option<&'a T>,
3087}
3088
3089pub trait BlockwiseAttentionBackend: NeuralBackend {
3093 type BlockwiseAccumulator;
3095
3096 fn begin_blockwise_attention(
3098 spec: BlockwiseAttentionSpec<'_, Self::Tensor>,
3099 context: &<Self::Tensor as Tensor>::Context,
3100 ) -> Result<Self::BlockwiseAccumulator, Error>;
3101
3102 fn accumulate_blockwise_attention(
3104 accumulator: &mut Self::BlockwiseAccumulator,
3105 start: i64,
3106 end: i64,
3107 keys: Self::Tensor,
3108 values: Self::Tensor,
3109 context: &<Self::Tensor as Tensor>::Context,
3110 ) -> Result<u64, Error>;
3111
3112 fn finish_blockwise_attention(
3114 accumulator: Self::BlockwiseAccumulator,
3115 context: &<Self::Tensor as Tensor>::Context,
3116 ) -> Result<Self::Tensor, Error>;
3117}
3118
3119pub trait CompressedAttentionCache<T: Tensor>: Debug {
3124 type Checkpoint: Clone + Debug;
3126
3127 fn offset(&self) -> i32;
3129 fn is_paged(&self) -> bool;
3131 fn append(
3133 &mut self,
3134 state: CompressedAttentionState<T>,
3135 context: &T::Context,
3136 ) -> Result<CompressedAttentionView<T>, Error>;
3137 fn visit_blocks<F>(
3140 &mut self,
3141 query_tokens: i32,
3142 context: &T::Context,
3143 visitor: F,
3144 ) -> Result<CompressedAttentionScan, Error>
3145 where
3146 F: FnMut(CompressedAttentionBlock<T>) -> Result<u64, Error>;
3147 fn checkpoint(&self) -> Self::Checkpoint;
3149 fn restore(&mut self, checkpoint: &Self::Checkpoint, context: &T::Context)
3151 -> Result<(), Error>;
3152 fn finalize(&mut self) -> Result<(), Error>;
3154 fn clear(&mut self) -> Result<(), Error>;
3156}
3157
3158#[derive(Debug, Clone)]
3160pub struct PoolingWindows<T> {
3161 pub values: T,
3163 pub gates: T,
3165 pub base_position: i32,
3167}
3168
3169#[derive(Debug, Clone)]
3171pub struct PoolingOverlap<T> {
3172 pub values: Option<T>,
3174 pub gates: Option<T>,
3176}
3177
3178pub trait PoolingAttentionCache<T: Tensor>: Debug {
3185 type Checkpoint: Clone + Debug;
3187
3188 fn offset(&self) -> i32;
3190 fn pooling_ratio(&self, stream: u32) -> Option<i32>;
3192 fn append_local(&mut self, keys: T, context: &T::Context) -> Result<T, Error>;
3195 fn local_mask(&self, query_tokens: i32, offset: i32, context: &T::Context) -> Result<T, Error>;
3197 fn accumulate_pooling_windows(
3199 &mut self,
3200 stream: u32,
3201 values: T,
3202 gates: T,
3203 absolute_offset: i32,
3204 context: &T::Context,
3205 ) -> Result<PoolingWindows<T>, Error>;
3206 fn replace_pooling_overlap(
3208 &mut self,
3209 stream: u32,
3210 values: T,
3211 gates: T,
3212 ) -> Result<PoolingOverlap<T>, Error>;
3213 fn append_pooled(&mut self, stream: u32, values: T, context: &T::Context) -> Result<T, Error>;
3215 fn pooling_mask(
3217 &self,
3218 stream: u32,
3219 query_tokens: i32,
3220 offset: i32,
3221 context: &T::Context,
3222 ) -> Result<Option<T>, Error>;
3223 fn checkpoint(&self) -> Self::Checkpoint;
3225 fn restore(&mut self, checkpoint: &Self::Checkpoint, context: &T::Context)
3227 -> Result<(), Error>;
3228 fn finalize(&mut self) -> Result<(), Error>;
3230 fn clear(&mut self) -> Result<(), Error>;
3232}
3233
3234#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
3241pub struct NeuralOperatorCapabilities(u64);
3242
3243impl NeuralOperatorCapabilities {
3244 pub const NONE: Self = Self(0);
3246 pub const GELU_APPROXIMATE: Self = Self(1 << 0);
3248 pub const SIGMOID: Self = Self(1 << 1);
3250 pub const SOFTPLUS: Self = Self(1 << 2);
3252 pub const EXP: Self = Self(1 << 3);
3254 pub const GATED_GROUP_RMS_NORM: Self = Self(1 << 4);
3256 pub const L2_NORMALIZE: Self = Self(1 << 5);
3258 pub const SILU_GATED_GROUP_RMS_NORM: Self = Self(1 << 6);
3260 pub const SEGMENTED_ATTENTION: Self = Self(1 << 7);
3262 pub const GATED_DELTA_SCAN: Self = Self(1 << 8);
3264 pub const SELECTIVE_STATE_SPACE_SCAN: Self = Self(1 << 9);
3266 pub const INDEXED_ATTENTION: Self = Self(1 << 10);
3268 pub const POOLED_ATTENTION: Self = Self(1 << 11);
3270 pub const POOLED_POSITION_SELECTION: Self = Self(1 << 12);
3272 pub const POOLED_MASK_GATHER: Self = Self(1 << 13);
3274 pub const ATTENTION_SINKS: Self = Self(1 << 14);
3276 pub const RELATIVE_ATTENTION: Self = Self(1 << 15);
3278 pub const JOINT_GROUP_SELECTION: Self = Self(1 << 16);
3280 pub const RMS_NORM_WITHOUT_WEIGHT: Self = Self(1 << 17);
3282 pub const GROUPED_LINEAR: Self = Self(1 << 18);
3284 pub const SUM_PARALLEL: Self = Self(1 << 19);
3286 pub const UNLOADED_I32: Self = Self(1 << 20);
3288 pub const FROM_I32_SLICE: Self = Self(1 << 21);
3290 pub const TO_F32_VEC: Self = Self(1 << 22);
3292 pub const TO_I32_VEC: Self = Self(1 << 23);
3294 pub const FULL_F32: Self = Self(1 << 24);
3296 pub const FULL_I32: Self = Self(1 << 25);
3298 pub const TANH: Self = Self(1 << 26);
3300 pub const CLIP: Self = Self(1 << 27);
3302 pub const SOFTMAX_AXIS: Self = Self(1 << 28);
3304 pub const BROADCAST_TO: Self = Self(1 << 29);
3306 pub const ZEROS_LIKE: Self = Self(1 << 30);
3308 pub const EQUAL_I32: Self = Self(1 << 31);
3310 pub const LOGICAL_OR: Self = Self(1 << 32);
3312 pub const WHERE_CONDITION: Self = Self(1 << 33);
3314 pub const MASKED_SCATTER: Self = Self(1 << 34);
3316 pub const ROPE_WITH_FREQUENCIES: Self = Self(1 << 35);
3318 pub const CONV2D: Self = Self(1 << 36);
3320 pub const MULTI_AXIS_ROTARY_EMBEDDINGS: Self = Self(1 << 37);
3322 pub const MASKED_OUTPUT_PROJECTION: Self = Self(1 << 38);
3324 pub const ALL: Self = Self((1 << 39) - 1);
3326
3327 pub const fn union(self, other: Self) -> Self {
3329 Self(self.0 | other.0)
3330 }
3331
3332 pub const fn contains(self, required: Self) -> bool {
3334 self.0 & required.0 == required.0
3335 }
3336
3337 pub fn missing_capability_names(self, required: Self) -> Vec<&'static str> {
3339 const NAMES: &[(NeuralOperatorCapabilities, &str)] = &[
3340 (
3341 NeuralOperatorCapabilities::GELU_APPROXIMATE,
3342 "gelu_approximate",
3343 ),
3344 (NeuralOperatorCapabilities::SIGMOID, "sigmoid"),
3345 (NeuralOperatorCapabilities::SOFTPLUS, "softplus"),
3346 (NeuralOperatorCapabilities::EXP, "exp"),
3347 (
3348 NeuralOperatorCapabilities::GATED_GROUP_RMS_NORM,
3349 "gated_group_rms_norm",
3350 ),
3351 (NeuralOperatorCapabilities::L2_NORMALIZE, "l2_normalize"),
3352 (
3353 NeuralOperatorCapabilities::SILU_GATED_GROUP_RMS_NORM,
3354 "silu_gated_group_rms_norm",
3355 ),
3356 (
3357 NeuralOperatorCapabilities::SEGMENTED_ATTENTION,
3358 "segmented_attention",
3359 ),
3360 (
3361 NeuralOperatorCapabilities::GATED_DELTA_SCAN,
3362 "gated_delta_scan",
3363 ),
3364 (
3365 NeuralOperatorCapabilities::SELECTIVE_STATE_SPACE_SCAN,
3366 "selective_state_space_scan",
3367 ),
3368 (
3369 NeuralOperatorCapabilities::INDEXED_ATTENTION,
3370 "indexed_attention",
3371 ),
3372 (
3373 NeuralOperatorCapabilities::POOLED_ATTENTION,
3374 "pooled_attention",
3375 ),
3376 (
3377 NeuralOperatorCapabilities::POOLED_POSITION_SELECTION,
3378 "select_pooled_positions",
3379 ),
3380 (
3381 NeuralOperatorCapabilities::POOLED_MASK_GATHER,
3382 "gather_pooled_mask",
3383 ),
3384 (
3385 NeuralOperatorCapabilities::ATTENTION_SINKS,
3386 "attention_sinks",
3387 ),
3388 (
3389 NeuralOperatorCapabilities::RELATIVE_ATTENTION,
3390 "relative_attention",
3391 ),
3392 (
3393 NeuralOperatorCapabilities::JOINT_GROUP_SELECTION,
3394 "joint_group_selection",
3395 ),
3396 (
3397 NeuralOperatorCapabilities::RMS_NORM_WITHOUT_WEIGHT,
3398 "rms_norm_without_weight",
3399 ),
3400 (NeuralOperatorCapabilities::GROUPED_LINEAR, "grouped_linear"),
3401 (NeuralOperatorCapabilities::SUM_PARALLEL, "sum_parallel"),
3402 (NeuralOperatorCapabilities::UNLOADED_I32, "unloaded_i32"),
3403 (NeuralOperatorCapabilities::FROM_I32_SLICE, "from_i32_slice"),
3404 (NeuralOperatorCapabilities::TO_F32_VEC, "to_f32_vec"),
3405 (NeuralOperatorCapabilities::TO_I32_VEC, "to_i32_vec"),
3406 (NeuralOperatorCapabilities::FULL_F32, "full_f32"),
3407 (NeuralOperatorCapabilities::FULL_I32, "full_i32"),
3408 (NeuralOperatorCapabilities::TANH, "tanh"),
3409 (NeuralOperatorCapabilities::CLIP, "clip"),
3410 (NeuralOperatorCapabilities::SOFTMAX_AXIS, "softmax_axis"),
3411 (NeuralOperatorCapabilities::BROADCAST_TO, "broadcast_to"),
3412 (NeuralOperatorCapabilities::ZEROS_LIKE, "zeros_like"),
3413 (NeuralOperatorCapabilities::EQUAL_I32, "equal_i32"),
3414 (NeuralOperatorCapabilities::LOGICAL_OR, "logical_or"),
3415 (
3416 NeuralOperatorCapabilities::WHERE_CONDITION,
3417 "where_condition",
3418 ),
3419 (NeuralOperatorCapabilities::MASKED_SCATTER, "masked_scatter"),
3420 (
3421 NeuralOperatorCapabilities::ROPE_WITH_FREQUENCIES,
3422 "rope_with_frequencies",
3423 ),
3424 (NeuralOperatorCapabilities::CONV2D, "conv2d"),
3425 (
3426 NeuralOperatorCapabilities::MULTI_AXIS_ROTARY_EMBEDDINGS,
3427 "multi_axis_rotary_embeddings",
3428 ),
3429 (
3430 NeuralOperatorCapabilities::MASKED_OUTPUT_PROJECTION,
3431 "masked_output_projection",
3432 ),
3433 ];
3434 NAMES
3435 .iter()
3436 .filter_map(|(capability, name)| {
3437 (required.contains(*capability) && !self.contains(*capability)).then_some(*name)
3438 })
3439 .collect()
3440 }
3441}
3442
3443#[cfg(test)]
3444mod neural_operator_capability_tests {
3445 use super::NeuralOperatorCapabilities as C;
3446
3447 #[test]
3448 fn all_includes_every_fail_closed_tensor_operation() {
3449 for (capability, name) in [
3450 (C::UNLOADED_I32, "unloaded_i32"),
3451 (C::FROM_I32_SLICE, "from_i32_slice"),
3452 (C::TO_F32_VEC, "to_f32_vec"),
3453 (C::TO_I32_VEC, "to_i32_vec"),
3454 (C::FULL_F32, "full_f32"),
3455 (C::FULL_I32, "full_i32"),
3456 (C::TANH, "tanh"),
3457 (C::CLIP, "clip"),
3458 (C::SOFTMAX_AXIS, "softmax_axis"),
3459 (C::BROADCAST_TO, "broadcast_to"),
3460 (C::ZEROS_LIKE, "zeros_like"),
3461 (C::EQUAL_I32, "equal_i32"),
3462 (C::LOGICAL_OR, "logical_or"),
3463 (C::WHERE_CONDITION, "where_condition"),
3464 (C::MASKED_SCATTER, "masked_scatter"),
3465 (C::ROPE_WITH_FREQUENCIES, "rope_with_frequencies"),
3466 (C::CONV2D, "conv2d"),
3467 (
3468 C::MULTI_AXIS_ROTARY_EMBEDDINGS,
3469 "multi_axis_rotary_embeddings",
3470 ),
3471 (C::MASKED_OUTPUT_PROJECTION, "masked_output_projection"),
3472 ] {
3473 assert!(C::ALL.contains(capability));
3474 assert_eq!(C::NONE.missing_capability_names(capability), [name]);
3475 }
3476 }
3477}
3478
3479pub trait NeuralBackend: Sized + 'static {
3484 const OPERATOR_CAPABILITIES: NeuralOperatorCapabilities = NeuralOperatorCapabilities::NONE;
3486
3487 type Tensor: Tensor;
3489 type Linear: LinearOperator<Self::Tensor>;
3491 type Embedding: EmbeddingOperator<Self::Tensor>;
3493 type Normalization: NormalizationOperator<Self::Tensor>;
3495 type Rotary: RotaryOperator<Self::Tensor>;
3497 type ParallelContext: ?Sized;
3499
3500 fn require_operator_capabilities(
3503 architecture: &'static str,
3504 required: NeuralOperatorCapabilities,
3505 ) -> Result<(), Error> {
3506 let available = Self::OPERATOR_CAPABILITIES;
3507 if available.contains(required) {
3508 return Ok(());
3509 }
3510 Err(Error::backend(format!(
3511 "{architecture} requires unsupported backend operators: {}",
3512 available.missing_capability_names(required).join(", ")
3513 )))
3514 }
3515
3516 fn linear(
3518 spec: LinearSpec,
3519 context: &<Self::Tensor as Tensor>::Context,
3520 ) -> Result<Self::Linear, Error>;
3521 fn embedding(
3523 spec: EmbeddingSpec,
3524 context: &<Self::Tensor as Tensor>::Context,
3525 ) -> Result<Self::Embedding, Error>;
3526 fn normalization(
3528 spec: NormalizationConstructionSpec,
3529 context: &<Self::Tensor as Tensor>::Context,
3530 ) -> Result<Self::Normalization, Error>;
3531 fn rotary(
3533 spec: RotarySpec,
3534 context: &<Self::Tensor as Tensor>::Context,
3535 ) -> Result<Self::Rotary, Error>;
3536 fn silu(
3538 input: Self::Tensor,
3539 context: &<Self::Tensor as Tensor>::Context,
3540 ) -> Result<Self::Tensor, Error>;
3541 fn gelu_approximate(
3543 input: Self::Tensor,
3544 context: &<Self::Tensor as Tensor>::Context,
3545 ) -> Result<Self::Tensor, Error> {
3546 let _ = (input, context);
3547 Err(Error::backend(
3548 "approximate GELU is not implemented by this backend",
3549 ))
3550 }
3551 fn sigmoid(
3553 input: Self::Tensor,
3554 context: &<Self::Tensor as Tensor>::Context,
3555 ) -> Result<Self::Tensor, Error> {
3556 let _ = (input, context);
3557 Err(Error::backend("sigmoid is not implemented by this backend"))
3558 }
3559 fn softplus(
3561 input: Self::Tensor,
3562 context: &<Self::Tensor as Tensor>::Context,
3563 ) -> Result<Self::Tensor, Error> {
3564 let _ = (input, context);
3565 Err(Error::backend(
3566 "softplus is not implemented by this backend",
3567 ))
3568 }
3569 fn exp(
3571 input: Self::Tensor,
3572 context: &<Self::Tensor as Tensor>::Context,
3573 ) -> Result<Self::Tensor, Error> {
3574 let _ = (input, context);
3575 Err(Error::backend(
3576 "exponential is not implemented by this backend",
3577 ))
3578 }
3579 fn gated_group_rms_norm(
3581 input: &Self::Tensor,
3582 gate: &Self::Tensor,
3583 weight: &Self::Tensor,
3584 groups: i32,
3585 epsilon: f32,
3586 context: &<Self::Tensor as Tensor>::Context,
3587 ) -> Result<Self::Tensor, Error> {
3588 let _ = (input, gate, weight, groups, epsilon, context);
3589 Err(Error::backend(
3590 "gated grouped RMS normalization is not implemented by this backend",
3591 ))
3592 }
3593 fn l2_normalize(
3595 input: &Self::Tensor,
3596 epsilon: f32,
3597 context: &<Self::Tensor as Tensor>::Context,
3598 ) -> Result<Self::Tensor, Error> {
3599 let _ = (input, epsilon, context);
3600 Err(Error::backend(
3601 "L2 normalization is not implemented by this backend",
3602 ))
3603 }
3604 fn silu_gated_group_rms_norm(
3607 input: &Self::Tensor,
3608 gate: &Self::Tensor,
3609 weight: &Self::Tensor,
3610 groups: i32,
3611 epsilon: f32,
3612 context: &<Self::Tensor as Tensor>::Context,
3613 ) -> Result<Self::Tensor, Error> {
3614 let _ = (input, gate, weight, groups, epsilon, context);
3615 Err(Error::backend(
3616 "SiLU-gated grouped RMS normalization is not implemented by this backend",
3617 ))
3618 }
3619 fn expand_heads(
3622 input: &Self::Tensor,
3623 expansion: HeadExpansion,
3624 context: &<Self::Tensor as Tensor>::Context,
3625 ) -> Result<Self::Tensor, Error> {
3626 expansion.validate(input)?;
3627 if expansion.source_heads == expansion.target_heads {
3628 return Ok(input.clone());
3629 }
3630 let mut expanded_shape = input.shape().to_vec();
3631 expanded_shape.insert(expansion.axis + 1, 1);
3632 let expanded = input.reshape(&expanded_shape, context)?;
3633 expanded_shape[expansion.axis + 1] = expansion.repeats();
3634 let expanded = expanded.broadcast_to(&expanded_shape, context)?;
3635 expanded_shape[expansion.axis] = expansion.target_heads;
3636 expanded_shape.remove(expansion.axis + 1);
3637 expanded.reshape(&expanded_shape, context)
3638 }
3639 fn segmented_attention(
3642 input: SegmentedAttentionInput<'_, Self::Tensor>,
3643 context: &<Self::Tensor as Tensor>::Context,
3644 ) -> Result<Self::Tensor, Error> {
3645 input.validate()?;
3646 let _ = context;
3647 Err(Error::backend(
3648 "segmented attention is not implemented by this backend",
3649 ))
3650 }
3651 fn add_residual(
3653 residual: &Self::Tensor,
3654 branch: &Self::Tensor,
3655 fp32: bool,
3656 context: &<Self::Tensor as Tensor>::Context,
3657 ) -> Result<Self::Tensor, Error> {
3658 let _ = fp32;
3659 residual.add(branch, context)
3660 }
3661 fn gated_delta_scan(
3667 input: GatedDeltaScanInput<'_, Self::Tensor>,
3668 context: &<Self::Tensor as Tensor>::Context,
3669 ) -> Result<GatedDeltaScanOutput<Self::Tensor>, Error> {
3670 let _ = (input, context);
3671 Err(Error::backend(
3672 "gated-delta scan is not implemented by this backend",
3673 ))
3674 }
3675 fn selective_state_space_scan(
3677 input: SelectiveStateSpaceScanInput<'_, Self::Tensor>,
3678 context: &<Self::Tensor as Tensor>::Context,
3679 ) -> Result<SelectiveStateSpaceScanOutput<Self::Tensor>, Error> {
3680 let _ = (input, context);
3681 Err(Error::backend(
3682 "selective state-space scan is not implemented by this backend",
3683 ))
3684 }
3685 fn indexed_attention(
3689 input: IndexedAttentionInput<'_, Self::Tensor>,
3690 context: &<Self::Tensor as Tensor>::Context,
3691 ) -> Result<Self::Tensor, Error> {
3692 let _ = (input, context);
3693 Err(Error::backend(
3694 "indexed attention is not implemented by this backend",
3695 ))
3696 }
3697 fn pooled_attention(
3699 input: PooledAttentionInput<'_, Self::Tensor>,
3700 context: &<Self::Tensor as Tensor>::Context,
3701 ) -> Result<Self::Tensor, Error> {
3702 let _ = (input, context);
3703 Err(Error::backend(
3704 "pooled attention is not implemented by this backend",
3705 ))
3706 }
3707 fn select_pooled_positions(
3709 input: PooledPositionInput<'_, Self::Tensor>,
3710 context: &<Self::Tensor as Tensor>::Context,
3711 ) -> Result<Self::Tensor, Error> {
3712 let _ = (input, context);
3713 Err(Error::backend(
3714 "pooled-position selection is not implemented by this backend",
3715 ))
3716 }
3717 fn gather_pooled_mask(
3719 mask: &Self::Tensor,
3720 selected_positions: &Self::Tensor,
3721 context: &<Self::Tensor as Tensor>::Context,
3722 ) -> Result<Self::Tensor, Error> {
3723 let _ = (mask, selected_positions, context);
3724 Err(Error::backend(
3725 "pooled-mask gathering is not implemented by this backend",
3726 ))
3727 }
3728 fn attention_with_sinks(
3730 request: AttentionRequest<'_, Self::Tensor>,
3731 context: &<Self::Tensor as Tensor>::Context,
3732 ) -> Result<Self::Tensor, Error> {
3733 request.validate()?;
3734 if request.sinks.is_some() {
3735 return Err(Error::backend(
3736 "attention sinks are not implemented by this backend",
3737 ));
3738 }
3739 Self::attention(
3740 request.queries,
3741 request.keys,
3742 request.values,
3743 request.scale,
3744 request.mask,
3745 context,
3746 )
3747 }
3748 fn sliding_window_attention_with_sinks(
3750 request: AttentionRequest<'_, Self::Tensor>,
3751 window: i32,
3752 position_offset: i32,
3753 context: &<Self::Tensor as Tensor>::Context,
3754 ) -> Result<Self::Tensor, Error> {
3755 request.validate()?;
3756 if request.sinks.is_some() {
3757 return Err(Error::backend(
3758 "sliding-window attention sinks are not implemented by this backend",
3759 ));
3760 }
3761 Self::sliding_window_attention(
3762 request.queries,
3763 request.keys,
3764 request.values,
3765 request.scale,
3766 window,
3767 position_offset,
3768 context,
3769 )
3770 }
3771 fn relative_attention(
3773 input: RelativeAttentionInput<'_, Self::Tensor>,
3774 context: &<Self::Tensor as Tensor>::Context,
3775 ) -> Result<Self::Tensor, Error> {
3776 let _ = (input, context);
3777 Err(Error::backend(
3778 "relative-profile attention is not implemented by this backend",
3779 ))
3780 }
3781 fn rms_norm_without_weight(
3783 input: &Self::Tensor,
3784 epsilon: f32,
3785 context: &<Self::Tensor as Tensor>::Context,
3786 ) -> Result<Self::Tensor, Error> {
3787 let _ = (input, epsilon, context);
3788 Err(Error::backend(
3789 "weightless RMS normalization is not implemented by this backend",
3790 ))
3791 }
3792 fn rms_norm_with_weight(
3798 input: &Self::Tensor,
3799 weight: &Self::Tensor,
3800 epsilon: f32,
3801 context: &<Self::Tensor as Tensor>::Context,
3802 ) -> Result<Self::Tensor, Error> {
3803 Self::rms_norm_without_weight(input, epsilon, context)?.multiply(weight, context)
3804 }
3805 fn gated_product(
3807 gate: Self::Tensor,
3808 up: Self::Tensor,
3809 policy: GatedProductPolicy,
3810 context: &<Self::Tensor as Tensor>::Context,
3811 ) -> Result<Self::Tensor, Error>;
3812 fn attention(
3814 queries: Self::Tensor,
3815 keys: Self::Tensor,
3816 values: Self::Tensor,
3817 scale: f32,
3818 mask: Option<&Self::Tensor>,
3819 context: &<Self::Tensor as Tensor>::Context,
3820 ) -> Result<Self::Tensor, Error>;
3821 #[allow(clippy::too_many_arguments)]
3823 fn sliding_window_attention(
3824 queries: Self::Tensor,
3825 keys: Self::Tensor,
3826 values: Self::Tensor,
3827 scale: f32,
3828 window: i32,
3829 position_offset: i32,
3830 context: &<Self::Tensor as Tensor>::Context,
3831 ) -> Result<Self::Tensor, Error>;
3832 fn causal_mask(
3837 sequence: i32,
3838 offset: i32,
3839 window: Option<i32>,
3840 context: &<Self::Tensor as Tensor>::Context,
3841 ) -> Result<Self::Tensor, Error>;
3842 fn row_parallel_linear(
3844 linear: &mut Self::Linear,
3845 input: &Self::Tensor,
3846 parallel: &Self::ParallelContext,
3847 context: &<Self::Tensor as Tensor>::Context,
3848 ) -> Result<Self::Tensor, Error>;
3849 fn parallel_size(_parallel: &Self::ParallelContext) -> usize {
3851 1
3852 }
3853}
3854
3855pub trait DistributedNeuralBackend: NeuralBackend {
3861 fn vocabulary_parallel_embedding(
3863 spec: EmbeddingSpec,
3864 range: VocabularyParallelRange,
3865 context: &<Self::Tensor as Tensor>::Context,
3866 ) -> Result<Self::Embedding, Error>;
3867 fn vocabulary_parallel_linear(
3869 spec: LinearSpec,
3870 range: VocabularyParallelRange,
3871 context: &<Self::Tensor as Tensor>::Context,
3872 ) -> Result<Self::Linear, Error>;
3873 fn vocabulary_parallel_lookup(
3875 embedding: &mut Self::Embedding,
3876 input: &Self::Tensor,
3877 policy: EmbeddingLookupPolicy,
3878 parallel: &Self::ParallelContext,
3879 context: &<Self::Tensor as Tensor>::Context,
3880 ) -> Result<Self::Tensor, Error>;
3881 fn vocabulary_parallel_project(
3883 linear: &mut Self::Linear,
3884 input: &Self::Tensor,
3885 parallel: &Self::ParallelContext,
3886 context: &<Self::Tensor as Tensor>::Context,
3887 ) -> Result<Self::Tensor, Error>;
3888 fn vocabulary_parallel_embedding_project(
3890 embedding: &mut Self::Embedding,
3891 input: &Self::Tensor,
3892 parallel: &Self::ParallelContext,
3893 context: &<Self::Tensor as Tensor>::Context,
3894 ) -> Result<Self::Tensor, Error>;
3895 fn sum_parallel(
3897 value: Self::Tensor,
3898 parallel: &Self::ParallelContext,
3899 context: &<Self::Tensor as Tensor>::Context,
3900 ) -> Result<Self::Tensor, Error>;
3901}
3902
3903#[derive(Debug, Clone, Copy)]
3905pub struct GatedDeltaScanInput<'a, T> {
3906 pub query: &'a T,
3908 pub key: &'a T,
3910 pub value: &'a T,
3912 pub log_decay: &'a T,
3914 pub beta: &'a T,
3916 pub initial_state: Option<&'a T>,
3918}
3919
3920#[derive(Debug, Clone)]
3922pub struct GatedDeltaScanOutput<T> {
3923 pub state: T,
3925 pub output: T,
3927}
3928
3929#[derive(Debug, Clone, Copy)]
3931pub struct SelectiveStateSpaceScanInput<'a, T> {
3932 pub values: &'a T,
3934 pub input_state: &'a T,
3936 pub output_state: &'a T,
3938 pub time_step: &'a T,
3940 pub time_step_bias: &'a T,
3942 pub transition_log: &'a T,
3944 pub skip: &'a T,
3946 pub initial_state: Option<&'a T>,
3948 pub time_step_floor: f32,
3950 pub chunk_size: usize,
3952}
3953
3954#[derive(Debug, Clone)]
3956pub struct SelectiveStateSpaceScanOutput<T> {
3957 pub state: T,
3959 pub output: T,
3961}
3962
3963#[allow(clippy::too_many_arguments)]
3965pub fn reference_selective_state_space_scan(
3966 batch: usize,
3967 sequence: usize,
3968 heads: usize,
3969 head_dimensions: usize,
3970 state_dimensions: usize,
3971 values: &[f32],
3972 input_state: &[f32],
3973 output_state: &[f32],
3974 time_step: &[f32],
3975 time_step_bias: &[f32],
3976 transition_log: &[f32],
3977 skip: &[f32],
3978 time_step_floor: f32,
3979 initial_state: Option<&[f32]>,
3980) -> Result<(Vec<f32>, Vec<f32>), Error> {
3981 let groups = batch * sequence * heads;
3982 let values_len = groups * head_dimensions;
3983 let vectors_len = groups * state_dimensions;
3984 let state_len = batch * heads * head_dimensions * state_dimensions;
3985 if values.len() != values_len
3986 || input_state.len() != vectors_len
3987 || output_state.len() != vectors_len
3988 || time_step.len() != groups
3989 || time_step_bias.len() != heads
3990 || transition_log.len() != heads
3991 || skip.len() != heads
3992 || initial_state.is_some_and(|state| state.len() != state_len)
3993 || !time_step_floor.is_finite()
3994 || time_step_floor < 0.0
3995 {
3996 return Err(Error::backend(
3997 "invalid selective state-space reference geometry",
3998 ));
3999 }
4000 let mut state = initial_state.map_or_else(|| vec![0.0; state_len], <[f32]>::to_vec);
4001 let mut output = vec![0.0; values_len];
4002 for batch_index in 0..batch {
4003 for token in 0..sequence {
4004 for head in 0..heads {
4005 let group = (batch_index * sequence + token) * heads + head;
4006 let dt =
4007 ((time_step[group] + time_step_bias[head]).exp().ln_1p()).max(time_step_floor);
4008 let transition = (-transition_log[head].exp() * dt).exp();
4009 let vector_base = group * state_dimensions;
4010 for dimension in 0..head_dimensions {
4011 let value_index = group * head_dimensions + dimension;
4012 let state_base =
4013 (batch_index * heads + head) * head_dimensions * state_dimensions
4014 + dimension * state_dimensions;
4015 let value = values[value_index];
4016 let mut projected = 0.0f32;
4017 for state_dimension in 0..state_dimensions {
4018 let state_index = state_base + state_dimension;
4019 state[state_index] = state[state_index] * transition
4020 + dt * input_state[vector_base + state_dimension] * value;
4021 projected +=
4022 state[state_index] * output_state[vector_base + state_dimension];
4023 }
4024 output[value_index] = projected + value * skip[head];
4025 }
4026 }
4027 }
4028 }
4029 Ok((state, output))
4030}
4031
4032#[allow(clippy::too_many_arguments)]
4038pub fn reference_gated_delta_scan(
4039 batch: usize,
4040 sequence: usize,
4041 heads: usize,
4042 key_dim: usize,
4043 value_dim: usize,
4044 query: &[f32],
4045 key: &[f32],
4046 value: &[f32],
4047 log_decay: &[f32],
4048 vector_decay: bool,
4049 beta: &[f32],
4050 initial_state: Option<&[f32]>,
4051) -> Result<(Vec<f32>, Vec<f32>), Error> {
4052 let key_values = batch * sequence * heads * key_dim;
4053 let values = batch * sequence * heads * value_dim;
4054 let groups = batch * sequence * heads;
4055 let state_values = batch * heads * key_dim * value_dim;
4056 if query.len() != key_values
4057 || key.len() != key_values
4058 || value.len() != values
4059 || beta.len() != groups
4060 || log_decay.len() != if vector_decay { key_values } else { groups }
4061 || initial_state.is_some_and(|state| state.len() != state_values)
4062 {
4063 return Err(Error::backend("invalid gated-delta reference geometry"));
4064 }
4065 let mut state = initial_state.map_or_else(|| vec![0.0; state_values], <[f32]>::to_vec);
4066 let mut output = vec![0.0; values];
4067 for batch_index in 0..batch {
4068 for token in 0..sequence {
4069 for head in 0..heads {
4070 let group = (batch_index * sequence + token) * heads + head;
4071 let state_group = (batch_index * heads + head) * key_dim * value_dim;
4072 for value_index in 0..value_dim {
4073 let mut memory = 0.0f32;
4074 for key_index in 0..key_dim {
4075 let vector_index = group * key_dim + key_index;
4076 let decay = if vector_decay {
4077 log_decay[vector_index]
4078 } else {
4079 log_decay[group]
4080 }
4081 .exp();
4082 let state_index = state_group + key_index * value_dim + value_index;
4083 state[state_index] *= decay;
4084 memory += state[state_index] * key[vector_index];
4085 }
4086 let value_index_flat = group * value_dim + value_index;
4087 let delta = (value[value_index_flat] - memory) * beta[group];
4088 let mut accumulated = 0.0f32;
4089 for key_index in 0..key_dim {
4090 let vector_index = group * key_dim + key_index;
4091 let state_index = state_group + key_index * value_dim + value_index;
4092 state[state_index] += key[vector_index] * delta;
4093 accumulated += state[state_index] * query[vector_index];
4094 }
4095 output[value_index_flat] = accumulated;
4096 }
4097 }
4098 }
4099 }
4100 Ok((state, output))
4101}
4102
4103#[cfg(test)]
4104mod gated_delta_reference_tests {
4105 use super::reference_gated_delta_scan;
4106
4107 #[test]
4108 fn chunked_continuation_matches_one_scan() {
4109 let query = [0.5, -0.25, 0.1, 0.2, -0.4, 0.8];
4110 let key = [0.3, 0.4, -0.2, 0.7, 0.6, -0.1];
4111 let value = [1.0, -0.5, 0.25, 0.75, -0.3, 0.9];
4112 let decay = [-0.2, -0.1, -0.4, -0.3, -0.5, -0.25];
4113 let beta = [0.8, 0.6, 0.4];
4114 let (expected_state, expected) = reference_gated_delta_scan(
4115 1, 3, 1, 2, 2, &query, &key, &value, &decay, true, &beta, None,
4116 )
4117 .unwrap();
4118 let (state, mut actual) = reference_gated_delta_scan(
4119 1,
4120 2,
4121 1,
4122 2,
4123 2,
4124 &query[..4],
4125 &key[..4],
4126 &value[..4],
4127 &decay[..4],
4128 true,
4129 &beta[..2],
4130 None,
4131 )
4132 .unwrap();
4133 let (actual_state, tail) = reference_gated_delta_scan(
4134 1,
4135 1,
4136 1,
4137 2,
4138 2,
4139 &query[4..],
4140 &key[4..],
4141 &value[4..],
4142 &decay[4..],
4143 true,
4144 &beta[2..],
4145 Some(&state),
4146 )
4147 .unwrap();
4148 actual.extend(tail);
4149 assert!(expected
4150 .iter()
4151 .zip(actual)
4152 .all(|(left, right)| (left - right).abs() < 1e-6));
4153 assert!(expected_state
4154 .iter()
4155 .zip(actual_state)
4156 .all(|(left, right)| (left - right).abs() < 1e-6));
4157 }
4158}
4159
4160#[cfg(test)]
4161mod selective_state_space_reference_tests {
4162 use super::reference_selective_state_space_scan;
4163
4164 #[test]
4165 fn continuation_matches_one_scan() {
4166 let values = [0.2, -0.4, 0.8, 0.5, -0.3, 0.7];
4167 let input_state = [0.1, 0.3, -0.2, 0.4, 0.6, -0.5];
4168 let output_state = [0.7, -0.1, 0.2, 0.5, -0.4, 0.9];
4169 let time_step = [-0.3, 0.1, -0.2];
4170 let bias = [0.05];
4171 let transition = [-0.4];
4172 let skip = [0.25];
4173 let (expected_state, expected) = reference_selective_state_space_scan(
4174 1,
4175 3,
4176 1,
4177 2,
4178 2,
4179 &values,
4180 &input_state,
4181 &output_state,
4182 &time_step,
4183 &bias,
4184 &transition,
4185 &skip,
4186 0.001,
4187 None,
4188 )
4189 .unwrap();
4190 let (state, mut actual) = reference_selective_state_space_scan(
4191 1,
4192 2,
4193 1,
4194 2,
4195 2,
4196 &values[..4],
4197 &input_state[..4],
4198 &output_state[..4],
4199 &time_step[..2],
4200 &bias,
4201 &transition,
4202 &skip,
4203 0.001,
4204 None,
4205 )
4206 .unwrap();
4207 let (actual_state, tail) = reference_selective_state_space_scan(
4208 1,
4209 1,
4210 1,
4211 2,
4212 2,
4213 &values[4..],
4214 &input_state[4..],
4215 &output_state[4..],
4216 &time_step[2..],
4217 &bias,
4218 &transition,
4219 &skip,
4220 0.001,
4221 Some(&state),
4222 )
4223 .unwrap();
4224 actual.extend(tail);
4225 assert!(expected
4226 .iter()
4227 .zip(actual)
4228 .all(|(left, right)| (left - right).abs() < 1e-6));
4229 assert!(expected_state
4230 .iter()
4231 .zip(actual_state)
4232 .all(|(left, right)| (left - right).abs() < 1e-6));
4233 }
4234}
4235
4236pub trait Tensor: Clone + Debug + Sized + 'static {
4242 type Context: ?Sized;
4244
4245 fn shape(&self) -> &[i32];
4247
4248 fn dim(&self, axis: usize) -> i32 {
4250 self.shape()[axis]
4251 }
4252
4253 fn unloaded_f32(shape: &[i32], context: &Self::Context) -> Result<Self, Error>;
4255 fn unloaded_i32(shape: &[i32], context: &Self::Context) -> Result<Self, Error> {
4257 let _ = (shape, context);
4258 Err(Error::backend(
4259 "I32 parameter allocation is not implemented by this backend",
4260 ))
4261 }
4262 fn from_f32_slice(
4264 values: &[f32],
4265 shape: &[i32],
4266 context: &Self::Context,
4267 ) -> Result<Self, Error>;
4268 fn from_i32_slice(
4270 values: &[i32],
4271 shape: &[i32],
4272 context: &Self::Context,
4273 ) -> Result<Self, Error> {
4274 let _ = (values, shape, context);
4275 Err(Error::backend(
4276 "I32 tensor construction is not implemented by this backend",
4277 ))
4278 }
4279 fn to_f32_vec(&self, context: &Self::Context) -> Result<Vec<f32>, Error> {
4281 let _ = context;
4282 Err(Error::backend(
4283 "F32 host materialization is not implemented by this backend",
4284 ))
4285 }
4286 fn to_i32_vec(&self, context: &Self::Context) -> Result<Vec<i32>, Error> {
4288 let _ = context;
4289 Err(Error::backend(
4290 "I32 host materialization is not implemented by this backend",
4291 ))
4292 }
4293 fn full_f32(value: f32, shape: &[i32], context: &Self::Context) -> Result<Self, Error> {
4295 let _ = (value, shape, context);
4296 Err(Error::backend(
4297 "filled tensor construction is not implemented by this backend",
4298 ))
4299 }
4300 fn full_i32(value: i32, shape: &[i32], context: &Self::Context) -> Result<Self, Error> {
4302 let _ = (value, shape, context);
4303 Err(Error::backend(
4304 "filled I32 tensor construction is not implemented by this backend",
4305 ))
4306 }
4307 fn add(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4309 fn subtract(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4311 fn multiply(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4313 fn multiply_scalar(&self, rhs: f32, context: &Self::Context) -> Result<Self, Error>;
4315 fn divide(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4317 fn square(&self, context: &Self::Context) -> Result<Self, Error>;
4319 fn tanh(&self, context: &Self::Context) -> Result<Self, Error> {
4321 let _ = context;
4322 Err(Error::backend(
4323 "tanh is not implemented by this tensor backend",
4324 ))
4325 }
4326 fn maximum_scalar(&self, rhs: f32, context: &Self::Context) -> Result<Self, Error>;
4328 fn clip(&self, minimum: &Self, maximum: &Self, context: &Self::Context) -> Result<Self, Error> {
4330 let _ = (minimum, maximum, context);
4331 Err(Error::backend(
4332 "clip is not implemented by this tensor backend",
4333 ))
4334 }
4335
4336 fn softmax_axis(
4338 &self,
4339 axis: i32,
4340 precise: bool,
4341 context: &Self::Context,
4342 ) -> Result<Self, Error> {
4343 let _ = (axis, precise, context);
4344 Err(Error::backend(
4345 "softmax is not implemented by this tensor backend",
4346 ))
4347 }
4348
4349 fn reshape(&self, shape: &[i32], context: &Self::Context) -> Result<Self, Error>;
4351 fn broadcast_to(&self, shape: &[i32], context: &Self::Context) -> Result<Self, Error> {
4353 let _ = (shape, context);
4354 Err(Error::backend(
4355 "broadcasting is not implemented by this tensor backend",
4356 ))
4357 }
4358 fn transpose_axes(&self, axes: &[i32], context: &Self::Context) -> Result<Self, Error>;
4360 fn swap_axes(&self, left: i32, right: i32, context: &Self::Context) -> Result<Self, Error>;
4362 fn transpose(&self, context: &Self::Context) -> Result<Self, Error>;
4364 fn expand_dims(&self, axis: i32, context: &Self::Context) -> Result<Self, Error>;
4366 fn squeeze_axes(&self, axes: &[i32], context: &Self::Context) -> Result<Self, Error>;
4368 fn index(&self, indexes: &[Index], context: &Self::Context) -> Result<Self, Error>;
4370 fn take_axis(&self, indexes: &Self, axis: i32, context: &Self::Context) -> Result<Self, Error>;
4372 fn zeros_like(&self, context: &Self::Context) -> Result<Self, Error> {
4374 let _ = context;
4375 Err(Error::backend(
4376 "dtype-preserving zero allocation is not implemented by this tensor backend",
4377 ))
4378 }
4379 fn equal_i32(&self, value: i32, context: &Self::Context) -> Result<Self, Error> {
4381 let _ = (value, context);
4382 Err(Error::backend(
4383 "integer scalar comparison is not implemented by this tensor backend",
4384 ))
4385 }
4386 fn logical_or(&self, rhs: &Self, context: &Self::Context) -> Result<Self, Error> {
4388 let _ = (rhs, context);
4389 Err(Error::backend(
4390 "logical disjunction is not implemented by this tensor backend",
4391 ))
4392 }
4393 fn where_condition(
4395 condition: &Self,
4396 when_true: &Self,
4397 when_false: &Self,
4398 context: &Self::Context,
4399 ) -> Result<Self, Error> {
4400 let _ = (condition, when_true, when_false, context);
4401 Err(Error::backend(
4402 "conditional selection is not implemented by this tensor backend",
4403 ))
4404 }
4405 fn masked_scatter(
4407 &self,
4408 mask: &Self,
4409 source: &Self,
4410 context: &Self::Context,
4411 ) -> Result<Self, Error> {
4412 let _ = (mask, source, context);
4413 Err(Error::backend(
4414 "masked scatter is not implemented by this tensor backend",
4415 ))
4416 }
4417
4418 fn rope_with_frequencies(
4420 &self,
4421 dimensions: i32,
4422 traditional: bool,
4423 offset: i32,
4424 frequencies: &Self,
4425 context: &Self::Context,
4426 ) -> Result<Self, Error> {
4427 let _ = (dimensions, traditional, offset, frequencies, context);
4428 Err(Error::backend(
4429 "explicit-frequency rotary positions are not implemented by this tensor backend",
4430 ))
4431 }
4432
4433 fn concatenate(values: &[Self], axis: i32, context: &Self::Context) -> Result<Self, Error>;
4435 fn stack(values: &[Self], axis: i32, context: &Self::Context) -> Result<Self, Error>;
4437 fn matmul(lhs: &Self, rhs: &Self, context: &Self::Context) -> Result<Self, Error>;
4439 fn sum_axis(
4441 value: &Self,
4442 axis: i32,
4443 keep_dims: bool,
4444 context: &Self::Context,
4445 ) -> Result<Self, Error>;
4446 fn mean_axis(
4448 value: &Self,
4449 axis: i32,
4450 keep_dims: bool,
4451 context: &Self::Context,
4452 ) -> Result<Self, Error> {
4453 let width = value
4454 .shape()
4455 .get(if axis < 0 {
4456 usize::try_from(value.shape().len() as i32 + axis).unwrap_or(usize::MAX)
4457 } else {
4458 usize::try_from(axis).unwrap_or(usize::MAX)
4459 })
4460 .copied()
4461 .ok_or_else(|| Error::backend(format!("mean axis {axis} is out of range")))?;
4462 Self::sum_axis(value, axis, keep_dims, context)?
4463 .multiply_scalar(1.0 / width as f32, context)
4464 }
4465 fn argmin_axis(
4467 value: &Self,
4468 axis: i32,
4469 keep_dims: bool,
4470 context: &Self::Context,
4471 ) -> Result<Self, Error>;
4472 fn pad(
4474 value: &Self,
4475 widths: &[(i32, i32)],
4476 mode: PadMode,
4477 context: &Self::Context,
4478 ) -> Result<Self, Error>;
4479
4480 #[allow(clippy::too_many_arguments)]
4482 fn conv1d(
4483 input: &Self,
4484 weight: &Self,
4485 stride: i32,
4486 padding: i32,
4487 dilation: i32,
4488 groups: i32,
4489 context: &Self::Context,
4490 ) -> Result<Self, Error>;
4491 #[allow(clippy::too_many_arguments)]
4493 fn conv2d(
4494 input: &Self,
4495 weight: &Self,
4496 stride: (i32, i32),
4497 padding: (i32, i32),
4498 dilation: (i32, i32),
4499 groups: i32,
4500 context: &Self::Context,
4501 ) -> Result<Self, Error> {
4502 let _ = (input, weight, stride, padding, dilation, groups, context);
4503 Err(Error::backend(
4504 "two-dimensional convolution is not implemented by this backend",
4505 ))
4506 }
4507 #[allow(clippy::too_many_arguments)]
4509 fn conv_transpose1d(
4510 input: &Self,
4511 weight: &Self,
4512 stride: i32,
4513 padding: i32,
4514 dilation: i32,
4515 output_padding: i32,
4516 groups: i32,
4517 context: &Self::Context,
4518 ) -> Result<Self, Error>;
4519 fn linear(
4521 input: &Self,
4522 weight: &Self,
4523 bias: Option<&Self>,
4524 context: &Self::Context,
4525 ) -> Result<Self, Error>;
4526 fn layer_norm(
4528 input: &Self,
4529 weight: Option<&Self>,
4530 bias: Option<&Self>,
4531 epsilon: f32,
4532 context: &Self::Context,
4533 ) -> Result<Self, Error>;
4534 fn gelu(input: &Self, context: &Self::Context) -> Result<Self, Error>;
4536 fn elu(input: &Self, alpha: f32, context: &Self::Context) -> Result<Self, Error>;
4538 #[allow(clippy::too_many_arguments)]
4540 fn rope(
4541 input: &Self,
4542 dimensions: i32,
4543 traditional: bool,
4544 base: f32,
4545 scale: f32,
4546 offset: i32,
4547 context: &Self::Context,
4548 ) -> Result<Self, Error>;
4549 fn multi_axis_rotary_embeddings(
4551 position_ids: &Self,
4552 spec: &multimodal::MultiAxisRotarySpec,
4553 context: &Self::Context,
4554 ) -> Result<(Self, Self), Error> {
4555 let _ = (position_ids, spec, context);
4556 Err(Error::backend(
4557 "multi-axis rotary embeddings are not implemented by this backend",
4558 ))
4559 }
4560 fn masked_output_projection(
4562 input: multimodal::MaskedOutputProjectionInput<'_, Self>,
4563 context: &Self::Context,
4564 ) -> Result<Self, Error> {
4565 let _ = (input, context);
4566 Err(Error::backend(
4567 "masked output projection is not implemented by this backend",
4568 ))
4569 }
4570 fn scaled_dot_product_attention(
4572 queries: &Self,
4573 keys: &Self,
4574 values: &Self,
4575 scale: f32,
4576 mask: AttentionMask<'_, Self>,
4577 context: &Self::Context,
4578 ) -> Result<Self, Error>;
4579}
4580
4581#[derive(Debug, Clone)]
4583pub struct Parameter<T> {
4584 spec: ParameterSpec,
4585 trainable: bool,
4586 value: T,
4587}
4588
4589impl<T> Parameter<T> {
4590 pub fn new(spec: ParameterSpec, value: T) -> Self {
4592 let trainable = spec.trainable;
4593 Self {
4594 spec,
4595 trainable,
4596 value,
4597 }
4598 }
4599 pub const fn as_ref(&self) -> &T {
4601 &self.value
4602 }
4603 pub fn replace(&mut self, value: T) {
4605 self.value = value;
4606 }
4607}
4608
4609impl<T: Tensor> Parameter<T> {
4610 pub fn unloaded(
4612 spec: ParameterSpec,
4613 shape: &[i32],
4614 context: &T::Context,
4615 ) -> Result<Self, Error> {
4616 Ok(Self::new(spec, T::unloaded_f32(shape, context)?))
4617 }
4618
4619 pub fn unloaded_i32(
4621 spec: ParameterSpec,
4622 shape: &[i32],
4623 context: &T::Context,
4624 ) -> Result<Self, Error> {
4625 Ok(Self::new(spec, T::unloaded_i32(shape, context)?))
4626 }
4627}
4628
4629impl<T: 'static> Parameterized<T> for Parameter<T> {
4630 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
4631 where
4632 V: ParameterVisitor<'a, T>,
4633 {
4634 visitor.visit(
4635 ParameterMetadata::from_spec(&self.spec, self.trainable),
4636 &self.value,
4637 );
4638 }
4639
4640 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
4641 where
4642 V: ParameterVisitorMut<'a, T>,
4643 {
4644 visitor.visit_mut(
4645 ParameterMetadata::from_spec(&self.spec, self.trainable),
4646 &mut self.value,
4647 );
4648 }
4649
4650 fn set_trainable(&mut self, trainable: bool) {
4651 self.trainable = trainable;
4652 }
4653}
4654
4655impl<T: 'static, M: Parameterized<T>> Parameterized<T> for Vec<M> {
4656 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
4657 where
4658 V: ParameterVisitor<'a, T>,
4659 {
4660 for module in self {
4661 module.visit_parameters(visitor);
4662 }
4663 }
4664
4665 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
4666 where
4667 V: ParameterVisitorMut<'a, T>,
4668 {
4669 for module in self {
4670 module.visit_parameters_mut(visitor);
4671 }
4672 }
4673
4674 fn set_trainable(&mut self, trainable: bool) {
4675 for module in self {
4676 module.set_trainable(trainable);
4677 }
4678 }
4679}
4680
4681impl<T: 'static, M: Parameterized<T>> Parameterized<T> for Option<M> {
4682 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
4683 where
4684 V: ParameterVisitor<'a, T>,
4685 {
4686 if let Some(module) = self {
4687 module.visit_parameters(visitor);
4688 }
4689 }
4690
4691 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
4692 where
4693 V: ParameterVisitorMut<'a, T>,
4694 {
4695 if let Some(module) = self {
4696 module.visit_parameters_mut(visitor);
4697 }
4698 }
4699
4700 fn set_trainable(&mut self, trainable: bool) {
4701 if let Some(module) = self {
4702 module.set_trainable(trainable);
4703 }
4704 }
4705}
4706
4707#[derive(Debug, Clone, Copy, Eq, PartialEq)]
4709pub enum ConvolutionActivation {
4710 Identity,
4712 Silu,
4714}
4715
4716#[derive(Debug, Clone)]
4718pub struct CausalDepthwiseConvolutionSpec {
4719 pub channels: i32,
4721 pub kernel_size: i32,
4723 pub weight: ParameterSpec,
4725 pub bias: Option<ParameterSpec>,
4727 pub activation: ConvolutionActivation,
4729}
4730
4731impl CausalDepthwiseConvolutionSpec {
4732 pub fn validate(&self) -> Result<(), Error> {
4734 if self.channels <= 0 {
4735 return Err(Error::backend(format!(
4736 "causal depthwise convolution channels must be positive, got {}",
4737 self.channels
4738 )));
4739 }
4740 if self.kernel_size <= 0 {
4741 return Err(Error::backend(format!(
4742 "causal depthwise convolution kernel size must be positive, got {}",
4743 self.kernel_size
4744 )));
4745 }
4746 Ok(())
4747 }
4748}
4749
4750#[derive(Debug, Clone)]
4752pub struct CausalDepthwiseConvolutionOutput<T> {
4753 pub output: T,
4755 pub history: Option<T>,
4757}
4758
4759#[derive(Debug, Clone, Parameterized)]
4764#[parameterized(tensor = "B::Tensor")]
4765pub struct CausalDepthwiseConvolution<B: NeuralBackend> {
4766 pub weight: Parameter<B::Tensor>,
4768 pub bias: Option<Parameter<B::Tensor>>,
4770 #[parameter(skip)]
4771 channels: i32,
4772 #[parameter(skip)]
4773 kernel_size: i32,
4774 #[parameter(skip)]
4775 activation: ConvolutionActivation,
4776}
4777
4778impl<B: NeuralBackend> CausalDepthwiseConvolution<B> {
4779 pub fn new(
4781 spec: CausalDepthwiseConvolutionSpec,
4782 context: &<B::Tensor as Tensor>::Context,
4783 ) -> Result<Self, Error> {
4784 spec.validate()?;
4785 Ok(Self {
4786 weight: Parameter::unloaded(
4787 spec.weight,
4788 &[spec.channels, 1, spec.kernel_size],
4789 context,
4790 )?,
4791 bias: spec
4792 .bias
4793 .map(|bias| Parameter::unloaded(bias, &[spec.channels], context))
4794 .transpose()?,
4795 channels: spec.channels,
4796 kernel_size: spec.kernel_size,
4797 activation: spec.activation,
4798 })
4799 }
4800
4801 pub const fn history_len(&self) -> i32 {
4803 self.kernel_size - 1
4804 }
4805
4806 pub fn forward(
4808 &self,
4809 input: &B::Tensor,
4810 history: Option<&B::Tensor>,
4811 context: &<B::Tensor as Tensor>::Context,
4812 ) -> Result<CausalDepthwiseConvolutionOutput<B::Tensor>, Error> {
4813 let shape = input.shape();
4814 if shape.len() != 3 || shape[0] <= 0 || shape[1] <= 0 || shape[2] != self.channels {
4815 return Err(Error::backend(format!(
4816 "causal depthwise convolution expects [batch, sequence, {}], got {shape:?}",
4817 self.channels
4818 )));
4819 }
4820 let history_len = self.history_len();
4821 let padded = if history_len == 0 {
4822 if history.is_some() {
4823 return Err(Error::backend(
4824 "width-one causal convolution does not accept history",
4825 ));
4826 }
4827 input.clone()
4828 } else if let Some(history) = history {
4829 let expected = [shape[0], history_len, self.channels];
4830 if history.shape() != expected {
4831 return Err(Error::backend(format!(
4832 "causal depthwise convolution history must have shape {expected:?}, got {:?}",
4833 history.shape()
4834 )));
4835 }
4836 B::Tensor::concatenate(&[history.clone(), input.clone()], 1, context)?
4837 } else {
4838 B::Tensor::pad(
4839 input,
4840 &[(0, 0), (history_len, 0), (0, 0)],
4841 PadMode::Constant,
4842 context,
4843 )?
4844 };
4845 let execution_weight = self.weight.as_ref().swap_axes(1, 2, context)?;
4846 let mut output =
4847 B::Tensor::conv1d(&padded, &execution_weight, 1, 0, 1, self.channels, context)?;
4848 if output.shape() != shape {
4849 return Err(Error::backend(format!(
4850 "causal depthwise convolution backend returned shape {:?}, expected {shape:?}",
4851 output.shape()
4852 )));
4853 }
4854 if let Some(bias) = &self.bias {
4855 let bias = bias
4856 .as_ref()
4857 .reshape(&[1, 1, self.channels], context)?
4858 .broadcast_to(shape, context)?;
4859 output = output.add(&bias, context)?;
4860 }
4861 if self.activation == ConvolutionActivation::Silu {
4862 output = B::silu(output, context)?;
4863 }
4864 let history = (history_len > 0)
4865 .then(|| {
4866 padded.index(
4867 &[
4868 Index::Full,
4869 Index::Range(shape[1], shape[1] + history_len),
4870 Index::Full,
4871 ],
4872 context,
4873 )
4874 })
4875 .transpose()?;
4876 Ok(CausalDepthwiseConvolutionOutput { output, history })
4877 }
4878}
4879
4880#[derive(Debug, Clone)]
4882pub struct GatedShortConvolutionSpec {
4883 pub input_dimensions: i32,
4885 pub channels: i32,
4887 pub output_dimensions: i32,
4889 pub input_projection: LinearSpec,
4891 pub output_projection: LinearSpec,
4893 pub convolution: CausalDepthwiseConvolutionSpec,
4895}
4896
4897impl GatedShortConvolutionSpec {
4898 pub fn validate(&self) -> Result<(), Error> {
4900 self.convolution.validate()?;
4901 let fused = self
4902 .channels
4903 .checked_mul(3)
4904 .ok_or_else(|| Error::backend("gated short-convolution width overflowed"))?;
4905 if self.input_dimensions <= 0
4906 || self.channels <= 0
4907 || self.output_dimensions <= 0
4908 || self.convolution.channels != self.channels
4909 || self.input_projection.input != self.input_dimensions
4910 || self.input_projection.output != fused
4911 || self.output_projection.input != self.channels
4912 || self.output_projection.output != self.output_dimensions
4913 {
4914 return Err(Error::backend(format!(
4915 "invalid gated short-convolution geometry input={} channels={} output={} fused_projection={}x{} output_projection={}x{} convolution_channels={}",
4916 self.input_dimensions,
4917 self.channels,
4918 self.output_dimensions,
4919 self.input_projection.input,
4920 self.input_projection.output,
4921 self.output_projection.input,
4922 self.output_projection.output,
4923 self.convolution.channels,
4924 )));
4925 }
4926 Ok(())
4927 }
4928}
4929
4930#[derive(Debug, Clone)]
4932pub struct GatedShortConvolutionOutput<T> {
4933 pub output: T,
4935 pub history: Option<T>,
4937}
4938
4939#[derive(Debug, Clone, Parameterized)]
4941#[parameterized(tensor = "B::Tensor")]
4942pub struct GatedShortConvolution<B: NeuralBackend> {
4943 pub input_projection: B::Linear,
4945 pub convolution: CausalDepthwiseConvolution<B>,
4947 pub output_projection: B::Linear,
4949 #[parameter(skip)]
4950 channels: i32,
4951}
4952
4953impl<B: NeuralBackend> GatedShortConvolution<B> {
4954 pub fn new(
4956 spec: GatedShortConvolutionSpec,
4957 context: &<B::Tensor as Tensor>::Context,
4958 ) -> Result<Self, Error> {
4959 spec.validate()?;
4960 Ok(Self {
4961 input_projection: B::linear(spec.input_projection, context)?,
4962 convolution: CausalDepthwiseConvolution::new(spec.convolution, context)?,
4963 output_projection: B::linear(spec.output_projection, context)?,
4964 channels: spec.channels,
4965 })
4966 }
4967
4968 fn hidden(
4969 &mut self,
4970 input: &B::Tensor,
4971 history: Option<&B::Tensor>,
4972 context: &<B::Tensor as Tensor>::Context,
4973 ) -> Result<(B::Tensor, Option<B::Tensor>), Error> {
4974 let projected = self.input_projection.forward(input, context)?;
4975 let rank = projected.shape().len();
4976 if rank == 0 || projected.shape()[rank - 1] != 3 * self.channels {
4977 return Err(Error::backend(format!(
4978 "gated short-convolution projection returned shape {:?}, expected final width {}",
4979 projected.shape(),
4980 3 * self.channels
4981 )));
4982 }
4983 let mut segment = vec![Index::Full; rank];
4984 segment[rank - 1] = Index::Range(0, self.channels);
4985 let b = projected.index(&segment, context)?;
4986 segment[rank - 1] = Index::Range(self.channels, 2 * self.channels);
4987 let c = projected.index(&segment, context)?;
4988 segment[rank - 1] = Index::Range(2 * self.channels, 3 * self.channels);
4989 let x = projected.index(&segment, context)?;
4990 let convolution = self
4991 .convolution
4992 .forward(&b.multiply(&x, context)?, history, context)?;
4993 Ok((
4994 c.multiply(&convolution.output, context)?,
4995 convolution.history,
4996 ))
4997 }
4998
4999 pub fn forward(
5001 &mut self,
5002 input: &B::Tensor,
5003 history: Option<&B::Tensor>,
5004 context: &<B::Tensor as Tensor>::Context,
5005 ) -> Result<GatedShortConvolutionOutput<B::Tensor>, Error> {
5006 let (hidden, history) = self.hidden(input, history, context)?;
5007 Ok(GatedShortConvolutionOutput {
5008 output: self.output_projection.forward(&hidden, context)?,
5009 history,
5010 })
5011 }
5012
5013 pub fn forward_parallel(
5015 &mut self,
5016 input: &B::Tensor,
5017 history: Option<&B::Tensor>,
5018 parallel: &B::ParallelContext,
5019 context: &<B::Tensor as Tensor>::Context,
5020 ) -> Result<GatedShortConvolutionOutput<B::Tensor>, Error> {
5021 let (hidden, history) = self.hidden(input, history, context)?;
5022 Ok(GatedShortConvolutionOutput {
5023 output: B::row_parallel_linear(
5024 &mut self.output_projection,
5025 &hidden,
5026 parallel,
5027 context,
5028 )?,
5029 history,
5030 })
5031 }
5032}
5033
5034#[cfg(test)]
5035mod grouped_contract_tests {
5036 use super::*;
5037
5038 fn dense_format() -> LinearFormatSpec {
5039 LinearFormatSpec::unscaled(LinearFormat::Dense).unwrap()
5040 }
5041
5042 fn parameters(prefix: &str) -> GatedProductGroupParameters {
5043 let projection = |name| {
5044 GroupedProjectionSpec::new(
5045 ParameterSpec::trainable(name).unwrap(),
5046 None,
5047 dense_format(),
5048 )
5049 .unwrap()
5050 };
5051 GatedProductGroupParameters::new(
5052 projection(format!("{prefix}.gate.weight")),
5053 projection(format!("{prefix}.up.weight")),
5054 projection(format!("{prefix}.down.weight")),
5055 )
5056 }
5057
5058 #[test]
5059 fn top_k_selection_policy_rejects_invalid_counts() {
5060 assert!(TopKGroupSelectionSpec::new(8, 2, GroupScoring::Softmax, true).is_ok());
5061 assert!(TopKGroupSelectionSpec::new(0, 1, GroupScoring::Softmax, false).is_err());
5062 assert!(TopKGroupSelectionSpec::new(8, 9, GroupScoring::Softmax, false).is_err());
5063 }
5064
5065 #[test]
5066 fn gated_product_policy_rejects_malformed_scalars() {
5067 assert!(
5068 GatedProductPolicy::new(GatedProductActivation::Silu, Some(0.0), None, 1.0, 0.0,)
5069 .is_err()
5070 );
5071 assert!(GatedProductPolicy::new(
5072 GatedProductActivation::Silu,
5073 None,
5074 Some(f32::NAN),
5075 1.0,
5076 0.0,
5077 )
5078 .is_err());
5079 assert!(
5080 GatedProductPolicy::new(GatedProductActivation::Silu, None, None, 0.0, 0.0,).is_err()
5081 );
5082 assert!(GatedProductPolicy::new(
5083 GatedProductActivation::Silu,
5084 None,
5085 None,
5086 1.0,
5087 f32::INFINITY,
5088 )
5089 .is_err());
5090 }
5091
5092 #[test]
5093 fn selector_projection_and_correction_biases_require_distinct_identities() {
5094 let shared_bias = ParameterSpec::trainable("selector.bias").unwrap();
5095 let spec = TopKGroupSelectorSpec::new(
5096 4,
5097 ParameterSpec::trainable("selector.weight").unwrap(),
5098 dense_format(),
5099 TopKGroupSelectionSpec::new(2, 1, GroupScoring::SelectedSoftmax, false).unwrap(),
5100 )
5101 .unwrap()
5102 .with_bias(shared_bias.clone())
5103 .unwrap();
5104
5105 assert!(spec.with_correction_bias(shared_bias).is_err());
5106 }
5107
5108 #[test]
5109 fn independent_group_layout_requires_exact_cardinality() {
5110 assert!(GroupedGatedProductSpec::new(
5111 2,
5112 16,
5113 8,
5114 16,
5115 eredu_nn::GatedProductPolicy::ordinary_silu(),
5116 GatedProductGroupLayout::Independent(vec![parameters("e0"), parameters("e1")]),
5117 )
5118 .is_ok());
5119 assert!(GroupedGatedProductSpec::new(
5120 2,
5121 16,
5122 8,
5123 16,
5124 eredu_nn::GatedProductPolicy::ordinary_silu(),
5125 GatedProductGroupLayout::Independent(vec![parameters("e0")]),
5126 )
5127 .is_err());
5128 }
5129
5130 #[test]
5131 fn gated_product_bank_rejects_reused_projection_bias_identity() {
5132 let shared = ParameterSpec::trainable("groups.gate_up").unwrap();
5133 let gate_up = GroupedProjectionSpec::new(shared.clone(), Some(shared), dense_format());
5134 assert!(gate_up.is_err());
5135 }
5136
5137 #[test]
5138 fn quantized_group_projection_requires_explicit_companion_identities() {
5139 let format =
5140 LinearFormat::Affine(eredu_checkpoint::AffineQuantization::new(32, 4).unwrap());
5141 let projection = |format| {
5142 GroupedProjectionSpec::new(
5143 ParameterSpec::trainable("arbitrary.group.matrix").unwrap(),
5144 None,
5145 format,
5146 )
5147 };
5148 assert!(LinearFormatSpec::unscaled(format).is_err());
5149 assert!(projection(
5150 LinearFormatSpec::affine(
5151 format,
5152 ParameterSpec::trainable("unrelated.scale.identity").unwrap(),
5153 ParameterSpec::trainable("unrelated.affine.identity").unwrap(),
5154 )
5155 .unwrap()
5156 )
5157 .is_ok());
5158 }
5159}
5160
5161#[derive(Debug, Clone)]
5163pub struct Linear<T> {
5164 pub weight: Parameter<T>,
5166 pub bias: Option<Parameter<T>>,
5168}
5169
5170impl<T: Tensor> Linear<T> {
5171 pub fn unloaded(spec: LinearSpec, context: &T::Context) -> Result<Self, Error> {
5173 Ok(Self {
5174 weight: Parameter::unloaded(spec.weight, &[spec.output, spec.input], context)?,
5175 bias: spec
5176 .bias
5177 .map(|bias| Parameter::unloaded(bias, &[spec.output], context))
5178 .transpose()?,
5179 })
5180 }
5181
5182 pub fn forward(&self, input: &T, context: &T::Context) -> Result<T, Error> {
5184 T::linear(
5185 input,
5186 self.weight.as_ref(),
5187 self.bias.as_ref().map(Parameter::as_ref),
5188 context,
5189 )
5190 }
5191}
5192
5193impl<T: 'static> Parameterized<T> for Linear<T> {
5194 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
5195 where
5196 V: ParameterVisitor<'a, T>,
5197 {
5198 self.weight.visit_parameters(visitor);
5199 if let Some(bias) = &self.bias {
5200 bias.visit_parameters(visitor);
5201 }
5202 }
5203
5204 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
5205 where
5206 V: ParameterVisitorMut<'a, T>,
5207 {
5208 self.weight.visit_parameters_mut(visitor);
5209 if let Some(bias) = &mut self.bias {
5210 bias.visit_parameters_mut(visitor);
5211 }
5212 }
5213
5214 fn set_trainable(&mut self, trainable: bool) {
5215 self.weight.set_trainable(trainable);
5216 if let Some(bias) = &mut self.bias {
5217 bias.set_trainable(trainable);
5218 }
5219 }
5220}
5221
5222#[derive(Debug, Clone)]
5224pub struct LayerNorm<T> {
5225 pub epsilon: f32,
5227 pub weight: Option<Parameter<T>>,
5229 pub bias: Option<Parameter<T>>,
5231}
5232
5233impl<T: Tensor> LayerNorm<T> {
5234 pub fn unloaded(
5236 dimensions: i32,
5237 epsilon: f32,
5238 weight: Option<ParameterSpec>,
5239 bias: Option<ParameterSpec>,
5240 context: &T::Context,
5241 ) -> Result<Self, Error> {
5242 Ok(Self {
5243 epsilon,
5244 weight: weight
5245 .map(|weight| Parameter::unloaded(weight, &[dimensions], context))
5246 .transpose()?,
5247 bias: bias
5248 .map(|bias| Parameter::unloaded(bias, &[dimensions], context))
5249 .transpose()?,
5250 })
5251 }
5252
5253 pub fn forward(&self, input: &T, context: &T::Context) -> Result<T, Error> {
5255 T::layer_norm(
5256 input,
5257 self.weight.as_ref().map(Parameter::as_ref),
5258 self.bias.as_ref().map(Parameter::as_ref),
5259 self.epsilon,
5260 context,
5261 )
5262 }
5263}
5264
5265impl<T: 'static> Parameterized<T> for LayerNorm<T> {
5266 fn visit_parameters<'a, V>(&'a self, visitor: &mut V)
5267 where
5268 V: ParameterVisitor<'a, T>,
5269 {
5270 if let Some(weight) = &self.weight {
5271 weight.visit_parameters(visitor);
5272 }
5273 if let Some(bias) = &self.bias {
5274 bias.visit_parameters(visitor);
5275 }
5276 }
5277
5278 fn visit_parameters_mut<'a, V>(&'a mut self, visitor: &mut V)
5279 where
5280 V: ParameterVisitorMut<'a, T>,
5281 {
5282 if let Some(weight) = &mut self.weight {
5283 weight.visit_parameters_mut(visitor);
5284 }
5285 if let Some(bias) = &mut self.bias {
5286 bias.visit_parameters_mut(visitor);
5287 }
5288 }
5289
5290 fn set_trainable(&mut self, trainable: bool) {
5291 if let Some(weight) = &mut self.weight {
5292 weight.set_trainable(trainable);
5293 }
5294 if let Some(bias) = &mut self.bias {
5295 bias.set_trainable(trainable);
5296 }
5297 }
5298}
5299
5300#[cfg(test)]
5301mod parameter_topology_tests {
5302 use super::*;
5303
5304 #[derive(Parameterized)]
5305 #[parameterized(tensor = "i32")]
5306 struct DerivedModule {
5307 first: Parameter<i32>,
5308 second: Option<Parameter<i32>>,
5309 #[parameter(skip)]
5310 label: &'static str,
5311 }
5312
5313 #[derive(Parameterized)]
5314 #[parameterized(tensor = "i32")]
5315 enum DerivedChoice {
5316 Present(Parameter<i32>),
5317 Empty,
5318 }
5319
5320 fn parameter(id: &str, value: i32) -> Parameter<i32> {
5321 Parameter::new(ParameterSpec::trainable(id).unwrap(), value)
5322 }
5323
5324 #[test]
5325 fn derive_recurses_through_structs_options_and_enums() {
5326 let mut module = DerivedModule {
5327 first: parameter("first.weight", 1),
5328 second: Some(parameter("second.weight", 2)),
5329 label: "not a parameter",
5330 };
5331 assert_eq!(module.label, "not a parameter");
5332 let metadata = validate_parameter_topology::<i32, _>(&module).unwrap();
5333 assert_eq!(
5334 metadata
5335 .iter()
5336 .map(|entry| entry.id.as_str())
5337 .collect::<Vec<_>>(),
5338 ["first.weight", "second.weight"]
5339 );
5340
5341 module.set_trainable(false);
5342 assert!(validate_parameter_topology::<i32, _>(&module)
5343 .unwrap()
5344 .iter()
5345 .all(|entry| !entry.trainable));
5346
5347 let choice = DerivedChoice::Present(parameter("choice.weight", 3));
5348 assert_eq!(
5349 validate_parameter_topology::<i32, _>(&choice).unwrap()[0]
5350 .id
5351 .as_str(),
5352 "choice.weight"
5353 );
5354 assert!(validate_parameter_topology::<i32, _>(&DerivedChoice::Empty)
5355 .unwrap()
5356 .is_empty());
5357 }
5358
5359 #[test]
5360 fn validation_rejects_duplicates_and_invalid_aliases() {
5361 let duplicate = vec![parameter("same.weight", 1), parameter("same.weight", 2)];
5362 assert!(matches!(
5363 validate_parameter_topology::<i32, _>(&duplicate),
5364 Err(ParameterTopologyError::DuplicateId(id)) if id.as_str() == "same.weight"
5365 ));
5366
5367 let alias = Parameter::new(
5368 ParameterSpec {
5369 id: ParameterId::new("alias.weight").unwrap(),
5370 trainable: true,
5371 alias_of: Some(ParameterId::new("missing.weight").unwrap()),
5372 group: None,
5373 linear_companion: None,
5374 linear_companion_of: None,
5375 },
5376 1,
5377 );
5378 assert!(matches!(
5379 validate_parameter_topology::<i32, _>(&alias),
5380 Err(ParameterTopologyError::MissingAliasDestination { .. })
5381 ));
5382 }
5383}
5384
5385#[cfg(test)]
5386mod fused_projection_layout_tests {
5387 use super::*;
5388
5389 #[test]
5390 fn component_major_layout_is_checked_and_stable() {
5391 let layout = FusedProjectionLayout::new([
5392 FusedProjectionSegment::new("query", 8).unwrap(),
5393 FusedProjectionSegment::new("key", 4).unwrap(),
5394 FusedProjectionSegment::new("value", 4).unwrap(),
5395 ])
5396 .unwrap();
5397 assert_eq!(layout.output_width(), 16);
5398 assert_eq!(
5399 layout
5400 .segments()
5401 .iter()
5402 .map(|segment| (segment.name(), segment.width()))
5403 .collect::<Vec<_>>(),
5404 [("query", 8), ("key", 4), ("value", 4)]
5405 );
5406 assert!(FusedProjectionLayout::new(Vec::new()).is_err());
5407 assert!(FusedProjectionLayout::new([
5408 FusedProjectionSegment::new("same", 1).unwrap(),
5409 FusedProjectionSegment::new("same", 1).unwrap(),
5410 ])
5411 .is_err());
5412 assert!(FusedProjectionSegment::new("", 1).is_err());
5413 assert!(FusedProjectionSegment::new("bad", 0).is_err());
5414 }
5415
5416 #[test]
5417 fn zero_sentinel_cannot_alias_an_embedding_row() {
5418 EmbeddingLookupPolicy::Strict.validate().unwrap();
5419 EmbeddingLookupPolicy::ZeroSentinel(-1).validate().unwrap();
5420 assert!(EmbeddingLookupPolicy::ZeroSentinel(0).validate().is_err());
5421 }
5422
5423 #[test]
5424 fn vocabulary_parallel_ownership_requires_exact_global_rows() {
5425 let range = VocabularyParallelRange {
5426 global_vocabulary: 5,
5427 local: 0..3,
5428 };
5429 range.validate_global_rows(5).unwrap();
5430 assert!(range.validate_global_rows(4).is_err());
5431 assert!(range.validate_global_rows(-1).is_err());
5432 }
5433}
5434
5435#[derive(Debug, Clone, Copy)]
5437pub struct Rope {
5438 dimensions: i32,
5439 traditional: bool,
5440 base: f32,
5441 scale: f32,
5442}
5443
5444impl Rope {
5445 pub const fn new(dimensions: i32, traditional: bool, base: f32, scale: f32) -> Self {
5447 Self {
5448 dimensions,
5449 traditional,
5450 base,
5451 scale,
5452 }
5453 }
5454
5455 pub fn forward<T: Tensor>(
5457 &self,
5458 input: &T,
5459 offset: i32,
5460 context: &T::Context,
5461 ) -> Result<T, Error> {
5462 T::rope(
5463 input,
5464 self.dimensions,
5465 self.traditional,
5466 self.base,
5467 self.scale,
5468 offset,
5469 context,
5470 )
5471 }
5472}