hermes_simd_core/cow/math.rs
1//! Mathematical extensions for [`SimdCow`]: norm, normalize, scalar-broadcast ops.
2//!
3//! # Zero-Cost Contract
4//!
5//! All methods monomorphize per `(T, Arch, Align)`. The `Arch` and `Align` ZST markers
6//! are erased at codegen. Scalar-broadcast ops (`add_scalar_cow`, `mul_scalar_cow`, etc.)
7//! allocate exactly one `AlignedVec` output; no intermediate buffer is allocated.
8//!
9//! # Norm and Normalize
10//!
11//! - `norm_sq` — squared Euclidean norm: `∑ self[i]²`. Delegates to `zip_reduce(Dot)`.
12//! - `norm` — Euclidean norm: `sqrt(norm_sq)`. Uses `T::sqrt_scalar` from `FloatElement`.
13//! - `normalize` — returns a unit-length owned `SimdCow<'static, T, Arch, Align>`.
14//! Empty or zero-norm vectors return `zeros(self.len())` rather than NaN / division by zero.
15//!
16//! # Safety
17//!
18//! Two obligations recur here. Kernel calls are `#[target_feature]`-gated, and
19//! that precondition holds by construction: a `SimdCow` exists only for an
20//! architecture the host can execute, since its borrowed form comes from
21//! [`SimdView::new`](crate::view::SimdView::new) and its owned constructors
22//! assert the same condition. The second is local — these routines build their
23//! output buffer with `with_capacity` and write it through a raw pointer,
24//! raising the length only once every element is initialized. That avoids both
25//! a zero-fill of a buffer about to be overwritten and any `&mut [T]` spanning
26//! uninitialized elements, so each such site carries a `SAFETY` comment showing
27//! the write coverage. `gather` and `prefix_scan` reserve capacity and fill it
28//! through the view's `*_into_uninit` methods over
29//! [`AlignedVec::spare_capacity_mut`](crate::vec::AlignedVec::spare_capacity_mut),
30//! then raise the length once those report success, so those paths never zero
31//! the buffer either.
32
33use super::SimdCow;
34use crate::align::Alignment;
35use crate::arch::SimdArch;
36use crate::kernel::SimdKernel;
37use crate::ops::{Dot, Sub};
38use crate::scalar::{FloatElement, Scalar};
39use crate::vec::AlignedVec;
40use crate::view::SimdError;
41
42extern crate alloc;
43
44// ---------------------------------------------------------------------------
45// Scalar-broadcast arithmetic
46// ---------------------------------------------------------------------------
47
48impl<'a, T: 'a, Arch, Align> SimdCow<'a, T, Arch, Align>
49where
50 T: Scalar,
51 Arch: SimdArch + SimdKernel<T>,
52 Align: Alignment,
53{
54 /// Add scalar `rhs` to every element: `out[i] = self[i] + rhs`.
55 ///
56 /// One allocation. No second `SimdCow` allocation.
57 #[inline]
58 pub fn add_scalar_cow(&self, rhs: T) -> SimdCow<'static, T, Arch, Align> {
59 broadcast_op::<T, Arch, Align>(
60 self,
61 rhs,
62 |a, b| a + b,
63 |va, vsplat| unsafe { Arch::add(va, vsplat) },
64 )
65 }
66
67 /// Subtract scalar `rhs` from every element: `out[i] = self[i] - rhs`.
68 ///
69 /// One allocation.
70 #[inline]
71 pub fn sub_scalar_cow(&self, rhs: T) -> SimdCow<'static, T, Arch, Align> {
72 broadcast_op::<T, Arch, Align>(
73 self,
74 rhs,
75 |a, b| a - b,
76 |va, vsplat| unsafe { Arch::sub(va, vsplat) },
77 )
78 }
79
80 /// Multiply every element by scalar `rhs`: `out[i] = self[i] * rhs`.
81 ///
82 /// One allocation. For in-place scaling use [`SimdCow::scale_in_place`].
83 #[inline]
84 pub fn mul_scalar_cow(&self, rhs: T) -> SimdCow<'static, T, Arch, Align> {
85 broadcast_op::<T, Arch, Align>(
86 self,
87 rhs,
88 |a, b| a * b,
89 |va, vsplat| unsafe { Arch::mul(va, vsplat) },
90 )
91 }
92
93 /// Elementwise division by scalar `rhs`: `out[i] = self[i] / rhs`.
94 ///
95 /// One allocation.
96 #[inline]
97 pub fn div_scalar_cow(&self, rhs: T) -> SimdCow<'static, T, Arch, Align> {
98 broadcast_op::<T, Arch, Align>(
99 self,
100 rhs,
101 |a, b| a / b,
102 |va, vsplat| unsafe { Arch::div(va, vsplat) },
103 )
104 }
105
106 /// Elementwise division: `out[i] = self[i] / other[i]`.
107 ///
108 /// One allocation.
109 ///
110 /// # Errors
111 /// Returns `SimdError::LengthMismatch` if lengths differ.
112 #[inline]
113 pub fn div_cow(
114 &self,
115 other: &SimdCow<'_, T, Arch, Align>,
116 ) -> Result<SimdCow<'static, T, Arch, Align>, SimdError> {
117 self.zip_cow(other, crate::ops::Div)
118 }
119
120 /// Elementwise subtraction returning owned `SimdCow`, non-method form for symmetry.
121 ///
122 /// Equivalent to `self.sub_cow(other)` — delegates to `zip_cow(Sub)`.
123 ///
124 /// # Errors
125 /// Returns `SimdError::LengthMismatch` if lengths differ.
126 #[inline]
127 pub fn sub_cow_op(
128 &self,
129 other: &SimdCow<'_, T, Arch, Align>,
130 ) -> Result<SimdCow<'static, T, Arch, Align>, SimdError> {
131 self.zip_cow(other, Sub)
132 }
133}
134
135// ---------------------------------------------------------------------------
136// Norm and normalize (float-element only)
137// ---------------------------------------------------------------------------
138
139impl<'a, T: 'a, Arch, Align> SimdCow<'a, T, Arch, Align>
140where
141 T: Scalar + FloatElement,
142 Arch: SimdArch + SimdKernel<T>,
143 Align: Alignment,
144{
145 /// Squared Euclidean norm: `∑ self[i]²`.
146 ///
147 /// Zero-copy: delegates to `zip_reduce(Dot)` which is a single SIMD pass.
148 #[inline]
149 pub fn norm_sq(&self) -> T {
150 let v = self.view();
151 v.zip_reduce(&v, Dot).unwrap_or(T::ZERO)
152 }
153
154 /// Euclidean norm: `√(∑ self[i]²)`.
155 ///
156 /// Delegates to `norm_sq` then `T::sqrt_scalar`.
157 #[inline]
158 pub fn norm(&self) -> T {
159 self.norm_sq().sqrt()
160 }
161
162 /// Returns a unit-length copy: `self / ‖self‖`.
163 ///
164 /// - Empty vector → empty `SimdCow<'static, T, Arch, Align>`.
165 /// - Zero-norm vector → `zeros(self.len())` (safe, no NaN).
166 /// - Otherwise → `self * (1 / ‖self‖)`.
167 ///
168 /// One allocation for the output `AlignedVec`.
169 #[inline]
170 pub fn normalize(&self) -> SimdCow<'static, T, Arch, Align> {
171 let n = self.norm();
172 if n == T::ZERO {
173 return SimdCow::zeros(self.len());
174 }
175 // Compute reciprocal once, multiply — avoids a per-element division.
176 let inv = T::ONE / n;
177 self.mul_scalar_cow(inv)
178 }
179
180 /// Scalar histogram over this cow's values.
181 ///
182 /// Partitions `[lo, hi)` into `n_bins` equal-width bins and counts how many
183 /// elements fall in each bin. Elements outside `[lo, hi)` are ignored.
184 ///
185 /// Returns a `Vec<usize>` of length `n_bins`.
186 ///
187 /// # Panics
188 /// Panics if `n_bins == 0` or `lo >= hi`.
189 #[inline]
190 pub fn histogram_cow(&self, n_bins: usize, lo: T, hi: T) -> alloc::vec::Vec<usize>
191 where
192 T: PartialOrd,
193 {
194 assert!(n_bins > 0, "n_bins must be > 0");
195 assert!(lo < hi, "lo must be < hi");
196
197 // Bin indices are computed in f64: it is a strict superset of every
198 // supported lane precision (f16/bf16/f32/f64), so the index — an
199 // integer output, never narrowed back to `T` — is exact for all `T`.
200 let lo_w = lo.to_f64();
201 let bin_width = (hi.to_f64() - lo_w) / n_bins as f64;
202 let mut counts = alloc::vec![0usize; n_bins];
203
204 for &x in self.as_ref().iter() {
205 if x < lo || x >= hi {
206 continue;
207 }
208 let bin = (((x.to_f64() - lo_w) / bin_width) as usize).min(n_bins - 1);
209 counts[bin] += 1;
210 }
211 counts
212 }
213}
214
215// ---------------------------------------------------------------------------
216// Private helpers
217// ---------------------------------------------------------------------------
218
219/// SIMD broadcast-apply: `out[i] = scalar_op(data[i], rhs)`.
220///
221/// Uses `Arch::splat(rhs)` once, then loops over SIMD vectors with `vector_op`,
222/// followed by a scalar tail loop with `scalar_op`. One allocation.
223#[inline(always)]
224fn broadcast_op<T, Arch, Align>(
225 cow: &SimdCow<'_, T, Arch, Align>,
226 rhs: T,
227 scalar_op: impl Fn(T, T) -> T + Copy,
228 vector_op: impl Fn(Arch::Vector, Arch::Vector) -> Arch::Vector + Copy,
229) -> SimdCow<'static, T, Arch, Align>
230where
231 T: Scalar,
232 Arch: SimdArch + SimdKernel<T>,
233 Align: Alignment,
234{
235 let data = cow.as_ref();
236 let len = data.len();
237 let mut out: AlignedVec<T, Align> = AlignedVec::with_capacity(len);
238
239 let lane_count = Arch::LANE_COUNT;
240 let simd_len = (len / lane_count) * lane_count;
241 let ptr_in = data.as_ptr();
242 let ptr_out = out.as_mut_ptr();
243
244 // SAFETY: `with_capacity(len)` reserved `len` elements and `ptr_in` covers
245 // the same `len` elements, so every access below stays inside its
246 // allocation. The length is raised only after the vector and scalar loops
247 // have together written every element, so no reference spans uninitialized
248 // memory.
249 unsafe {
250 let vsplat = Arch::splat(rhs);
251 let load = |p: *const T| -> Arch::Vector {
252 if crate::align::is_aligned_for_arch::<Arch, Align>() {
253 Arch::load_aligned(p)
254 } else {
255 Arch::load_unaligned(p)
256 }
257 };
258 let store = |p: *mut T, v: Arch::Vector| {
259 if crate::align::is_aligned_for_arch::<Arch, Align>() {
260 Arch::store_aligned(p, v);
261 } else {
262 Arch::store_unaligned(p, v);
263 }
264 };
265 let mut i = 0usize;
266 while i < simd_len {
267 let va = load(ptr_in.add(i));
268 let vr = vector_op(va, vsplat);
269 store(ptr_out.add(i), vr);
270 i += lane_count;
271 }
272 for i in simd_len..len {
273 core::ptr::write(ptr_out.add(i), scalar_op(*ptr_in.add(i), rhs));
274 }
275 out.set_len(len);
276 }
277
278 SimdCow::Owned(out)
279}