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 simde__m256d casadi_blazing_boor_der(simde__m256d boor, simde__m256d scale) {
253  simde__m256d sb = simde_mm256_mul_pd(boor, scale);
254  simde__m256d shifted = simde_mm256_permute4x64_pd(sb, SIMDE_MM_SHUFFLE(2, 1, 0, 0));
255  shifted = simde_mm256_blend_pd(simde_mm256_setzero_pd(), shifted, 0xE);
256  return simde_mm256_sub_pd(shifted, sb);
257 }
258 
259 // SYMBOL "blazing_shift_left"
260 // Shift AVX vector one position to the left, filling position 3 with zero.
261 // [a, b, c, d] -> [b, c, d, 0]
262 template<typename T1>
263 simde__m256d casadi_blazing_shift_left(simde__m256d v) {
264  simde__m256d shifted = simde_mm256_permute4x64_pd(v, SIMDE_MM_SHUFFLE(3, 3, 2, 1));
265  return simde_mm256_blend_pd(shifted, simde_mm256_setzero_pd(), 0x8);
266 }
267 
268 // SYMBOL "blazing_knot_scale"
269 // Compute knot scale with safe division: degree/(t_hi - t_lo), returning 0 where span is 0.
270 // This avoids NaN from 0/0 at knot boundaries where both the basis function and
271 // the knot span are zero.
272 template<typename T1>
273 simde__m256d casadi_blazing_knot_scale(simde__m256d degree, simde__m256d t_hi, simde__m256d t_lo) {
274  simde__m256d zero = simde_mm256_setzero_pd();
275  simde__m256d denom = simde_mm256_sub_pd(t_hi, t_lo);
276  simde__m256d denom_mask = simde_mm256_cmp_pd(denom, zero, SIMDE_CMP_EQ_OQ);
277  simde__m256d scale = simde_mm256_div_pd(degree, denom);
278  return simde_mm256_blendv_pd(scale, zero, denom_mask);
279 }
280 
281 // SYMBOL "blazing_boor_init"
282 // Per-dimension de Boor setup: knot lookup, boundary-case init, and de Boor evaluation.
283 // Returns the start index and fills d0, d1, d2 with basis function intermediates.
284 // dim_cache points at THIS dimension's slice of the global cache (or NULL).
285 // The caller is responsible for advancing the pointer between dims.
286 //
287 // Per-dimension cache layout (always emitted unconditionally when the cache
288 // is present, even when lookup_mode != 1):
289 // [intercept, slope, inv1[n_k], inv2[n_k], inv3[n_k]]
290 // where slope = (ng-1) / (grid[ng-1] - grid[0])
291 // intercept = -grid[0] * slope
292 // and ng = n_k - 2*degree. The first two entries feed 'exact' lookup as a
293 // single FMA; the inv1/inv2/inv3 spans feed the de Boor recurrence.
294 //
295 // If inv2_out/inv3_out are non-NULL, they receive pre-positioned derivative
296 // base pointers that can be passed directly to dbasis/d2basis.
297 template<typename T1>
298 casadi_int casadi_blazing_boor_init(
299  T1 x, const T1* all_knots, const T1* dim_cache,
300  casadi_int knot_offset, casadi_int knot_offset_next,
301  casadi_int lookup_mode,
302  simde__m256d* d0, simde__m256d* d1, simde__m256d* d2,
303  const T1** inv2_out, const T1** inv3_out) {
304  casadi_int degree = 3;
305  const T1* knots = all_knots + knot_offset;
306  casadi_int n_knots = knot_offset_next - knot_offset;
307  casadi_int n_b = n_knots - degree - 1;
308  // First two entries of dim_cache are {intercept, slope} for 'exact' lookup.
309  const T1* grid_inv = dim_cache;
310  casadi_int L = casadi_blazing_low<T1>(x, knots + degree,
311  n_knots - 2*degree,
312  lookup_mode, grid_inv);
313  casadi_int start = L;
314  if (start > n_b - degree - 1) start = n_b - degree - 1;
315 
316  simde__m256d d3 = simde_mm256_setzero_pd();
317  if (x >= knots[0] && x <= knots[n_knots-1]) {
318  if (x == knots[1]) {
319  d3 = simde_mm256_set1_pd(1.0);
320  } else if (x == knots[n_knots-1]) {
321  d3 = simde_mm256_set_pd(1.0, 0.0, 0.0, 0.0);
322  } else if (knots[L+degree] == x) {
323  d3 = simde_mm256_set_pd(0.0, 1.0, 0.0, 0.0);
324  } else {
325  d3 = simde_mm256_set_pd(1.0, 0.0, 0.0, 0.0);
326  }
327  }
328  const T1 *inv1 = 0, *inv2 = 0, *inv3 = 0;
329  if (dim_cache) {
330  // Skip the {intercept, slope} prefix to reach the inv1/inv2/inv3 spans.
331  const T1* inv_base = dim_cache + 2;
332  inv1 = inv_base + start;
333  inv2 = inv_base + n_knots + start;
334  inv3 = inv_base + 2*n_knots + start;
335  if (inv2_out) *inv2_out = inv_base + n_knots + start + 1;
336  if (inv3_out) *inv3_out = inv_base + 2*n_knots + start + 1;
337  } else {
338  if (inv2_out) *inv2_out = 0;
339  if (inv3_out) *inv3_out = 0;
340  }
341  casadi_blazing_de_boor(x, knots + start, inv1, inv2, inv3, d0, d1, d2, &d3);
342  return start;
343 }
344 
345 // SYMBOL "blazing_dbasis"
346 // Compute 1st-derivative basis functions for one dimension.
347 // t points to knots at starts[i] for this dimension.
348 // inv3 can be 0 (NULL) to use the division path; otherwise 1/(t[k+3]-t[k]).
349 template<typename T1>
350 simde__m256d casadi_blazing_dbasis(simde__m256d boor_d1, const T1* t, const T1* inv3) {
351  simde__m256d three = simde_mm256_set1_pd(3.0);
352  simde__m256d s1;
353  if (inv3) {
354  s1 = simde_mm256_mul_pd(three, simde_mm256_loadu_pd(inv3));
355  } else {
356  s1 = casadi_blazing_knot_scale<T1>(three,
357  simde_mm256_loadu_pd(t + 4), simde_mm256_loadu_pd(t + 1));
358  }
359  return casadi_blazing_boor_der<T1>(
360  casadi_blazing_shift_left<T1>(boor_d1), s1);
361 }
362 
363 // SYMBOL "blazing_d2basis"
364 // Compute 2nd-derivative basis functions for one dimension.
365 // t points to knots at starts[i] for this dimension.
366 // inv2, inv3 can be 0 (NULL) to use the division path;
367 // otherwise 1/(t[k+2]-t[k]) and 1/(t[k+3]-t[k]).
368 template<typename T1>
369 simde__m256d casadi_blazing_d2basis(simde__m256d boor_d2, const T1* t, const T1* inv2, const T1* inv3) {// NOLINT(whitespace/line_length)
370  simde__m256d three = simde_mm256_set1_pd(3.0);
371  simde__m256d two = simde_mm256_set1_pd(2.0);
372  simde__m256d s1, s2;
373  if (inv3) {
374  s1 = simde_mm256_mul_pd(three, simde_mm256_loadu_pd(inv3));
375  s2 = simde_mm256_mul_pd(two, simde_mm256_loadu_pd(inv2));
376  } else {
377  s1 = casadi_blazing_knot_scale<T1>(three,
378  simde_mm256_loadu_pd(t + 4), simde_mm256_loadu_pd(t + 1));
379  s2 = casadi_blazing_knot_scale<T1>(two,
380  simde_mm256_loadu_pd(t + 3), simde_mm256_loadu_pd(t + 1));
381  }
382  simde__m256d inner = casadi_blazing_boor_der<T1>(
383  casadi_blazing_shift_left<T1>(boor_d2), s2);
384  return casadi_blazing_boor_der<T1>(
385  casadi_blazing_shift_left<T1>(inner), s1);
386 }
387 
388 // ===== Tensor-times-vector contractions =====
389 
390 // AVX2 horizontal sum: reduce 4-wide __m256d to scalar double
391 // SYMBOL "blazing_hsum"
392 template<typename T1>
393 T1 casadi_blazing_hsum(simde__m256d r) {
394  simde__m128d r0 = simde_mm256_castpd256_pd128(r);
395  simde__m128d r1 = simde_mm256_extractf128_pd(r, 1);
396  r0 = simde_mm_add_pd(r0, r1);
397  return simde_mm_cvtsd_f64(simde_mm_add_sd(r0, simde_mm_unpackhi_pd(r0, r0)));
398 }
399 
400 // AVX2 tensor-times-vector for 2D cubic B-splines (degree 3, m=1).
401 //
402 // result = sum_{i,j} a[i] * b[j] * C[j][i]
403 //
404 // where a[i] are in AVX lanes, b[j] are in AVX lanes (broadcast internally),
405 // and C[4] holds coefficient vectors (4 doubles along dim 0 each).
406 //
407 // SYMBOL "blazing_tensor_ttv2"
408 template<typename T1>
409 T1 casadi_blazing_tensor_ttv2(const simde__m256d C[4],
410  simde__m256d a, simde__m256d b) {
411  simde__m256d r;
412  // Broadcast dim 1 weights and form outer product with dim 0
413  simde__m256d ab0 = simde_mm256_mul_pd(a,
414  simde_mm256_permute4x64_pd(b, SIMDE_MM_SHUFFLE(0, 0, 0, 0)));
415  simde__m256d ab1 = simde_mm256_mul_pd(a,
416  simde_mm256_permute4x64_pd(b, SIMDE_MM_SHUFFLE(1, 1, 1, 1)));
417  simde__m256d ab2 = simde_mm256_mul_pd(a,
418  simde_mm256_permute4x64_pd(b, SIMDE_MM_SHUFFLE(2, 2, 2, 2)));
419  simde__m256d ab3 = simde_mm256_mul_pd(a,
420  simde_mm256_permute4x64_pd(b, SIMDE_MM_SHUFFLE(3, 3, 3, 3)));
421  // Contract dim 1: r = sum_j ab[j] * C[j]
422  r = simde_mm256_mul_pd(ab0, C[0]);
423  r = simde_mm256_fmadd_pd(ab1, C[1], r);
424  r = simde_mm256_fmadd_pd(ab2, C[2], r);
425  r = simde_mm256_fmadd_pd(ab3, C[3], r);
426  // Horizontal sum contracts dim 0
427  return casadi_blazing_hsum<T1>(r);
428 }
429 
430 // AVX2 tensor-times-vector for 3D cubic B-splines (degree 3, m=1).
431 //
432 // result = sum_{i,j,k} a[i] * b[j] * c[k] * C[j + 4*k][i]
433 //
434 // where a[i] are in AVX lanes, b and c hold 4 weights each (broadcast internally),
435 // and C[16] holds 4x4 coefficient vectors (4 doubles along dim 0 each).
436 //
437 // SYMBOL "blazing_tensor_ttv3"
438 template<typename T1>
439 T1 casadi_blazing_tensor_ttv3(const simde__m256d C[16],
440  simde__m256d a, simde__m256d b, simde__m256d c) {
441  simde__m256d ab[4], cab[4], r;
442  int i;
443  // Broadcast dim 1 weights and form outer product with dim 0
444  simde__m256d b0 = simde_mm256_permute4x64_pd(b, SIMDE_MM_SHUFFLE(0, 0, 0, 0));
445  simde__m256d b1 = simde_mm256_permute4x64_pd(b, SIMDE_MM_SHUFFLE(1, 1, 1, 1));
446  simde__m256d b2 = simde_mm256_permute4x64_pd(b, SIMDE_MM_SHUFFLE(2, 2, 2, 2));
447  simde__m256d b3 = simde_mm256_permute4x64_pd(b, SIMDE_MM_SHUFFLE(3, 3, 3, 3));
448  ab[0] = simde_mm256_mul_pd(a, b0);
449  ab[1] = simde_mm256_mul_pd(a, b1);
450  ab[2] = simde_mm256_mul_pd(a, b2);
451  ab[3] = simde_mm256_mul_pd(a, b3);
452  // Contract dim 1: cab[k] = sum_j ab[j] * C[j + 4*k]
453  for (i = 0; i < 4; ++i) {
454  cab[i] = simde_mm256_mul_pd(ab[0], C[4*i+0]);
455  cab[i] = simde_mm256_fmadd_pd(ab[1], C[4*i+1], cab[i]);
456  cab[i] = simde_mm256_fmadd_pd(ab[2], C[4*i+2], cab[i]);
457  cab[i] = simde_mm256_fmadd_pd(ab[3], C[4*i+3], cab[i]);
458  }
459  // Broadcast dim 2 weights and contract: r = sum_k cab[k] * c_k
460  r = simde_mm256_mul_pd(cab[0],
461  simde_mm256_permute4x64_pd(c, SIMDE_MM_SHUFFLE(0, 0, 0, 0)));
462  r = simde_mm256_fmadd_pd(cab[1],
463  simde_mm256_permute4x64_pd(c, SIMDE_MM_SHUFFLE(1, 1, 1, 1)), r);
464  r = simde_mm256_fmadd_pd(cab[2],
465  simde_mm256_permute4x64_pd(c, SIMDE_MM_SHUFFLE(2, 2, 2, 2)), r);
466  r = simde_mm256_fmadd_pd(cab[3],
467  simde_mm256_permute4x64_pd(c, SIMDE_MM_SHUFFLE(3, 3, 3, 3)), r);
468  // Horizontal sum contracts dim 0
469  return casadi_blazing_hsum<T1>(r);
470 }
471 
472 // AVX2 tensor-times-vector for 4D cubic B-splines (degree 3, m=1).
473 //
474 // result = sum_{i,j,k,l} a[i] * b[j] * c[k] * d[l]
475 // * coeffs[i + s1*j + s2*k + s3*l]
476 //
477 // Contracts dim 3 from memory into C[16] registers, then delegates to ttv3.
478 // a[i] in AVX lanes (dim 0), b/c/d hold 4 weights each.
479 // coeffs points to the base of the 4x4x4x4 sub-tensor, with strides s1, s2, s3.
480 //
481 // SYMBOL "blazing_tensor_ttv4"
482 template<typename T1>
483 T1 casadi_blazing_tensor_ttv4(const T1* coeffs,
484  casadi_int s1, casadi_int s2, casadi_int s3,
485  simde__m256d a, simde__m256d b, simde__m256d c, simde__m256d d) {
486  simde__m256d C[16];
487  int j, k, l;
488  // Broadcast dim 3 weights into an array indexable by l.
489  simde__m256d dbr[4] = {
490  simde_mm256_permute4x64_pd(d, SIMDE_MM_SHUFFLE(0, 0, 0, 0)),
491  simde_mm256_permute4x64_pd(d, SIMDE_MM_SHUFFLE(1, 1, 1, 1)),
492  simde_mm256_permute4x64_pd(d, SIMDE_MM_SHUFFLE(2, 2, 2, 2)),
493  simde_mm256_permute4x64_pd(d, SIMDE_MM_SHUFFLE(3, 3, 3, 3))
494  };
495  // Contract dim 3 from memory: C[j+4*k] = sum_l d[l] * coeffs[... + s3*l]
496  // Loop order: large stride s3 outer, so the 4x4 (j,k) gather stays in
497  // L1d; reversed order conflict-thrashes when s3 is a power-of-2 stride.
498  for (int idx = 0; idx < 16; ++idx) C[idx] = simde_mm256_setzero_pd();
499  for (l = 0; l < 4; ++l) {
500  const T1* base = coeffs + s3*l;
501  simde__m256d dl = dbr[l];
502  for (j = 0; j < 4; ++j) {
503  for (k = 0; k < 4; ++k) {
504  C[j+4*k] = simde_mm256_fmadd_pd(
505  simde_mm256_loadu_pd(base + s1*j + s2*k), dl, C[j+4*k]);
506  }
507  }
508  }
509  // Dims 0-2 handled by ttv3
510  return casadi_blazing_tensor_ttv3<T1>(C, a, b, c);
511 }
512 
513 // AVX2 tensor-times-vector for 5D cubic B-splines (degree 3, m=1).
514 //
515 // result = sum_{i,j,k,l,m} a[i] * b[j] * c[k] * d[l] * e[m]
516 // * coeffs[i + s1*j + s2*k + s3*l + s4*m]
517 //
518 // Contracts dims 3+4 fused from memory into C[16] registers,
519 // then delegates to ttv3. a[i] in AVX lanes (dim 0),
520 // b/c/d/e hold 4 weights each.
521 //
522 // SYMBOL "blazing_tensor_ttv5"
523 template<typename T1>
524 T1 casadi_blazing_tensor_ttv5(const T1* coeffs,
525  casadi_int s1, casadi_int s2, casadi_int s3, casadi_int s4,
526  simde__m256d a, simde__m256d b, simde__m256d c,
527  simde__m256d d, simde__m256d e) {
528  simde__m256d C[16];
529  simde__m256d de[16];
530  int j, k, l, m;
531  // Pre-broadcast dim 3 and dim 4 weights (compile-time constant immediates)
532  simde__m256d dbr[4] = {
533  simde_mm256_permute4x64_pd(d, SIMDE_MM_SHUFFLE(0, 0, 0, 0)),
534  simde_mm256_permute4x64_pd(d, SIMDE_MM_SHUFFLE(1, 1, 1, 1)),
535  simde_mm256_permute4x64_pd(d, SIMDE_MM_SHUFFLE(2, 2, 2, 2)),
536  simde_mm256_permute4x64_pd(d, SIMDE_MM_SHUFFLE(3, 3, 3, 3))
537  };
538  simde__m256d ebr[4] = {
539  simde_mm256_permute4x64_pd(e, SIMDE_MM_SHUFFLE(0, 0, 0, 0)),
540  simde_mm256_permute4x64_pd(e, SIMDE_MM_SHUFFLE(1, 1, 1, 1)),
541  simde_mm256_permute4x64_pd(e, SIMDE_MM_SHUFFLE(2, 2, 2, 2)),
542  simde_mm256_permute4x64_pd(e, SIMDE_MM_SHUFFLE(3, 3, 3, 3))
543  };
544  // Precompute outer product d x e (16 broadcast weights)
545  for (l = 0; l < 4; ++l) {
546  for (m = 0; m < 4; ++m) {
547  de[l+4*m] = simde_mm256_mul_pd(dbr[l], ebr[m]);
548  }
549  }
550  // Contract dims 3+4 from memory: C[j+4*k] = sum_{l,m} de[l+4*m] * coeffs[...]
551  // Loop order: large strides s3, s4 outer, so the 4x4 (j,k) gather stays
552  // in L1d; reversed order conflict-thrashes on power-of-2 strides.
553  for (int idx = 0; idx < 16; ++idx) C[idx] = simde_mm256_setzero_pd();
554  for (l = 0; l < 4; ++l) {
555  for (m = 0; m < 4; ++m) {
556  const T1* base = coeffs + s3*l + s4*m;
557  simde__m256d delm = de[l+4*m];
558  for (j = 0; j < 4; ++j) {
559  for (k = 0; k < 4; ++k) {
560  C[j+4*k] = simde_mm256_fmadd_pd(
561  simde_mm256_loadu_pd(base + s1*j + s2*k),
562  delm, C[j+4*k]);
563  }
564  }
565  }
566  }
567  // Dims 0-2 handled by ttv3
568  return casadi_blazing_tensor_ttv3<T1>(C, a, b, c);
569 }