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
463constexpr const char *kRelu = R"SOFIE(
464inline void Relu(float *output, float const *input, int size)
465{
466 for (int i = 0; i < size; i++) {
467 output[i] = (input[i] > 0.0f) ? input[i] : 0.0f;
468 }
469}
470)SOFIE";
471
472constexpr const char *kFill = R"SOFIE(
473inline void Fill(float *output, float value, int size)
474{
475 std::fill(output, output + size, value);
476}
477)SOFIE";
478
479constexpr const char *kCopy = R"SOFIE(
480template <class T>
481inline void Copy(T *output, T const *input, int size)
482{
483 std::copy(input, input + size, output);
484}
485)SOFIE";
486
487constexpr const char *kReadTensorFromStream = R"SOFIE(
488inline float ParseFloatToken(const std::string &s)
489{
490 if (s == "inf")
491 return std::numeric_limits<float>::infinity();
492 if (s == "-inf")
493 return -std::numeric_limits<float>::infinity();
494 if (s == "nan")
495 return std::numeric_limits<float>::quiet_NaN();
496 return std::stof(s);
497}
498
499template <class T>
500void ReadTensorFromStream(std::istream &is, T &target, std::string const &expectedName, std::size_t expectedLength)
501{
502 std::string name;
503 std::size_t length;
504 is >> name >> length;
505 if (name != expectedName) {
506 std::string err_msg =
507 "TMVA-SOFIE failed to read the correct tensor name; expected name is " + expectedName + " , read " + name;
508 throw std::runtime_error(err_msg);
509 }
510 if (length != expectedLength) {
511 std::string err_msg = "TMVA-SOFIE failed to read the correct tensor size; expected size is " +
512 std::to_string(expectedLength) + " , read " + std::to_string(length);
513 throw std::runtime_error(err_msg);
514 }
515 std::string token;
516 for (std::size_t i = 0; i < length; ++i) {
517 is >> token;
518 target[i] = ParseFloatToken(token);
519 }
520 if (is.fail()) {
521 throw std::runtime_error("TMVA-SOFIE failed to read the values for tensor " + expectedName);
522 }
523}
524)SOFIE";
525
526// Constexpr helpers carrying static/symbolic shape metadata for the model's
527// input tensors into the emitted code.
528constexpr const char *kInputTensorDims = R"SOFIE(
529struct SingleDim {
530 enum class Kind { Static, Symbolic };
531 Kind kind;
532 std::size_t dim;
533 std::string_view name;
534 constexpr SingleDim(std::size_t v) : kind(Kind::Static), dim(v), name() {}
535 constexpr SingleDim(const char *v) : kind(Kind::Symbolic), dim(0), name(v) {}
536};
537
538struct TensorDims {
539 const SingleDim *data;
540 std::size_t size;
541 constexpr std::size_t total_size() const
542 {
543 std::size_t result = 1;
544 for (std::size_t i = 0; i < size; ++i) {
545 result *= data[i].dim;
546 }
547 return result;
548 }
549};
550
551template <class Arr>
552constexpr TensorDims makeDims(Arr const &arr)
553{
554 return TensorDims{arr.data(), arr.size()};
555}
556)SOFIE";
557
558constexpr const char *kDynamicMemory = R"SOFIE(
559struct TensorLifeInfo {
560 int begin; // start time (operator index) of the tensor's lifetime
561 int end; // end time (operator index)
562 std::size_t size; // size in bytes
563};
564
565struct MemoryResult {
566 std::size_t total_bytes = 0; // total memory needed
567 std::vector<std::size_t> offsets; // resulting offset for each tensor
568};
569
570namespace memory_detail {
571struct FreeBlock {
572 std::size_t offset;
573 std::size_t size;
574 // order by offset for deterministic coalescing
575 bool operator<(const FreeBlock &other) const { return offset < other.offset; }
576};
577struct MemoryEvent {
578 int t; // time (operator index)
579 int type; // 0 = END, 1 = START
580 int idx; // tensor index
581 bool operator<(const MemoryEvent &o) const
582 {
583 if (t != o.t)
584 return t < o.t;
585 return type < o.type; // END before START at the same time
586 }
587};
588} // namespace memory_detail
589
590// Greedy best-fit planner with a coalescing free list.
591inline MemoryResult OrganizeMemory(const std::vector<TensorLifeInfo> &tensorsInfo)
592{
593 using memory_detail::FreeBlock;
594 using memory_detail::MemoryEvent;
595 for (const auto &t : tensorsInfo) {
596 if (!(t.end > t.begin)) {
597 throw std::runtime_error("Each tensor must have end > begin.");
598 }
599 }
600
601 std::vector<MemoryEvent> events;
602 events.reserve(tensorsInfo.size() * 2);
603 for (int i = 0; i < (int)tensorsInfo.size(); ++i) {
604 events.push_back({tensorsInfo[i].end, 0, i});
605 events.push_back({tensorsInfo[i].begin, 1, i});
606 }
607 std::sort(events.begin(), events.end());
608
609 std::vector<std::size_t> tensorsOffset(tensorsInfo.size());
610 std::set<FreeBlock> free_list;
611 std::unordered_map<int, std::size_t> live_size;
612 std::unordered_map<int, std::size_t> live_offset;
613 std::size_t total_bytes = 0;
614
615 auto allocate_best_fit = [&](std::size_t need) -> std::size_t {
616 // Smallest free block with size >= need. free_list is ordered by offset, so
617 // this scans linearly; for very large tensor sets a size-keyed multimap
618 // would avoid the O(n) scan.
619 auto best = free_list.end();
620 for (auto it = free_list.begin(); it != free_list.end(); ++it) {
621 if (it->size >= need) {
622 if (best == free_list.end() || it->size < best->size)
623 best = it;
624 }
625 }
626 if (best != free_list.end()) {
627 std::size_t off = best->offset;
628 if (best->size == need) {
629 free_list.erase(best);
630 } else {
631 FreeBlock updated{best->offset + need, best->size - need};
632 free_list.erase(best);
633 free_list.insert(updated);
634 }
635 return off;
636 }
637 std::size_t off = total_bytes;
638 total_bytes += need;
639 return off;
640 };
641
642 auto try_coalesce = [&](std::set<FreeBlock>::iterator it) {
643 if (it != free_list.begin()) {
644 auto prev = std::prev(it);
645 if (prev->offset + prev->size == it->offset) {
646 FreeBlock merged{prev->offset, prev->size + it->size};
647 free_list.erase(prev);
648 it = free_list.erase(it);
649 it = free_list.insert(merged).first;
650 }
651 }
652 auto next = std::next(it);
653 if (next != free_list.end() && it->offset + it->size == next->offset) {
654 FreeBlock merged{it->offset, it->size + next->size};
655 free_list.erase(next);
656 it = free_list.erase(it);
657 free_list.insert(merged);
658 }
659 };
660
661 for (const auto &e : events) {
662 if (e.type == 0) {
663 auto it_sz = live_size.find(e.idx);
664 auto it_off = live_offset.find(e.idx);
665 if (it_sz != live_size.end() && it_off != live_offset.end()) {
666 FreeBlock fb{it_off->second, it_sz->second};
667 auto it = free_list.insert(fb).first;
668 try_coalesce(it);
669 live_size.erase(it_sz);
670 live_offset.erase(it_off);
671 }
672 } else {
673 auto &t = tensorsInfo[e.idx];
674 std::size_t off = allocate_best_fit(t.size);
675 tensorsOffset[e.idx] = off;
676 live_size[e.idx] = t.size;
677 live_offset[e.idx] = off;
678 }
679 }
680
681 return MemoryResult{total_bytes, std::move(tensorsOffset)};
682}
683)SOFIE";
684
685// GNN_Data is the shared input/output type passed between GNN inference
686// sessions (callers build a TMVA::Experimental::SOFIE::GNN_Data and hand it to
687// infer()), so unlike the other helpers it must NOT be re-defined per model:
688// each model aliases the one shared type. The definition (and RTensor) comes
689// from TMVA/SOFIE_common.hxx, which the generated header includes in this case.
690constexpr const char *kGNNData = R"SOFIE(
691using GNN_Data = TMVA::Experimental::SOFIE::GNN_Data;
692)SOFIE";
693
694} // anonymous namespace
695
697 const std::string &modelNamespace, bool sgemmAlreadyDeclared)
698{
699 auto need = [&](const char *key) { return neededHelpers.count(key) > 0; };
700
701 const bool im2col = need("Im2col");
702 const bool im2col3d = need("Im2col_3d");
703 const bool col2im = need("col2im");
704 const bool uniBroadcast = need("UnidirectionalBroadcast");
705 const bool convBias = need("BroadcastConvBias");
706 const bool gemm = need("Gemm_Call");
707 const bool relu = need("Relu");
708 const bool fill = need("Fill");
709 const bool copy = need("Copy");
710 const bool readTensor = need("ReadTensorFromStream");
711 const bool inputDims = need("InputTensorDims");
712 const bool dynMemory = need("DynamicMemory");
713 const bool gnnData = need("GNN_Data");
714
715 const bool im2colFamily = im2col || im2col3d || col2im;
717 const bool needConvertString = convBias;
718
719 // ---- collect the required standard headers -----------------------------
720 std::set<std::string> stdHeaders;
721 std::set<std::string> otherHeaders;
722 auto addStd = [&](std::initializer_list<const char *> hs) {
723 for (auto h : hs)
724 stdHeaders.insert(h);
725 };
726
727 if (im2colFamily || uniBroadcast || convBias || gemm || fill || copy || dynMemory)
728 addStd({"algorithm"});
730 addStd({"vector", "cstddef"});
732 addStd({"sstream", "string", "stdexcept"});
733 if (readTensor)
734 addStd({"string", "istream", "stdexcept", "limits"});
735 if (inputDims)
736 addStd({"array", "string_view", "cstddef"});
737 if (dynMemory)
738 addStd({"set", "unordered_map", "stdexcept", "iterator"});
739 if (gnnData)
740 // GNN_Data (and, transitively, RTensor) is provided by SOFIE_common.hxx;
741 // see kGNNData for why GNN keeps using the shared type.
742 otherHeaders.insert("TMVA/SOFIE_common.hxx");
743
744 std::string includes;
745 for (auto const &h : stdHeaders)
746 includes += "#include <" + h + ">\n";
747 for (auto const &h : otherHeaders)
748 includes += "#include \"" + h + "\"\n";
749
750 // ---- assemble the definitions ------------------------------------------
751 // The order matters: a definition must precede any non-dependent use of it.
752 std::string defs;
753 defs += "\n// --- Standalone SOFIE inference helper functions ---\n";
754
755 // sgemm_ declaration for Gemm_Call, unless the caller already emitted one.
757 defs += kBlasSgemm;
758
763
765 defs += "\nnamespace UTILITY {\n";
766 if (im2colFamily)
767 defs += kIsAGeZero;
768 if (im2col)
769 defs += kIm2col;
770 if (im2col3d)
771 defs += kIm2col3d;
772 if (col2im)
773 defs += kCol2im;
774 if (uniBroadcast)
776 if (convBias)
778 defs += "} // namespace UTILITY\n";
779 }
780
781 if (gemm)
782 defs += kGemmCall;
783 if (relu)
784 defs += kRelu;
785 if (fill)
786 defs += kFill;
787 if (copy)
788 defs += kCopy;
789 if (readTensor)
791 if (inputDims)
793 if (dynMemory)
795 if (gnnData)
796 defs += kGNNData;
797
798 defs += "// --- End of SOFIE inference helper functions ---\n\n";
799
800 // ---- Clad custom derivatives (pullbacks) -------------------------------
801 // Some helpers cannot be differentiated automatically by Clad (Gemm_Call
802 // calls the bodyless BLAS routine sgemm_). For those we emit a hand-written
803 // pullback. Clad looks up custom derivatives in
804 // clad::custom_derivatives::<function-namespace>, so the pullback is placed
805 // there (mirroring the generated model namespace) rather than next to the
806 // function. The definitions reference the model's own helpers via a using
807 // declaration, so no Clad header is pulled in and a user who never
808 // differentiates the model simply carries an unused inline function.
809 std::string cladDefs;
810 if (gemm || copy || fill || relu) {
811 cladDefs += "\nnamespace clad {\nnamespace custom_derivatives {\nnamespace " + modelNamespace + " {\n";
812 if (gemm) {
813 // Gemm_Call_pullback calls Gemm_Call, so bring it into scope.
814 cladDefs += "using ::" + modelNamespace + "::Gemm_Call;\n";
816 }
817 if (copy)
819 if (fill)
821 if (relu)
823 cladDefs += "} // namespace " + modelNamespace + "\n} // namespace custom_derivatives\n} // namespace clad\n";
824 }
825
826 return HelperFunctionsCode{std::move(includes), std::move(defs), std::move(cladDefs)};
827}
828
829} // namespace SOFIE
830} // namespace Experimental
831} // 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...