Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
SOFIE_common_helpers.cxx
Go to the documentation of this file.
1/// \file SOFIE_common_helpers.cxx
2/// Standalone definitions of the SOFIE inference helpers (Im2col, Gemm_Call, ...).
3/// RModel records which helpers a model needs (RModel_Base::AddNeededHelperFunction)
4/// and dumps only those into the generated namespace, so the emitted header is
5/// self-contained (no TMVA/SOFIE_common.hxx include). These are dependency-free
6/// copies of the SOFIE_common.hxx originals: keep them in sync when those change.
7
9
10namespace TMVA {
11namespace Experimental {
12namespace SOFIE {
13
14namespace {
15
16// Standalone helper snippets, each emitted verbatim inside the generated model
17// namespace. They depend only on the C++ stdlib (headers collected in
18// GenerateHelperFunctionsCode) and, for Gemm, on the local BLAS::sgemm_ below.
19
20// extern "C" declaration of the BLAS routine Gemm_Call needs, in a nested BLAS
21// namespace. Skipped when the caller already declared sgemm_ (see the
22// sgemmAlreadyDeclared parameter of GenerateHelperFunctionsCode).
23constexpr const char *kBlasSgemm = R"SOFIE(
24namespace BLAS {
25extern "C" void sgemm_(const char *transa, const char *transb, const int *m, const int *n, const int *k,
26 const float *alpha, const float *A, const int *lda, const float *B, const int *ldb,
27 const float *beta, float *C, const int *ldc);
28} // namespace BLAS
29)SOFIE";
30
31constexpr const char *kConvertShapeToLength = R"SOFIE(
32inline std::size_t ConvertShapeToLength(const std::vector<std::size_t> &shape)
33{
34 std::size_t length = 1;
35 for (auto &dim : shape)
36 length *= dim;
37 return length;
38}
39)SOFIE";
40
41constexpr const char *kConvertShapeToString = R"SOFIE(
42inline std::string ConvertShapeToString(const std::vector<std::size_t> &shape)
43{
44 std::stringstream out;
45 out << "{ ";
46 for (std::size_t i = 0; i < shape.size(); i++) {
47 out << shape[i];
48 if (i < shape.size() - 1)
49 out << " , ";
50 }
51 out << " }";
52 return out.str();
53}
54)SOFIE";
55
56// Branchless bounds check `0 <= a < b`: casting to unsigned collapses it to a
57// single comparison (a negative `a` wraps to a large value that fails `< b`).
58constexpr const char *kIsAGeZero = R"SOFIE(
59inline bool is_a_ge_zero_and_a_lt_b(int a, int b)
60{
61 return static_cast<unsigned>(a) < static_cast<unsigned>(b);
62}
63)SOFIE";
64
65// im2col: re-arrange convolution input into a matrix usable directly by BLAS.
66// It loops over each element of the filtered region first, following the input
67// layout, so reads/writes stay consecutive in memory; the result is already
68// transposed -- a (channels*kernel_h*kernel_w , output_h*output_w) matrix.
69// Example: input a1 a2 a3
70// b1 b2 b3 with a 2x2 kernel (k1,k2,k3,k4) and padding 1
71// c1 c2 c3
72// gives a 4x16 matrix, output-ordered (all elements for k1, then k2, ...):
73// ( 0 0 0 0 0 a1 a2 a3 0 b1 b2 b3 0 c1 c2 c3 ) k1
74// ( 0 0 0 0 a1 a2 a3 0 b1 b2 b3 0 c1 c2 c3 0 ) k2
75// ( 0 a1 a2 a3 0 b1 b2 b3 0 c1 c2 c3 0 0 0 0 ) k3
76// ( a1 a2 a3 0 b1 b2 b3 0 c1 c2 c3 0 0 0 0 0 ) k4
77// Per-axis begin/end padding can differ (ONNX "pads" attribute, and odd total
78// padding from the SAME_UPPER / SAME_LOWER autopad modes).
79constexpr const char *kIm2col = R"SOFIE(
80template <typename T>
81void Im2col(const T *data_im, const int channels, const int height, const int width, const int kernel_h,
82 const int kernel_w, const int pad_h_begin, const int pad_h_end, const int pad_w_begin,
83 const int pad_w_end, const int stride_h, const int stride_w,
84 const int dilation_h, const int dilation_w, T *data_col)
85{
86 const int output_h = (height + pad_h_begin + pad_h_end - (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1;
87 const int output_w = (width + pad_w_begin + pad_w_end - (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1;
88 const int channel_size = height * width;
89 for (int channel = channels; channel--; data_im += channel_size) {
90 for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) {
91 for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) {
92 int input_row = -pad_h_begin + kernel_row * dilation_h;
93 for (int output_rows = output_h; output_rows; output_rows--) {
94 if (!is_a_ge_zero_and_a_lt_b(input_row, height)) {
95 for (int output_cols = output_w; output_cols; output_cols--) {
96 *(data_col++) = 0;
97 }
98 } else {
99 int input_col = -pad_w_begin + kernel_col * dilation_w;
100 for (int output_col = output_w; output_col; output_col--) {
101 if (is_a_ge_zero_and_a_lt_b(input_col, width)) {
102 *(data_col++) = data_im[input_row * width + input_col];
103 } else {
104 *(data_col++) = 0;
105 }
106 input_col += stride_w;
107 }
108 }
109 input_row += stride_h;
110 }
111 }
112 }
113 }
114}
115)SOFIE";
116
117constexpr const char *kIm2col3d = R"SOFIE(
118template <typename T>
119void Im2col_3d(const T *data_im, const int channels,
120 const int depth, const int height, const int width,
121 const int kernel_d, const int kernel_h, const int kernel_w,
122 const int pad_d_begin, const int pad_d_end, const int pad_h_begin, const int pad_h_end,
123 const int pad_w_begin, const int pad_w_end,
124 const int stride_d, const int stride_h, const int stride_w,
125 const int dilation_d, const int dilation_h, const int dilation_w, T *data_col)
126{
127 const int output_h = (height + pad_h_begin + pad_h_end - (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1;
128 const int output_w = (width + pad_w_begin + pad_w_end - (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1;
129 const int output_d = (depth + pad_d_begin + pad_d_end - (dilation_d * (kernel_d - 1) + 1)) / stride_d + 1;
130 const int channel_size = height * width * depth;
131 for (int channel = channels; channel--; data_im += channel_size) {
132 for (int kernel_depth = 0; kernel_depth < kernel_d; kernel_depth++) {
133 for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) {
134 for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) {
135 int input_dep = -pad_d_begin + kernel_depth * dilation_d;
136 for (int output_dep = output_d; output_dep; output_dep--) {
137 if (!is_a_ge_zero_and_a_lt_b(input_dep, depth)) {
138 for (int output_rows = output_h; output_rows; output_rows--) {
139 for (int output_cols = output_w; output_cols; output_cols--) {
140 *(data_col++) = 0;
141 }
142 }
143 } else {
144 int input_row = -pad_h_begin + kernel_row * dilation_h;
145 for (int output_rows = output_h; output_rows; output_rows--) {
146 if (!is_a_ge_zero_and_a_lt_b(input_row, height)) {
147 for (int output_cols = output_w; output_cols; output_cols--) {
148 *(data_col++) = 0;
149 }
150 } else {
151 int input_col = -pad_w_begin + kernel_col * dilation_w;
152 for (int output_col = output_w; output_col; output_col--) {
153 if (is_a_ge_zero_and_a_lt_b(input_col, width)) {
154 *(data_col++) = data_im[input_dep * width * height + input_row * width + input_col];
155 } else {
156 *(data_col++) = 0;
157 }
158 input_col += stride_w;
159 }
160 }
161 input_row += stride_h;
162 }
163 }
164 input_dep += stride_d;
165 }
166 }
167 }
168 }
169 }
170}
171)SOFIE";
172
173constexpr const char *kCol2im = R"SOFIE(
174template <typename Dtype>
175void col2im(const Dtype *data_col, const int channels,
176 const int height, const int width, const int kernel_h, const int kernel_w,
177 const int pad_h, const int pad_w,
178 const int stride_h, const int stride_w,
179 const int dilation_h, const int dilation_w,
180 Dtype *data_im)
181{
182 // output must start zeroed: col2im scatters with += so overlapping columns accumulate
183 std::fill(data_im, data_im + height * width * channels, 0.);
184 const int output_h = (height + 2 * pad_h - (dilation_h * (kernel_h - 1) + 1)) / stride_h + 1;
185 const int output_w = (width + 2 * pad_w - (dilation_w * (kernel_w - 1) + 1)) / stride_w + 1;
186 const int channel_size = height * width;
187 for (int channel = channels; channel--; data_im += channel_size) {
188 for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) {
189 for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) {
190 int input_row = -pad_h + kernel_row * dilation_h;
191 for (int output_rows = output_h; output_rows; output_rows--) {
192 if (!is_a_ge_zero_and_a_lt_b(input_row, height)) {
193 data_col += output_w;
194 } else {
195 int input_col = -pad_w + kernel_col * dilation_w;
196 for (int output_col = output_w; output_col; output_col--) {
197 if (is_a_ge_zero_and_a_lt_b(input_col, width)) {
198 data_im[input_row * width + input_col] += *data_col;
199 }
200 data_col++;
201 input_col += stride_w;
202 }
203 }
204 input_row += stride_h;
205 }
206 }
207 }
208 }
209}
210)SOFIE";
211
212// Broadcast helpers implementing numpy broadcasting rules (see
213// https://numpy.org/doc/stable/user/basics.broadcasting.html and
214// https://github.com/onnx/onnx/blob/main/docs/Broadcasting.md). Unidirectional
215// broadcast: only the input shape is stretched to targetShape, not vice versa.
216// These are rewritten with respect to TMVA/SOFIE_common.hxx to avoid the
217// dependency on std::span (which would require C++20 or ROOT's RSpan.hxx in the
218// generated code): the input is passed as a raw pointer plus a length instead.
219constexpr const char *kBroadcastTensor = R"SOFIE(
220template <typename T>
221void BroadcastTensor(const T *data, std::size_t curLength, const std::vector<std::size_t> &shape,
222 const std::vector<std::size_t> &targetShape, T *broadcastedData)
223{
224 std::size_t size = shape.size();
225 if (size > 1 && shape.front() == targetShape.front() && shape.back() == 1) {
226 std::size_t bsize = targetShape.back();
227 for (int k = int(size) - 2; k >= 0; k--) {
228 if (shape[k] != 1)
229 break;
230 bsize *= targetShape[k];
231 }
232 for (std::size_t i = 0; i < curLength; i++) {
233 std::fill(broadcastedData + i * bsize, broadcastedData + (i + 1) * bsize, data[i]);
234 }
235 return;
236 }
237
238 std::copy(data, data + curLength, broadcastedData);
239 std::size_t arrayNum = 1;
240 std::vector<T> newData(ConvertShapeToLength(targetShape));
241
242 for (std::size_t idx = 0; idx < size; idx++) {
243 std::size_t dim = shape[idx];
244 std::size_t targetDim = targetShape[idx];
245 if (dim == 1 && targetDim > 1) {
246 std::size_t newLength = curLength * targetDim;
247 std::size_t arrayLength = curLength / arrayNum;
248 if (arrayLength > 1) {
249 for (std::size_t arrayIdx = 0; arrayIdx < arrayNum; arrayIdx++) {
250 for (std::size_t targetIdx = 0; targetIdx < targetDim; targetIdx++) {
251 std::size_t offset = arrayIdx * arrayLength * targetDim + targetIdx * arrayLength;
252 std::copy(broadcastedData + arrayIdx * arrayLength,
253 broadcastedData + (arrayIdx + 1) * arrayLength,
254 newData.begin() + offset);
255 }
256 }
257 } else {
258 for (std::size_t arrayIdx = 0; arrayIdx < arrayNum; arrayIdx++) {
259 std::fill(newData.begin() + arrayIdx * targetDim,
260 newData.begin() + (arrayIdx + 1) * targetDim, broadcastedData[arrayIdx]);
261 }
262 }
263 curLength = newLength;
264 std::copy(newData.begin(), newData.begin() + newLength, broadcastedData);
265 }
266 arrayNum *= targetDim;
267 }
268}
269
270template <typename T>
271T *CreateBroadcastTensor(const T *data, const std::vector<std::size_t> &shape,
272 const std::vector<std::size_t> &targetShape, std::size_t targetLength)
273{
274 T *broadcastedData = new T[targetLength];
275 std::size_t curLength = ConvertShapeToLength(shape);
276 BroadcastTensor<T>(data, curLength, shape, targetShape, broadcastedData);
277 return broadcastedData;
278}
279
280template <typename T>
281T *UnidirectionalBroadcast(const T *data, const std::vector<std::size_t> &shape,
282 const std::vector<std::size_t> &targetShape)
283{
284 if (shape.size() < targetShape.size()) {
285 std::size_t targetSize = targetShape.size();
286 std::vector<std::size_t> newShape(targetSize, 1);
287 std::size_t offset = targetSize - shape.size();
288 std::copy(shape.begin(), shape.end(), newShape.begin() + offset);
289 return CreateBroadcastTensor(data, newShape, targetShape, ConvertShapeToLength(targetShape));
290 }
291 return CreateBroadcastTensor(data, shape, targetShape, ConvertShapeToLength(targetShape));
292}
293
294template <typename T>
295void UnidirectionalBroadcast(const T *data, const std::vector<std::size_t> &shape,
296 const std::vector<std::size_t> &targetShape, T *broadcastedData)
297{
298 std::size_t curLength = ConvertShapeToLength(shape);
299 if (shape.size() < targetShape.size()) {
300 std::size_t targetSize = targetShape.size();
301 std::vector<std::size_t> newShape(targetSize, 1);
302 std::size_t offset = targetSize - shape.size();
303 std::copy(shape.begin(), shape.end(), newShape.begin() + offset);
304 BroadcastTensor(data, curLength, newShape, targetShape, broadcastedData);
305 return;
306 }
307 BroadcastTensor(data, curLength, shape, targetShape, broadcastedData);
308}
309)SOFIE";
310
311constexpr const char *kBroadcastConvBias = R"SOFIE(
312template <typename T>
313T *BroadcastConvBias(const T *data, const std::size_t channel, const std::vector<std::size_t> &targetShape)
314{
315 std::size_t size = targetShape.size();
316 if (targetShape[1] != channel) {
317 std::stringstream ss;
318 ss << "TMVA::SOFIE - Error broadcasting Conv Bias of shape {";
319 ss << std::to_string(channel);
320 ss << "} to ";
321 ss << ConvertShapeToString(targetShape);
322 throw std::runtime_error(ss.str());
323 }
324
325 std::size_t targetLength = ConvertShapeToLength(targetShape);
326 T *newData = new T[targetLength];
327
328 if (targetLength == channel) {
329 std::copy(data, data + channel, newData);
330 return newData;
331 }
332
333 std::size_t cStride = 1;
334 for (std::size_t i = 2; i < size; i++)
335 cStride *= targetShape[i];
336 for (std::size_t i = 0; i < channel; i++) {
337 std::fill(newData + i * cStride, newData + (i + 1) * cStride, data[i]);
338 }
339 std::size_t batch = targetShape[0];
340 std::size_t bStride = channel * cStride;
341 for (std::size_t i = 1; i < batch; i++) {
342 std::copy(newData, newData + bStride, newData + i * bStride);
343 }
344 return newData;
345}
346)SOFIE";
347
348constexpr const char *kGemmCall = R"SOFIE(
349inline void Gemm_Call(float *output, bool transa, bool transb, int m, int n, int k, float alpha, const float *A,
350 const float *B, float beta, const float *C)
351{
352 char ct = 't';
353 char cn = 'n';
354 const int *lda = transa ? &k : &m;
355 const int *ldb = transb ? &n : &k;
356 const int *ldc = &m;
357 if (C != nullptr) {
358 std::copy(C, C + m * n, output);
359 }
360 BLAS::sgemm_(transa ? &ct : &cn, transb ? &ct : &cn, &m, &n, &k, &alpha, A, lda, B, ldb, &beta, output, ldc);
361}
362)SOFIE";
363
364// Custom Clad reverse-mode pullbacks for the helpers (they used to live in
365// Math/CladDerivator.h). Gemm_Call and Copy need a hand-written pullback because
366// their bodies call bodyless routines (sgemm_, std::copy -> memmove) that Clad
367// cannot differentiate; Fill and Relu are included for completeness so a
368// differentiated model needs neither SOFIE_common.hxx nor CladDerivator.h. How
369// they are placed and found is explained at the emission site
370// (GenerateHelperFunctionsCode).
371constexpr const char *kGemmCallPullback = R"SOFIE(
372inline void Gemm_Call_pullback(float *output, bool transa, bool transb, int m, int n, int k, float alpha,
373 const float *A, const float *B, float beta, const float *C, float *_d_output, bool *,
374 bool *, int *, int *, int *, float *_d_alpha, float *_d_A, float *_d_B, float *_d_beta,
375 float *_d_C)
376{
377 // TODO:
378 // - fix and test the implementation for alpha != 1.0
379 if (alpha != 1.0f) {
380 return;
381 }
382
383 // beta needs to be one because we want to add to _d_A and _d_B instead of
384 // overwriting it.
385 float one = 1.;
386
387 // ---- dA ----
388 if (!transa) {
389 // dA += dY * op(B)^T
390 Gemm_Call(_d_A, false, !transb, m, k, n, one, _d_output, B, one, _d_A);
391 } else {
392 // dA += op(B) * dY^T
393 Gemm_Call(_d_A, transb, true, k, m, n, one, B, _d_output, one, _d_A);
394 }
395
396 // ---- dB ----
397 if (!transb) {
398 // dB += op(A)^T * dY
399 Gemm_Call(_d_B, !transa, false, k, n, m, one, A, _d_output, one, _d_B);
400 } else {
401 // dB += dY^T * op(A)
402 Gemm_Call(_d_B, true, transa, n, k, m, one, _d_output, A, one, _d_B);
403 }
404
405 int sizeC = n * m;
406
407 for (int i = 0; i < sizeC; ++i) {
408 if (C) {
409 *_d_alpha += _d_output[i] * (output[i] - beta * C[i]);
410 *_d_beta += _d_output[i] * C[i];
411 } else {
412 *_d_alpha += _d_output[i] * output[i];
413 }
414 if (_d_C)
415 _d_C[i] += _d_output[i] * beta;
416 }
417}
418)SOFIE";
419
420// Pullback for the Copy helper. Copy's body uses std::copy, which lowers to the
421// bodyless __builtin_memmove that Clad cannot differentiate, so a hand-written
422// pullback is required. The generated Copy is a template; this float overload
423// matches its float instantiation (the only one used by inference code).
424constexpr const char *kCopyPullback = R"SOFIE(
425inline void Copy_pullback(float *output, const float *input, int size, float *_d_output, float *_d_input, int *)
426{
427 for (int i = 0; i < size; i++) {
428 output[i] = input[i];
429 _d_input[i] += _d_output[i];
430 _d_output[i] = 0.F;
431 }
432}
433)SOFIE";
434
435// Pullback for the Fill helper (std::fill -> bodyless builtin).
436constexpr const char *kFillPullback = R"SOFIE(
437inline void Fill_pullback(float *output, float value, int size, float *_d_output, float *_d_value, int *)
438{
439 for (int i = 0; i < size; i++) {
440 output[i] = value;
441 *_d_value += _d_output[i];
442 _d_output[i] = 0.F;
443 }
444}
445)SOFIE";
446
447// Pullback for the Relu helper. Relu's body is differentiable by Clad on its
448// own, but providing the pullback keeps the derivative identical to the one
449// previously supplied by Math/CladDerivator.h.
450constexpr const char *kReluPullback = R"SOFIE(
451inline void Relu_pullback(float *output, const float *input, int size, float *_d_output, float *_d_input, int *)
452{
453 for (int i = 0; i < size; i++) {
454 output[i] = input[i] > 0.F ? input[i] : 0.F;
455 float _r_d0 = _d_output[i];
456 _d_output[i] = 0.F;
457 if (input[i] > 0.F)
458 _d_input[i] += _r_d0;
459 }
460}
461)SOFIE";
462
463// Custom Clad forward-mode pushforwards for the same helpers, following
464// clad's convention for void functions: the original parameters followed by
465// same-type tangent clones of every parameter. Clad computes Hessians as
466// reverse-mode derivatives of forward-mode code, so the bodies below are
467// themselves reverse-differentiated and must consist of plain loops and of
468// calls with custom pullbacks only (for the namespace alias on the
469// Gemm_Call calls, see the comment at the emission site). Note that
470// Gemm_Call_pullback silently bails out for alpha != 1 (see the TODO there),
471// so Hessians inherit that restriction.
472constexpr const char *kGemmCallPushforward = R"SOFIE(
473inline void Gemm_Call_pushforward(float *output, bool transa, bool transb, int m, int n, int k, float alpha,
474 const float *A, const float *B, float beta, const float *C, float *_d_output, bool,
475 bool, int, int, int, float _d_alpha, const float *_d_A, const float *_d_B,
476 float _d_beta, const float *_d_C)
477{
478 // Primal: output = alpha * op(A) op(B) + beta * C, where a null C means
479 // "accumulate onto the existing output" (see Gemm_Call).
480 // Tangent:
481 // d_output = alpha * (op(dA) op(B) + op(A) op(dB)) + beta * dC
482 // + d_alpha * op(A) op(B) + d_beta * C
483 // with (C, dC) read as (output, d_output) in accumulate mode. The tangent
484 // is therefore computed first, while output and d_output still hold the
485 // values the primal call overwrites. The pointer null-checks are hoisted
486 // into bools: with a pointer-typed ternary condition, Clad's reverse pass
487 // (as of v2.4) hoists and tapes the condition with the const qualifier
488 // dropped, generating code that does not compile.
489 const bool hasC = C != nullptr;
490 const bool hasdC = _d_C != nullptr;
491 SOFIE_MODEL_NS::Gemm_Call(_d_output, transa, transb, m, n, k, alpha, _d_A, B, (hasC && !hasdC) ? 0.0f : beta, _d_C);
492 SOFIE_MODEL_NS::Gemm_Call(_d_output, transa, transb, m, n, k, alpha, A, _d_B, 1.0f, nullptr);
493 if (_d_alpha != 0.0f) {
494 SOFIE_MODEL_NS::Gemm_Call(_d_output, transa, transb, m, n, k, _d_alpha, A, B, 1.0f, nullptr);
495 }
496 if (_d_beta != 0.0f) {
497 if (hasC) {
498 for (int i = 0; i < m * n; ++i) {
499 _d_output[i] += _d_beta * C[i];
500 }
501 } else {
502 for (int i = 0; i < m * n; ++i) {
503 _d_output[i] += _d_beta * output[i];
504 }
505 }
506 }
507 SOFIE_MODEL_NS::Gemm_Call(output, transa, transb, m, n, k, alpha, A, B, beta, C);
508}
509)SOFIE";
510
511constexpr const char *kCopyPushforward = R"SOFIE(
512inline void Copy_pushforward(float *output, const float *input, int size, float *_d_output, const float *_d_input,
513 int)
514{
515 for (int i = 0; i < size; i++) {
516 output[i] = input[i];
517 _d_output[i] = _d_input[i];
518 }
519}
520)SOFIE";
521
522constexpr const char *kFillPushforward = R"SOFIE(
523inline void Fill_pushforward(float *output, float value, int size, float *_d_output, float _d_value, int)
524{
525 for (int i = 0; i < size; i++) {
526 output[i] = value;
527 _d_output[i] = _d_value;
528 }
529}
530)SOFIE";
531
532constexpr const char *kReluPushforward = R"SOFIE(
533inline void Relu_pushforward(float *output, float const *input, int size, float *_d_output, float const *_d_input,
534 int)
535{
536 // Tangent first: the generated code applies Relu in place, so input/output
537 // (and their tangents) may alias.
538 for (int i = 0; i < size; i++) {
539 _d_output[i] = (input[i] > 0.0f) ? _d_input[i] : 0.0f;
540 output[i] = (input[i] > 0.0f) ? input[i] : 0.0f;
541 }
542}
543)SOFIE";
544
545constexpr const char *kRelu = R"SOFIE(
546inline void Relu(float *output, float const *input, int size)
547{
548 for (int i = 0; i < size; i++) {
549 output[i] = (input[i] > 0.0f) ? input[i] : 0.0f;
550 }
551}
552)SOFIE";
553
554constexpr const char *kFill = R"SOFIE(
555inline void Fill(float *output, float value, int size)
556{
557 std::fill(output, output + size, value);
558}
559)SOFIE";
560
561constexpr const char *kCopy = R"SOFIE(
562template <class T>
563inline void Copy(T *output, T const *input, int size)
564{
565 std::copy(input, input + size, output);
566}
567)SOFIE";
568
569constexpr const char *kReadTensorFromStream = R"SOFIE(
570inline float ParseFloatToken(const std::string &s)
571{
572 if (s == "inf")
573 return std::numeric_limits<float>::infinity();
574 if (s == "-inf")
575 return -std::numeric_limits<float>::infinity();
576 if (s == "nan")
577 return std::numeric_limits<float>::quiet_NaN();
578 return std::stof(s);
579}
580
581template <class T>
582void ReadTensorFromStream(std::istream &is, T &target, std::string const &expectedName, std::size_t expectedLength)
583{
584 std::string name;
585 std::size_t length;
586 is >> name >> length;
587 if (name != expectedName) {
588 std::string err_msg =
589 "TMVA-SOFIE failed to read the correct tensor name; expected name is " + expectedName + " , read " + name;
590 throw std::runtime_error(err_msg);
591 }
592 if (length != expectedLength) {
593 std::string err_msg = "TMVA-SOFIE failed to read the correct tensor size; expected size is " +
594 std::to_string(expectedLength) + " , read " + std::to_string(length);
595 throw std::runtime_error(err_msg);
596 }
597 std::string token;
598 for (std::size_t i = 0; i < length; ++i) {
599 is >> token;
600 target[i] = ParseFloatToken(token);
601 }
602 if (is.fail()) {
603 throw std::runtime_error("TMVA-SOFIE failed to read the values for tensor " + expectedName);
604 }
605}
606)SOFIE";
607
608// Constexpr helpers carrying static/symbolic shape metadata for the model's
609// input tensors into the emitted code.
610constexpr const char *kInputTensorDims = R"SOFIE(
611struct SingleDim {
612 enum class Kind { Static, Symbolic };
613 Kind kind;
614 std::size_t dim;
615 std::string_view name;
616 constexpr SingleDim(std::size_t v) : kind(Kind::Static), dim(v), name() {}
617 constexpr SingleDim(const char *v) : kind(Kind::Symbolic), dim(0), name(v) {}
618};
619
620struct TensorDims {
621 const SingleDim *data;
622 std::size_t size;
623 constexpr std::size_t total_size() const
624 {
625 std::size_t result = 1;
626 for (std::size_t i = 0; i < size; ++i) {
627 result *= data[i].dim;
628 }
629 return result;
630 }
631};
632
633template <class Arr>
634constexpr TensorDims makeDims(Arr const &arr)
635{
636 return TensorDims{arr.data(), arr.size()};
637}
638)SOFIE";
639
640constexpr const char *kDynamicMemory = R"SOFIE(
641struct TensorLifeInfo {
642 int begin; // start time (operator index) of the tensor's lifetime
643 int end; // end time (operator index)
644 std::size_t size; // size in bytes
645};
646
647struct MemoryResult {
648 std::size_t total_bytes = 0; // total memory needed
649 std::vector<std::size_t> offsets; // resulting offset for each tensor
650};
651
652namespace memory_detail {
653struct FreeBlock {
654 std::size_t offset;
655 std::size_t size;
656 // order by offset for deterministic coalescing
657 bool operator<(const FreeBlock &other) const { return offset < other.offset; }
658};
659struct MemoryEvent {
660 int t; // time (operator index)
661 int type; // 0 = END, 1 = START
662 int idx; // tensor index
663 bool operator<(const MemoryEvent &o) const
664 {
665 if (t != o.t)
666 return t < o.t;
667 return type < o.type; // END before START at the same time
668 }
669};
670} // namespace memory_detail
671
672// Greedy best-fit planner with a coalescing free list.
673inline MemoryResult OrganizeMemory(const std::vector<TensorLifeInfo> &tensorsInfo)
674{
675 using memory_detail::FreeBlock;
676 using memory_detail::MemoryEvent;
677 for (const auto &t : tensorsInfo) {
678 if (!(t.end > t.begin)) {
679 throw std::runtime_error("Each tensor must have end > begin.");
680 }
681 }
682
683 std::vector<MemoryEvent> events;
684 events.reserve(tensorsInfo.size() * 2);
685 for (int i = 0; i < (int)tensorsInfo.size(); ++i) {
686 events.push_back({tensorsInfo[i].end, 0, i});
687 events.push_back({tensorsInfo[i].begin, 1, i});
688 }
689 std::sort(events.begin(), events.end());
690
691 std::vector<std::size_t> tensorsOffset(tensorsInfo.size());
692 std::set<FreeBlock> free_list;
693 std::unordered_map<int, std::size_t> live_size;
694 std::unordered_map<int, std::size_t> live_offset;
695 std::size_t total_bytes = 0;
696
697 auto allocate_best_fit = [&](std::size_t need) -> std::size_t {
698 // Smallest free block with size >= need. free_list is ordered by offset, so
699 // this scans linearly; for very large tensor sets a size-keyed multimap
700 // would avoid the O(n) scan.
701 auto best = free_list.end();
702 for (auto it = free_list.begin(); it != free_list.end(); ++it) {
703 if (it->size >= need) {
704 if (best == free_list.end() || it->size < best->size)
705 best = it;
706 }
707 }
708 if (best != free_list.end()) {
709 std::size_t off = best->offset;
710 if (best->size == need) {
711 free_list.erase(best);
712 } else {
713 FreeBlock updated{best->offset + need, best->size - need};
714 free_list.erase(best);
715 free_list.insert(updated);
716 }
717 return off;
718 }
719 std::size_t off = total_bytes;
720 total_bytes += need;
721 return off;
722 };
723
724 auto try_coalesce = [&](std::set<FreeBlock>::iterator it) {
725 if (it != free_list.begin()) {
726 auto prev = std::prev(it);
727 if (prev->offset + prev->size == it->offset) {
728 FreeBlock merged{prev->offset, prev->size + it->size};
729 free_list.erase(prev);
730 it = free_list.erase(it);
731 it = free_list.insert(merged).first;
732 }
733 }
734 auto next = std::next(it);
735 if (next != free_list.end() && it->offset + it->size == next->offset) {
736 FreeBlock merged{it->offset, it->size + next->size};
737 free_list.erase(next);
738 it = free_list.erase(it);
739 free_list.insert(merged);
740 }
741 };
742
743 for (const auto &e : events) {
744 if (e.type == 0) {
745 auto it_sz = live_size.find(e.idx);
746 auto it_off = live_offset.find(e.idx);
747 if (it_sz != live_size.end() && it_off != live_offset.end()) {
748 FreeBlock fb{it_off->second, it_sz->second};
749 auto it = free_list.insert(fb).first;
750 try_coalesce(it);
751 live_size.erase(it_sz);
752 live_offset.erase(it_off);
753 }
754 } else {
755 auto &t = tensorsInfo[e.idx];
756 std::size_t off = allocate_best_fit(t.size);
757 tensorsOffset[e.idx] = off;
758 live_size[e.idx] = t.size;
759 live_offset[e.idx] = off;
760 }
761 }
762
763 return MemoryResult{total_bytes, std::move(tensorsOffset)};
764}
765)SOFIE";
766
767// GNN_Data is the shared input/output type passed between GNN inference
768// sessions (callers build a TMVA::Experimental::SOFIE::GNN_Data and hand it to
769// infer()), so unlike the other helpers it must NOT be re-defined per model:
770// each model aliases the one shared type. The definition (and RTensor) comes
771// from TMVA/SOFIE_common.hxx, which the generated header includes in this case.
772constexpr const char *kGNNData = R"SOFIE(
773using GNN_Data = TMVA::Experimental::SOFIE::GNN_Data;
774)SOFIE";
775
776} // anonymous namespace
777
779 const std::string &modelNamespace, bool sgemmAlreadyDeclared)
780{
781 auto need = [&](const char *key) { return neededHelpers.count(key) > 0; };
782
783 const bool im2col = need("Im2col");
784 const bool im2col3d = need("Im2col_3d");
785 const bool col2im = need("col2im");
786 const bool uniBroadcast = need("UnidirectionalBroadcast");
787 const bool convBias = need("BroadcastConvBias");
788 const bool gemm = need("Gemm_Call");
789 const bool relu = need("Relu");
790 const bool fill = need("Fill");
791 const bool copy = need("Copy");
792 const bool readTensor = need("ReadTensorFromStream");
793 const bool inputDims = need("InputTensorDims");
794 const bool dynMemory = need("DynamicMemory");
795 const bool gnnData = need("GNN_Data");
796
797 const bool im2colFamily = im2col || im2col3d || col2im;
799 const bool needConvertString = convBias;
800
801 // ---- collect the required standard headers -----------------------------
802 std::set<std::string> stdHeaders;
803 std::set<std::string> otherHeaders;
804 auto addStd = [&](std::initializer_list<const char *> hs) {
805 for (auto h : hs)
806 stdHeaders.insert(h);
807 };
808
809 if (im2colFamily || uniBroadcast || convBias || gemm || fill || copy || dynMemory)
810 addStd({"algorithm"});
812 addStd({"vector", "cstddef"});
814 addStd({"sstream", "string", "stdexcept"});
815 if (readTensor)
816 addStd({"string", "istream", "stdexcept", "limits"});
817 if (inputDims)
818 addStd({"array", "string_view", "cstddef"});
819 if (dynMemory)
820 addStd({"set", "unordered_map", "stdexcept", "iterator"});
821 if (gnnData)
822 // GNN_Data (and, transitively, RTensor) is provided by SOFIE_common.hxx;
823 // see kGNNData for why GNN keeps using the shared type.
824 otherHeaders.insert("TMVA/SOFIE_common.hxx");
825
826 std::string includes;
827 for (auto const &h : stdHeaders)
828 includes += "#include <" + h + ">\n";
829 for (auto const &h : otherHeaders)
830 includes += "#include \"" + h + "\"\n";
831
832 // ---- assemble the definitions ------------------------------------------
833 // The order matters: a definition must precede any non-dependent use of it.
834 std::string defs;
835 defs += "\n// --- Standalone SOFIE inference helper functions ---\n";
836
837 // sgemm_ declaration for Gemm_Call, unless the caller already emitted one.
839 defs += kBlasSgemm;
840
845
847 defs += "\nnamespace UTILITY {\n";
848 if (im2colFamily)
849 defs += kIsAGeZero;
850 if (im2col)
851 defs += kIm2col;
852 if (im2col3d)
853 defs += kIm2col3d;
854 if (col2im)
855 defs += kCol2im;
856 if (uniBroadcast)
858 if (convBias)
860 defs += "} // namespace UTILITY\n";
861 }
862
863 if (gemm)
864 defs += kGemmCall;
865 if (relu)
866 defs += kRelu;
867 if (fill)
868 defs += kFill;
869 if (copy)
870 defs += kCopy;
871 if (readTensor)
873 if (inputDims)
875 if (dynMemory)
877 if (gnnData)
878 defs += kGNNData;
879
880 defs += "// --- End of SOFIE inference helper functions ---\n\n";
881
882 // ---- Clad custom derivatives (pullbacks and pushforwards) --------------
883 // Some helpers cannot be differentiated automatically by Clad (Gemm_Call
884 // calls the bodyless BLAS routine sgemm_). For those we emit a hand-written
885 // pullback (reverse mode) and pushforward (forward mode). Clad looks up
886 // custom derivatives in
887 // clad::custom_derivatives::<function-namespace>, so they are placed
888 // there (mirroring the generated model namespace) rather than next to the
889 // function. The definitions reference the model's own helpers via a using
890 // declaration / namespace alias, so no Clad header is pulled in and a user
891 // who never differentiates the model simply carries unused inline
892 // functions.
893 std::string cladDefs;
894 if (gemm || copy || fill || relu) {
895 cladDefs += "\nnamespace clad {\nnamespace custom_derivatives {\nnamespace " + modelNamespace + " {\n";
896 if (gemm) {
897 // Gemm_Call_pullback calls Gemm_Call, so bring it into scope. The
898 // pushforward uses the SOFIE_MODEL_NS alias instead: its body gets
899 // reverse-differentiated by Clad for Hessians, and Clad resolves the
900 // custom Gemm_Call_pullback for the inner calls only when they do
901 // not go through a using-declaration.
902 cladDefs += "using ::" + modelNamespace + "::Gemm_Call;\n";
903 cladDefs += "namespace SOFIE_MODEL_NS = ::" + modelNamespace + ";\n";
906 }
907 if (copy) {
910 }
911 if (fill) {
914 }
915 if (relu) {
918 }
919 cladDefs += "} // namespace " + modelNamespace + "\n} // namespace custom_derivatives\n} // namespace clad\n";
920 }
921
922 return HelperFunctionsCode{std::move(includes), std::move(defs), std::move(cladDefs)};
923}
924
925} // namespace SOFIE
926} // namespace Experimental
927} // namespace TMVA
#define h(i)
Definition RSha256.hxx:106
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.
HelperFunctionsCode GenerateHelperFunctionsCode(const std::set< std::string > &neededHelpers, const std::string &modelNamespace, bool sgemmAlreadyDeclared=false)
Return the standalone C++ source of the inference helper functions requested in neededHelpers (see RM...
create variable transformations
Source code of the inference helper functions to embed in generated code so that it is standalone and...