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