casadi_blazing_common.hpp
1 //
2 // MIT No Attribution
3 //
4 // Copyright (C) 2010-2023 Joel Andersson, Joris Gillis, Moritz Diehl, KU Leuven.
5 //
6 // Permission is hereby granted, free of charge, to any person obtaining a copy of this
7 // software and associated documentation files (the "Software"), to deal in the Software
8 // without restriction, including without limitation the rights to use, copy, modify,
9 // merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
10 // permit persons to whom the Software is furnished to do so.
11 //
12 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13 // INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
14 // PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
15 // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
16 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
17 // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18 //
19 
20 // C-REPLACE "casadi_blazing_low<T1>" "casadi_blazing_low"
21 // C-REPLACE "casadi_blazing_boor_der<T1>" "casadi_blazing_boor_der"
22 // C-REPLACE "casadi_blazing_shift_left<T1>" "casadi_blazing_shift_left"
23 // C-REPLACE "casadi_blazing_knot_scale<T1>" "casadi_blazing_knot_scale"
24 // C-REPLACE "casadi_blazing_boor_init<T1>" "casadi_blazing_boor_init"
25 // C-REPLACE "casadi_blazing_dbasis<T1>" "casadi_blazing_dbasis"
26 // C-REPLACE "casadi_blazing_d2basis<T1>" "casadi_blazing_d2basis"
27 // C-REPLACE "casadi_blazing_hsum<T1>" "casadi_blazing_hsum"
28 // C-REPLACE "casadi_blazing_tensor_ttv2<T1>" "casadi_blazing_tensor_ttv2"
29 // C-REPLACE "casadi_blazing_tensor_ttv3<T1>" "casadi_blazing_tensor_ttv3"
30 // C-REPLACE "casadi_blazing_tensor_ttv4<T1>" "casadi_blazing_tensor_ttv4"
31 // C-REPLACE "casadi_blazing_tensor_ttv5<T1>" "casadi_blazing_tensor_ttv5"
32 // C-REPLACE "static_cast<T1>" "(double) "
33 
34 // ===== Knot-span lookup =====
35 
36 // SYMBOL "blazing_low"
37 // Forked from casadi_low for the blazing kernel. Same semantics
38 // (returns the largest i in [0, ng-2] such that grid[i] <= x < grid[i+1],
39 // with clamping at both ends), but three faster code paths:
40 // lookup_mode = 0 (linear): AVX2-vectorised scan, 4 grid points per
41 // SIMD iteration; ~4x fewer branches than scalar.
42 // lookup_mode = 1 (exact): if grid_inv is non-null, treats grid_inv as a
43 // 2-element {intercept, slope} pair and computes
44 // i = floor(x*slope + intercept) with a single
45 // FMA --- no FP divide, no extra subtract.
46 // Falls back to the original divide when null.
47 // lookup_mode = 2 (binary): branchless bisection via cmov (data-dependent
48 // updates only, no mispredictable branches inside
49 // the loop).
50 template<typename T1>
51 casadi_int casadi_blazing_low(T1 x, const T1* grid, casadi_int ng,
52  casadi_int lookup_mode, const T1* grid_inv) {
53  switch (lookup_mode) {
54  case 1:
55  {
56  // 'exact' --- uniform-grid direct lookup
57  casadi_int ret;
58  if (grid_inv) {
59  // Pre-baked at codegen time:
60  // slope = (ng-1) / (grid[ng-1] - grid[0])
61  // intercept = -grid[0] * slope
62  // so x*slope + intercept lands directly in the lookup index.
63  // One FMA, one truncate.
64  T1 intercept = grid_inv[0];
65  T1 slope = grid_inv[1];
66  ret = (casadi_int) (x * slope + intercept);
67  } else {
68  // Fallback: original formula with the FP divide.
69  T1 g0 = grid[0];
70  T1 dg = grid[ng-1] - g0;
71  ret = (casadi_int) ((x - g0) * (ng-1) / dg);
72  }
73  if (ret < 0) ret = 0;
74  if (ret > ng-2) ret = ng-2;
75  return ret;
76  }
77  case 2:
78  {
79  // 'binary' --- Skarupke-style branchless lower_bound
80  // (https://mhdm.dev/posts/sb_lower_bound/), then converted to
81  // casadi_low semantics (largest i with grid[i] <= x, clamped to
82  // [0, ng-2]).
83  //
84  // The `if (cmp) lo += len + 1` form lets the compiler fuse the
85  // compare-cmovae-add into a tight chain; measurably faster than
86  // the `lo = cmp ? probe : lo; len -= half;` style. Benchmarked
87  // at n=100 against std::lower_bound, Skarupke, sb, sbm, bb, sbp;
88  // Skarupke is the consistent winner across n=100..10000 in both
89  // fixed-x and random-x regimes.
90  casadi_int lo = 0;
91  casadi_int len = ng;
92  while (len > 0) {
93  len /= 2;
94  if (grid[lo + len] < x) lo += len + 1;
95  }
96  // Convert lower_bound -> "largest i s.t. grid[i] <= x".
97  if (lo > 0) lo--;
98  if (lo > ng - 2) lo = ng - 2;
99  return lo;
100  }
101  default:
102  {
103  // 'linear' --- AVX2-vectorised forward scan.
104  // Compare 4 grid points to x per iteration; tzcnt picks the first
105  // lane with x < grid[i+1]. Falls back to scalar for the tail.
106  casadi_int n = ng - 2;
107  if (n <= 0) return 0;
108  simde__m256d xv = simde_mm256_set1_pd(static_cast<T1>(x));
109  casadi_int i = 0;
110  for (; i + 4 <= n; i += 4) {
111  simde__m256d gv = simde_mm256_loadu_pd((const T1*)(grid + i + 1));
112  simde__m256d cmp = simde_mm256_cmp_pd(xv, gv, SIMDE_CMP_LT_OQ);
113  int m = simde_mm256_movemask_pd(cmp);
114  // m is a 4-bit movemask in [1, 15]; find the lowest set lane.
115  if (m) return i + ((m & 1) ? 0 : (m & 2) ? 1 : (m & 4) ? 2 : 3);
116  }
117  for (; i < n; ++i) {
118  if (x < grid[i+1]) return i;
119  }
120  return n;
121  }
122  }
123 }
124 
125 // ===== De Boor evaluation =====
126 
127 // SYMBOL "blazing_printvec"
128 template<typename T1>
129 void casadi_blazing_printvec(const simde__m256d* e) {
130  double elements[4];
131  simde_mm256_storeu_pd(elements, *e);
132  printf("mm256d: <%.4f %.4f %.4f %.4f>\n", elements[0], elements[1], elements[2], elements[3]);
133 }
134 
135 // SYMBOL "blazing_de_boor"
136 template<typename T1>
137 void casadi_blazing_de_boor(T1 x, const T1* knots, const T1* inv1, const T1* inv2, const T1* inv3, simde__m256d* boor_d0, simde__m256d* boor_d1, simde__m256d* boor_d2, const simde__m256d* boor_d3) { // NOLINT(whitespace/line_length)
138  simde__m256d x_ = simde_mm256_set1_pd(x);
139  simde__m256d zero = simde_mm256_set1_pd(0.0);
140  simde__m256d mask_end = simde_mm256_set_pd(0.0, -1.0, -1.0, -1.0);
141  simde__m256d r;
142 
143  // shift one up
144  simde__m256d boor_d3i_1 = simde_mm256_permute4x64_pd(*boor_d3, SIMDE_MM_SHUFFLE(3, 3, 2, 1));
145  boor_d3i_1 = simde_mm256_blendv_pd(zero, boor_d3i_1, mask_end);
146 
147  simde__m256d knotsi = simde_mm256_loadu_pd(knots);
148  simde__m256d knotsi_2 = simde_mm256_loadu_pd(knots+2);
149  simde__m256d knotsi_3 = simde_mm256_loadu_pd(knots+3);
150  simde__m256d knotsi_4 = simde_mm256_loadu_pd(knots+4);
151 
152  if (inv1) {
153  // ---- Reciprocal path: multiply by precomputed 1/span ----
154  // d3 -> d2 (span-1)
155  r = simde_mm256_mul_pd(simde_mm256_sub_pd(x_, knotsi), simde_mm256_loadu_pd(inv1));
156  *boor_d2 = simde_mm256_mul_pd(r, *boor_d3);
157  r = simde_mm256_mul_pd(simde_mm256_sub_pd(knotsi_2, x_), simde_mm256_loadu_pd(inv1 + 1));
158  *boor_d2 = simde_mm256_fmadd_pd(r, boor_d3i_1, *boor_d2);
159 
160  // shift d2
161  simde__m256d boor_d2i_1 = simde_mm256_permute4x64_pd(*boor_d2, SIMDE_MM_SHUFFLE(3, 3, 2, 1));
162  boor_d2i_1 = simde_mm256_blendv_pd(zero, boor_d2i_1, mask_end);
163 
164  // d2 -> d1 (span-2)
165  r = simde_mm256_mul_pd(simde_mm256_sub_pd(x_, knotsi), simde_mm256_loadu_pd(inv2));
166  *boor_d1 = simde_mm256_mul_pd(r, *boor_d2);
167  r = simde_mm256_mul_pd(simde_mm256_sub_pd(knotsi_3, x_), simde_mm256_loadu_pd(inv2 + 1));
168  *boor_d1 = simde_mm256_fmadd_pd(r, boor_d2i_1, *boor_d1);
169 
170  // shift d1
171  simde__m256d boor_d1i_1 = simde_mm256_permute4x64_pd(*boor_d1, SIMDE_MM_SHUFFLE(3, 3, 2, 1));
172  boor_d1i_1 = simde_mm256_blendv_pd(zero, boor_d1i_1, mask_end);
173 
174  // d1 -> d0 (span-3)
175  r = simde_mm256_mul_pd(simde_mm256_sub_pd(x_, knotsi), simde_mm256_loadu_pd(inv3));
176  *boor_d0 = simde_mm256_mul_pd(r, *boor_d1);
177  r = simde_mm256_mul_pd(simde_mm256_sub_pd(knotsi_4, x_), simde_mm256_loadu_pd(inv3 + 1));
178  *boor_d0 = simde_mm256_fmadd_pd(r, boor_d1i_1, *boor_d0);
179  } else {
180  // ---- Original division path ----
181  simde__m256d knotsi_1 = simde_mm256_loadu_pd(knots+1);
182  simde__m256d bottom, bottom_mask;
183 
184  bottom = simde_mm256_sub_pd(knotsi_1, knotsi);
185  bottom_mask = simde_mm256_cmp_pd(bottom, zero, SIMDE_CMP_EQ_OQ);
186  r = simde_mm256_div_pd(simde_mm256_sub_pd(x_, knotsi), bottom);
187  r = simde_mm256_blendv_pd(r, zero, bottom_mask);
188  *boor_d2 = simde_mm256_mul_pd(r, *boor_d3);
189  *boor_d2 = simde_mm256_blendv_pd(*boor_d2, zero, bottom_mask);
190 
191  bottom = simde_mm256_sub_pd(knotsi_2, knotsi_1);
192  bottom_mask = simde_mm256_cmp_pd(bottom, zero, SIMDE_CMP_EQ_OQ);
193  r = simde_mm256_div_pd(simde_mm256_sub_pd(knotsi_2, x_), bottom);
194  r = simde_mm256_blendv_pd(r, zero, bottom_mask);
195  *boor_d2 = simde_mm256_fmadd_pd(r, boor_d3i_1, *boor_d2);
196 
197  simde__m256d boor_d2i_1 = simde_mm256_permute4x64_pd(*boor_d2, SIMDE_MM_SHUFFLE(3, 3, 2, 1));
198  boor_d2i_1 = simde_mm256_blendv_pd(zero, boor_d2i_1, mask_end);
199 
200  bottom = simde_mm256_sub_pd(knotsi_2, knotsi);
201  bottom_mask = simde_mm256_cmp_pd(bottom, zero, SIMDE_CMP_EQ_OQ);
202  r = simde_mm256_div_pd(simde_mm256_sub_pd(x_, knotsi), bottom);
203  r = simde_mm256_blendv_pd(r, zero, bottom_mask);
204  *boor_d1 = simde_mm256_mul_pd(r, *boor_d2);
205  *boor_d1 = simde_mm256_blendv_pd(*boor_d1, zero, bottom_mask);
206 
207  bottom = simde_mm256_sub_pd(knotsi_3, knotsi_1);
208  bottom_mask = simde_mm256_cmp_pd(bottom, zero, SIMDE_CMP_EQ_OQ);
209  r = simde_mm256_div_pd(simde_mm256_sub_pd(knotsi_3, x_), bottom);
210  r = simde_mm256_blendv_pd(r, zero, bottom_mask);
211  *boor_d1 = simde_mm256_fmadd_pd(r, boor_d2i_1, *boor_d1);
212 
213  simde__m256d boor_d1i_1 = simde_mm256_permute4x64_pd(*boor_d1, SIMDE_MM_SHUFFLE(3, 3, 2, 1));
214  boor_d1i_1 = simde_mm256_blendv_pd(zero, boor_d1i_1, mask_end);
215 
216  bottom = simde_mm256_sub_pd(knotsi_3, knotsi);
217  bottom_mask = simde_mm256_cmp_pd(bottom, zero, SIMDE_CMP_EQ_OQ);
218  r = simde_mm256_div_pd(simde_mm256_sub_pd(x_, knotsi), bottom);
219  r = simde_mm256_blendv_pd(r, zero, bottom_mask);
220  *boor_d0 = simde_mm256_mul_pd(r, *boor_d1);
221  *boor_d0 = simde_mm256_blendv_pd(*boor_d0, zero, bottom_mask);
222 
223  bottom = simde_mm256_sub_pd(knotsi_4, knotsi_1);
224  bottom_mask = simde_mm256_cmp_pd(bottom, zero, SIMDE_CMP_EQ_OQ);
225  r = simde_mm256_div_pd(simde_mm256_sub_pd(knotsi_4, x_), bottom);
226  r = simde_mm256_blendv_pd(r, zero, bottom_mask);
227  *boor_d0 = simde_mm256_fmadd_pd(r, boor_d1i_1, *boor_d0);
228  }
229 }
230 
231 // ===== Basis derivative helpers =====
232 
233 // SYMBOL "blazing_boor_der"
234 // Summation by parts: convert derivative from coefficient-space to basis-space.
235 //
236 // Given boor = [b0, b1, b2, b3] and scale = [s0, s1, s2, s3]:
237 // sb[i] = b[i]*s[i]
238 // result[i] = sb[i-1] - sb[i] (with sb[-1]=0)
239 // i.e. [-s0*b0, s0*b0-s1*b1, s1*b1-s2*b2, s2*b2-s3*b3]
240 //
241 // To compute the derivative of f = sum_i c[i]*N_{i,d}(x),
242 // use boor = shift_left(d_{d-1} basis) and appropriate knot scales.
243 //
244 // For cubic Jacobian: boor_J = blazing_boor_der(shift_left(d1), s1)
245 // where shift_left([0,N0,N1,N2]) = [N0,N1,N2,0]
246 // and s1[j] = 3/(t[j+start+4]-t[j+start+1])
247 //
248 // For cubic Hessian: inner = blazing_boor_der(shift_left(d2), s2)
249 // boor_H = blazing_boor_der(shift_left(inner), s1)
250 // where s2[j] = 2/(t[j+start+3]-t[j+start+1])
251 template<typename T1>
252 void casadi_blazing_boor_der(simde__m256d* out, const simde__m256d* boor,
253  const simde__m256d* scale) {
254  simde__m256d sb = simde_mm256_mul_pd(*boor, *scale);
255  simde__m256d shifted = simde_mm256_permute4x64_pd(sb, SIMDE_MM_SHUFFLE(2, 1, 0, 0));
256  shifted = simde_mm256_blend_pd(simde_mm256_setzero_pd(), shifted, 0xE);
257  *out = simde_mm256_sub_pd(shifted, sb);
258 }
259 
260 // SYMBOL "blazing_shift_left"
261 // Shift AVX vector one position to the left, filling position 3 with zero.
262 // [a, b, c, d] -> [b, c, d, 0]
263 template<typename T1>
264 void casadi_blazing_shift_left(simde__m256d* out, const simde__m256d* v) {
265  simde__m256d shifted = simde_mm256_permute4x64_pd(*v, SIMDE_MM_SHUFFLE(3, 3, 2, 1));
266  *out = simde_mm256_blend_pd(shifted, simde_mm256_setzero_pd(), 0x8);
267 }
268 
269 // SYMBOL "blazing_knot_scale"
270 // Compute knot scale with safe division: degree/(t_hi - t_lo), returning 0 where span is 0.
271 // This avoids NaN from 0/0 at knot boundaries where both the basis function and
272 // the knot span are zero.
273 template<typename T1>
274 void casadi_blazing_knot_scale(simde__m256d* out, const simde__m256d* degree,
275  const simde__m256d* t_hi, const simde__m256d* t_lo) {
276  simde__m256d zero = simde_mm256_setzero_pd();
277  simde__m256d denom = simde_mm256_sub_pd(*t_hi, *t_lo);
278  simde__m256d denom_mask = simde_mm256_cmp_pd(denom, zero, SIMDE_CMP_EQ_OQ);
279  simde__m256d scale = simde_mm256_div_pd(*degree, denom);
280  *out = simde_mm256_blendv_pd(scale, zero, denom_mask);
281 }
282 
283 // SYMBOL "blazing_boor_init"
284 // Per-dimension de Boor setup: knot lookup, boundary-case init, and de Boor evaluation.
285 // Returns the start index and fills d0, d1, d2 with basis function intermediates.
286 // dim_cache points at THIS dimension's slice of the global cache (or NULL).
287 // The caller is responsible for advancing the pointer between dims.
288 //
289 // Per-dimension cache layout (always emitted unconditionally when the cache
290 // is present, even when lookup_mode != 1):
291 // [intercept, slope, inv1[n_k], inv2[n_k], inv3[n_k]]
292 // where slope = (ng-1) / (grid[ng-1] - grid[0])
293 // intercept = -grid[0] * slope
294 // and ng = n_k - 2*degree. The first two entries feed 'exact' lookup as a
295 // single FMA; the inv1/inv2/inv3 spans feed the de Boor recurrence.
296 //
297 // If inv2_out/inv3_out are non-NULL, they receive pre-positioned derivative
298 // base pointers that can be passed directly to dbasis/d2basis.
299 template<typename T1>
300 casadi_int casadi_blazing_boor_init(
301  T1 x, const T1* all_knots, const T1* dim_cache,
302  casadi_int knot_offset, casadi_int knot_offset_next,
303  casadi_int lookup_mode,
304  simde__m256d* d0, simde__m256d* d1, simde__m256d* d2,
305  const T1** inv2_out, const T1** inv3_out) {
306  casadi_int degree = 3;
307  const T1* knots = all_knots + knot_offset;
308  casadi_int n_knots = knot_offset_next - knot_offset;
309  casadi_int n_b = n_knots - degree - 1;
310  // First two entries of dim_cache are {intercept, slope} for 'exact' lookup.
311  const T1* grid_inv = dim_cache;
312  casadi_int L = casadi_blazing_low<T1>(x, knots + degree,
313  n_knots - 2*degree,
314  lookup_mode, grid_inv);
315  casadi_int start = L;
316  if (start > n_b - degree - 1) start = n_b - degree - 1;
317 
318  simde__m256d d3 = simde_mm256_setzero_pd();
319  if (x >= knots[0] && x <= knots[n_knots-1]) {
320  if (x == knots[1]) {
321  d3 = simde_mm256_set1_pd(1.0);
322  } else if (x == knots[n_knots-1]) {
323  d3 = simde_mm256_set_pd(1.0, 0.0, 0.0, 0.0);
324  } else if (knots[L+degree] == x) {
325  d3 = simde_mm256_set_pd(0.0, 1.0, 0.0, 0.0);
326  } else {
327  d3 = simde_mm256_set_pd(1.0, 0.0, 0.0, 0.0);
328  }
329  }
330  const T1 *inv1 = 0, *inv2 = 0, *inv3 = 0;
331  if (dim_cache) {
332  // Skip the {intercept, slope} prefix to reach the inv1/inv2/inv3 spans.
333  const T1* inv_base = dim_cache + 2;
334  inv1 = inv_base + start;
335  inv2 = inv_base + n_knots + start;
336  inv3 = inv_base + 2*n_knots + start;
337  if (inv2_out) *inv2_out = inv_base + n_knots + start + 1;
338  if (inv3_out) *inv3_out = inv_base + 2*n_knots + start + 1;
339  } else {
340  if (inv2_out) *inv2_out = 0;
341  if (inv3_out) *inv3_out = 0;
342  }
343  casadi_blazing_de_boor(x, knots + start, inv1, inv2, inv3, d0, d1, d2, &d3);
344  return start;
345 }
346 
347 // SYMBOL "blazing_dbasis"
348 // Compute 1st-derivative basis functions for one dimension.
349 // t points to knots at starts[i] for this dimension.
350 // inv3 can be 0 (NULL) to use the division path; otherwise 1/(t[k+3]-t[k]).
351 template<typename T1>
352 void casadi_blazing_dbasis(simde__m256d* out, const simde__m256d* boor_d1,
353  const T1* t, const T1* inv3) {
354  simde__m256d three = simde_mm256_set1_pd(3.0);
355  simde__m256d s1, t_hi, t_lo, shifted;
356  if (inv3) {
357  s1 = simde_mm256_mul_pd(three, simde_mm256_loadu_pd(inv3));
358  } else {
359  t_hi = simde_mm256_loadu_pd(t + 4);
360  t_lo = simde_mm256_loadu_pd(t + 1);
361  casadi_blazing_knot_scale<T1>(&s1, &three, &t_hi, &t_lo);
362  }
363  casadi_blazing_shift_left<T1>(&shifted, boor_d1);
364  casadi_blazing_boor_der<T1>(out, &shifted, &s1);
365 }
366 
367 // SYMBOL "blazing_d2basis"
368 // Compute 2nd-derivative basis functions for one dimension.
369 // t points to knots at starts[i] for this dimension.
370 // inv2, inv3 can be 0 (NULL) to use the division path;
371 // otherwise 1/(t[k+2]-t[k]) and 1/(t[k+3]-t[k]).
372 template<typename T1>
373 void casadi_blazing_d2basis(simde__m256d* out, const simde__m256d* boor_d2,
374  const T1* t, const T1* inv2, const T1* inv3) {
375  simde__m256d three = simde_mm256_set1_pd(3.0);
376  simde__m256d two = simde_mm256_set1_pd(2.0);
377  simde__m256d s1, s2, t_hi, t_lo, shifted, inner;
378  if (inv3) {
379  s1 = simde_mm256_mul_pd(three, simde_mm256_loadu_pd(inv3));
380  s2 = simde_mm256_mul_pd(two, simde_mm256_loadu_pd(inv2));
381  } else {
382  t_lo = simde_mm256_loadu_pd(t + 1);
383  t_hi = simde_mm256_loadu_pd(t + 4);
384  casadi_blazing_knot_scale<T1>(&s1, &three, &t_hi, &t_lo);
385  t_hi = simde_mm256_loadu_pd(t + 3);
386  casadi_blazing_knot_scale<T1>(&s2, &two, &t_hi, &t_lo);
387  }
388  casadi_blazing_shift_left<T1>(&shifted, boor_d2);
389  casadi_blazing_boor_der<T1>(&inner, &shifted, &s2);
390  casadi_blazing_shift_left<T1>(&shifted, &inner);
391  casadi_blazing_boor_der<T1>(out, &shifted, &s1);
392 }
393 
394 // ===== Tensor-times-vector contractions =====
395 
396 // AVX2 horizontal sum: reduce 4-wide __m256d to scalar double
397 // SYMBOL "blazing_hsum"
398 template<typename T1>
399 T1 casadi_blazing_hsum(const simde__m256d* r) {
400  simde__m128d r0 = simde_mm256_castpd256_pd128(*r);
401  simde__m128d r1 = simde_mm256_extractf128_pd(*r, 1);
402  r0 = simde_mm_add_pd(r0, r1);
403  return simde_mm_cvtsd_f64(simde_mm_add_sd(r0, simde_mm_unpackhi_pd(r0, r0)));
404 }
405 
406 // AVX2 tensor-times-vector for 2D cubic B-splines (degree 3, m=1).
407 //
408 // result = sum_{i,j} a[i] * b[j] * C[j][i]
409 //
410 // where a[i] are in AVX lanes, b[j] are in AVX lanes (broadcast internally),
411 // and C[4] holds coefficient vectors (4 doubles along dim 0 each).
412 //
413 // SYMBOL "blazing_tensor_ttv2"
414 template<typename T1>
415 T1 casadi_blazing_tensor_ttv2(const simde__m256d C[4],
416  const simde__m256d* a, const simde__m256d* b) {
417  simde__m256d r;
418  // Broadcast dim 1 weights and form outer product with dim 0
419  simde__m256d ab0 = simde_mm256_mul_pd(*a,
420  simde_mm256_permute4x64_pd(*b, SIMDE_MM_SHUFFLE(0, 0, 0, 0)));
421  simde__m256d ab1 = simde_mm256_mul_pd(*a,
422  simde_mm256_permute4x64_pd(*b, SIMDE_MM_SHUFFLE(1, 1, 1, 1)));
423  simde__m256d ab2 = simde_mm256_mul_pd(*a,
424  simde_mm256_permute4x64_pd(*b, SIMDE_MM_SHUFFLE(2, 2, 2, 2)));
425  simde__m256d ab3 = simde_mm256_mul_pd(*a,
426  simde_mm256_permute4x64_pd(*b, SIMDE_MM_SHUFFLE(3, 3, 3, 3)));
427  // Contract dim 1: r = sum_j ab[j] * C[j]
428  r = simde_mm256_mul_pd(ab0, C[0]);
429  r = simde_mm256_fmadd_pd(ab1, C[1], r);
430  r = simde_mm256_fmadd_pd(ab2, C[2], r);
431  r = simde_mm256_fmadd_pd(ab3, C[3], r);
432  // Horizontal sum contracts dim 0
433  return casadi_blazing_hsum<T1>(&r);
434 }
435 
436 // AVX2 tensor-times-vector for 3D cubic B-splines (degree 3, m=1).
437 //
438 // result = sum_{i,j,k} a[i] * b[j] * c[k] * C[j + 4*k][i]
439 //
440 // where a[i] are in AVX lanes, b and c hold 4 weights each (broadcast internally),
441 // and C[16] holds 4x4 coefficient vectors (4 doubles along dim 0 each).
442 //
443 // SYMBOL "blazing_tensor_ttv3"
444 template<typename T1>
445 T1 casadi_blazing_tensor_ttv3(const simde__m256d C[16],
446  const simde__m256d* a, const simde__m256d* b, const simde__m256d* c) {
447  simde__m256d ab[4], cab[4], r;
448  int i;
449  // Broadcast dim 1 weights and form outer product with dim 0
450  simde__m256d b0 = simde_mm256_permute4x64_pd(*b, SIMDE_MM_SHUFFLE(0, 0, 0, 0));
451  simde__m256d b1 = simde_mm256_permute4x64_pd(*b, SIMDE_MM_SHUFFLE(1, 1, 1, 1));
452  simde__m256d b2 = simde_mm256_permute4x64_pd(*b, SIMDE_MM_SHUFFLE(2, 2, 2, 2));
453  simde__m256d b3 = simde_mm256_permute4x64_pd(*b, SIMDE_MM_SHUFFLE(3, 3, 3, 3));
454  ab[0] = simde_mm256_mul_pd(*a, b0);
455  ab[1] = simde_mm256_mul_pd(*a, b1);
456  ab[2] = simde_mm256_mul_pd(*a, b2);
457  ab[3] = simde_mm256_mul_pd(*a, b3);
458  // Contract dim 1: cab[k] = sum_j ab[j] * C[j + 4*k]
459  for (i = 0; i < 4; ++i) {
460  cab[i] = simde_mm256_mul_pd(ab[0], C[4*i+0]);
461  cab[i] = simde_mm256_fmadd_pd(ab[1], C[4*i+1], cab[i]);
462  cab[i] = simde_mm256_fmadd_pd(ab[2], C[4*i+2], cab[i]);
463  cab[i] = simde_mm256_fmadd_pd(ab[3], C[4*i+3], cab[i]);
464  }
465  // Broadcast dim 2 weights and contract: r = sum_k cab[k] * c_k
466  r = simde_mm256_mul_pd(cab[0],
467  simde_mm256_permute4x64_pd(*c, SIMDE_MM_SHUFFLE(0, 0, 0, 0)));
468  r = simde_mm256_fmadd_pd(cab[1],
469  simde_mm256_permute4x64_pd(*c, SIMDE_MM_SHUFFLE(1, 1, 1, 1)), r);
470  r = simde_mm256_fmadd_pd(cab[2],
471  simde_mm256_permute4x64_pd(*c, SIMDE_MM_SHUFFLE(2, 2, 2, 2)), r);
472  r = simde_mm256_fmadd_pd(cab[3],
473  simde_mm256_permute4x64_pd(*c, SIMDE_MM_SHUFFLE(3, 3, 3, 3)), r);
474  // Horizontal sum contracts dim 0
475  return casadi_blazing_hsum<T1>(&r);
476 }
477 
478 // AVX2 tensor-times-vector for 4D cubic B-splines (degree 3, m=1).
479 //
480 // result = sum_{i,j,k,l} a[i] * b[j] * c[k] * d[l]
481 // * coeffs[i + s1*j + s2*k + s3*l]
482 //
483 // Contracts dim 3 from memory into C[16] registers, then delegates to ttv3.
484 // a[i] in AVX lanes (dim 0), b/c/d hold 4 weights each.
485 // coeffs points to the base of the 4x4x4x4 sub-tensor, with strides s1, s2, s3.
486 //
487 // SYMBOL "blazing_tensor_ttv4"
488 template<typename T1>
489 T1 casadi_blazing_tensor_ttv4(const T1* coeffs,
490  casadi_int s1, casadi_int s2, casadi_int s3,
491  const simde__m256d* a, const simde__m256d* b, const simde__m256d* c,
492  const simde__m256d* d) {
493  simde__m256d C[16];
494  int j, k, l;
495  // Broadcast dim 3 weights into an array indexable by l.
496  simde__m256d dbr[4] = {
497  simde_mm256_permute4x64_pd(*d, SIMDE_MM_SHUFFLE(0, 0, 0, 0)),
498  simde_mm256_permute4x64_pd(*d, SIMDE_MM_SHUFFLE(1, 1, 1, 1)),
499  simde_mm256_permute4x64_pd(*d, SIMDE_MM_SHUFFLE(2, 2, 2, 2)),
500  simde_mm256_permute4x64_pd(*d, SIMDE_MM_SHUFFLE(3, 3, 3, 3))
501  };
502  // Contract dim 3 from memory: C[j+4*k] = sum_l d[l] * coeffs[... + s3*l]
503  // Loop order: large stride s3 outer, so the 4x4 (j,k) gather stays in
504  // L1d; reversed order conflict-thrashes when s3 is a power-of-2 stride.
505  for (int idx = 0; idx < 16; ++idx) C[idx] = simde_mm256_setzero_pd();
506  for (l = 0; l < 4; ++l) {
507  const T1* base = coeffs + s3*l;
508  simde__m256d dl = dbr[l];
509  for (j = 0; j < 4; ++j) {
510  for (k = 0; k < 4; ++k) {
511  C[j+4*k] = simde_mm256_fmadd_pd(
512  simde_mm256_loadu_pd(base + s1*j + s2*k), dl, C[j+4*k]);
513  }
514  }
515  }
516  // Dims 0-2 handled by ttv3
517  return casadi_blazing_tensor_ttv3<T1>(C, a, b, c);
518 }
519 
520 // AVX2 tensor-times-vector for 5D cubic B-splines (degree 3, m=1).
521 //
522 // result = sum_{i,j,k,l,m} a[i] * b[j] * c[k] * d[l] * e[m]
523 // * coeffs[i + s1*j + s2*k + s3*l + s4*m]
524 //
525 // Contracts dims 3+4 fused from memory into C[16] registers,
526 // then delegates to ttv3. a[i] in AVX lanes (dim 0),
527 // b/c/d/e hold 4 weights each.
528 //
529 // SYMBOL "blazing_tensor_ttv5"
530 template<typename T1>
531 T1 casadi_blazing_tensor_ttv5(const T1* coeffs,
532  casadi_int s1, casadi_int s2, casadi_int s3, casadi_int s4,
533  const simde__m256d* a, const simde__m256d* b, const simde__m256d* c,
534  const simde__m256d* d, const simde__m256d* e) {
535  simde__m256d C[16];
536  simde__m256d de[16];
537  int j, k, l, m;
538  // Pre-broadcast dim 3 and dim 4 weights (compile-time constant immediates)
539  simde__m256d dbr[4] = {
540  simde_mm256_permute4x64_pd(*d, SIMDE_MM_SHUFFLE(0, 0, 0, 0)),
541  simde_mm256_permute4x64_pd(*d, SIMDE_MM_SHUFFLE(1, 1, 1, 1)),
542  simde_mm256_permute4x64_pd(*d, SIMDE_MM_SHUFFLE(2, 2, 2, 2)),
543  simde_mm256_permute4x64_pd(*d, SIMDE_MM_SHUFFLE(3, 3, 3, 3))
544  };
545  simde__m256d ebr[4] = {
546  simde_mm256_permute4x64_pd(*e, SIMDE_MM_SHUFFLE(0, 0, 0, 0)),
547  simde_mm256_permute4x64_pd(*e, SIMDE_MM_SHUFFLE(1, 1, 1, 1)),
548  simde_mm256_permute4x64_pd(*e, SIMDE_MM_SHUFFLE(2, 2, 2, 2)),
549  simde_mm256_permute4x64_pd(*e, SIMDE_MM_SHUFFLE(3, 3, 3, 3))
550  };
551  // Precompute outer product d x e (16 broadcast weights)
552  for (l = 0; l < 4; ++l) {
553  for (m = 0; m < 4; ++m) {
554  de[l+4*m] = simde_mm256_mul_pd(dbr[l], ebr[m]);
555  }
556  }
557  // Contract dims 3+4 from memory: C[j+4*k] = sum_{l,m} de[l+4*m] * coeffs[...]
558  // Loop order: large strides s3, s4 outer, so the 4x4 (j,k) gather stays
559  // in L1d; reversed order conflict-thrashes on power-of-2 strides.
560  for (int idx = 0; idx < 16; ++idx) C[idx] = simde_mm256_setzero_pd();
561  for (l = 0; l < 4; ++l) {
562  for (m = 0; m < 4; ++m) {
563  const T1* base = coeffs + s3*l + s4*m;
564  simde__m256d delm = de[l+4*m];
565  for (j = 0; j < 4; ++j) {
566  for (k = 0; k < 4; ++k) {
567  C[j+4*k] = simde_mm256_fmadd_pd(
568  simde_mm256_loadu_pd(base + s1*j + s2*k),
569  delm, C[j+4*k]);
570  }
571  }
572  }
573  }
574  // Dims 0-2 handled by ttv3
575  return casadi_blazing_tensor_ttv3<T1>(C, a, b, c);
576 }