Initial dynamic input support for gpu codegen - #45
Conversation
2946253 to
78eb28e
Compare
01ddd4c to
e8623a1
Compare
…input-alpaka # Conflicts: # core/inc/SOFIE/ROperator_Conv.hxx # test/TestCustomModelsFromONNXForAlpakaCuda.cxx
|
/runtest h100 |
|
|
|
/runtest h100-47gb |
|
|
|
|
sanjibansg
left a comment
There was a problem hiding this comment.
Thanks for this very useful implementation, couple of initial comments.
| } | ||
|
|
||
| std::vector<std::string> GetBlasRoutines() override { return { std::string("Copy"), std::string("Axpy") }; } | ||
| std::vector<std::string> GetBlasRoutines() override { return {}; } |
There was a problem hiding this comment.
we should keep the blas routines here anyway, since they might be needed for the cpu inference.
There was a problem hiding this comment.
actually the older implementation expanded the channels weights to full size at generation time so that scopy/saxpy could operate at same vector length, however the new implementation now uses fused per channel scale and a single fused loop (this approach is the same as what we have for root/sofie); so to sum it up we would not need to have blas routines now, i kept the function here since it was as is in root/sofie's implementation but maybe its better if we don't keep this (?)
| return out.str(); | ||
| } | ||
|
|
||
| // Computed once so the kernel signature and the call site cannot drift apart. |
There was a problem hiding this comment.
add a bit more descriptive comment here, i.e. signalling where is this computed and for what purpose.
| } | ||
| // find shape of Y and add it in the list of intermediate tensors | ||
| fShapeY = ShapeInference({fShapeX})[0]; | ||
| fShapeY = DoShapeInference(fShapeX); |
There was a problem hiding this comment.
Instead of a differently named method, can we have an overloaded one?
| op += SP + SP + SP + "for (std::size_t l = tid; l < axis_size; l += " + bs + "u) {\n"; | ||
| op += SP + SP + SP + SP + "T x = X[row_base + l * inner_stride];\n"; | ||
| op += SP + SP + SP + SP + "T m_new = (x > m) ? x : m;\n"; | ||
| op += SP + SP + SP + SP + "d = d * alpaka::math::exp(acc, m - m_new) + alpaka::math::exp(acc, x - m_new);\n"; |
There was a problem hiding this comment.
| op += SP + SP + SP + SP + "d = d * alpaka::math::exp(acc, m - m_new) + alpaka::math::exp(acc, x - m_new);\n"; | |
| op += SP + SP + SP + SP + "d = d * exp(acc, m - m_new) + exp(acc, x - m_new);\n"; |
There was a problem hiding this comment.
gpu/alpaka now has SOFIE_DEVICE_exp macro, so would it be better to use that here instead of simple exp??
| op += SP + SP + SP + SP + "T e = alpaka::math::exp(acc, X[idx] - vmax) * inv;\n"; | ||
| op += SP + SP + SP + SP + "Y[idx] = e;\n"; | ||
| if (fLogSoftmax) | ||
| op += SP + SP + SP + SP + "Y[idx] = alpaka::math::log(acc, e);\n"; |
There was a problem hiding this comment.
| op += SP + SP + SP + SP + "Y[idx] = alpaka::math::log(acc, e);\n"; | |
| op += SP + SP + SP + SP + "Y[idx] = log(acc, e);\n"; |
| op += SP + SP + SP + "T const inv = static_cast<T>(1) / sum;\n"; | ||
| op += SP + SP + SP + "for (std::size_t l = tid; l < axis_size; l += " + bs + "u) {\n"; | ||
| op += SP + SP + SP + SP + "std::size_t const idx = row_base + l * inner_stride;\n"; | ||
| op += SP + SP + SP + SP + "T e = alpaka::math::exp(acc, X[idx] - vmax) * inv;\n"; |
There was a problem hiding this comment.
| op += SP + SP + SP + SP + "T e = alpaka::math::exp(acc, X[idx] - vmax) * inv;\n"; | |
| op += SP + SP + SP + SP + "T e = exp(acc, X[idx] - vmax) * inv;\n"; |
| op += SP + SP + SP + SP + SP + "T m_a = smax[tid];\n"; | ||
| op += SP + SP + SP + SP + SP + "T m_b = smax[tid + s];\n"; | ||
| op += SP + SP + SP + SP + SP + "T m_r = (m_b > m_a) ? m_b : m_a;\n"; | ||
| op += SP + SP + SP + SP + SP + "ssum[tid] = ssum[tid] * alpaka::math::exp(acc, m_a - m_r) + ssum[tid + s] * alpaka::math::exp(acc, m_b - m_r);\n"; |
There was a problem hiding this comment.
| op += SP + SP + SP + SP + SP + "ssum[tid] = ssum[tid] * alpaka::math::exp(acc, m_a - m_r) + ssum[tid + s] * alpaka::math::exp(acc, m_b - m_r);\n"; | |
| op += SP + SP + SP + SP + SP + "ssum[tid] = ssum[tid] * exp(acc, m_a - m_r) + ssum[tid + s] * exp(acc, m_b - m_r);\n"; |
| out << SP << "auto const elementsPerThread_" << fNVal << " = Vec::all(static_cast<Idx>(1));\n"; | ||
| out << SP << "auto const elementsPerGrid_" << fNVal << " = Vec::all(static_cast<Idx>(" << numSlices << "));\n"; | ||
| out << SP << "auto const workDiv_" << fNVal << " = sofie_workdiv(elementsPerGrid_" << fNVal << ");\n"; | ||
| out << SP << "alpaka::exec<Acc>(queue, workDiv_" << fNVal << ", topKernel_" << fNVal |
There was a problem hiding this comment.
can we instead create a task kernel and enqueue it?
| // for(int i=0;i<fShapeX.size();i++) | ||
| // std::cout<<fShapeX[i]<<" "; | ||
| // std::cout<<"\ny size -> "<<fShapeY.size()<<std::endl; | ||
| fK = fShapeX[fAttrAxis].isParam ? kval : std::min(kval, fShapeX[fAttrAxis].dim); |
There was a problem hiding this comment.
fK seems like an important variable, be a bit more descriptive in its naming.
|
|
||
| op += SP + SP + SP + "for (int64_t l = " + K + "; l < (int64_t)nElAxis; ++l) {\n"; | ||
| op += SP + SP + SP + SP + "T v = x[xbase + strideXAxis * (std::size_t)l];\n"; | ||
| op += SP + SP + SP + SP + "if (v " + CMP + " bestV[" + K + "-1]) {\n"; |
There was a problem hiding this comment.
if cases are critical in GPUs, can we have a workaround here to avoid branching?
…input-alpaka # Conflicts: # core/src/RModel_ALPAKA.cxx # test/alpaka/TestAlpakaShapeIndexing.cxx
…xing codegen bugs
| // Extend chain: only if CURRENT op is elementwise and its single output can be fused | ||
| size_t current = i; | ||
| while (fOperators[current]->IsElementwise()) { | ||
| while (fOperators[current]->IsElementwise() && !fOperators[current]->IsOutputConstant()) { |
There was a problem hiding this comment.
remove 2nd condition. If IsElementwise() is true already then we won't need to check if the output is constant as it is not constant for current ops
| if (nextIdx != current + 1) break; | ||
| if (opAssigned[nextIdx]) break; | ||
| if (!fOperators[nextIdx]->IsElementwise()) break; | ||
| if (!fOperators[nextIdx]->IsElementwise() || fOperators[nextIdx]->IsOutputConstant()) break; |
|
|
||
| void EmitOutputCoords(std::string &op, const std::string &indent, |
There was a problem hiding this comment.
better descp for its use case in Tile, Comparison, etc
|
|
||
| UTILITY::SliceInfo UTILITY::ComputeSliceInfo(const std::vector<Dim> & shape, size_t axis) { |
There was a problem hiding this comment.
topK, softmax use case descp as a comment
| op += SP + SP + SP + SP + "std::size_t const col_row = elem_idx / " + std::to_string(colCols) + "u;\n"; | ||
| op += SP + SP + SP + SP + "std::size_t const col_col = elem_idx % " + std::to_string(colCols) + "u;\n\n"; | ||
| op += SP + SP + SP + SP + "std::size_t const col_row = elem_idx / (oDepth * oHeight * oWidth);\n"; | ||
| op += SP + SP + SP + SP + "std::size_t const col_col = elem_idx % (oDepth * oHeight * oWidth);\n\n"; |
There was a problem hiding this comment.
restore to unsigned int
| op += SP + SP + SP + SP + "std::size_t const od = col_col / " + std::to_string(oHeight * oWidth) + "u;\n"; | ||
| op += SP + SP + SP + SP + "std::size_t const oh = (col_col / " + std::to_string(oWidth) + "u) % " + std::to_string(oHeight) + "u;\n"; | ||
| op += SP + SP + SP + SP + "std::size_t const ow = col_col % " + std::to_string(oWidth) + "u;\n\n"; |
| } | ||
|
|
||
| // shape of output tensors given input tensors | ||
| std::vector<std::vector<size_t>> ShapeInference(std::vector<std::vector<size_t>> input) override { |
There was a problem hiding this comment.
base ShapeInference change to Dim based, replacing the DoShapeInference func
| std::string rGC; | ||
| ForEachInferArg_GPU_ALPAKA( | ||
| [&](const std::string &p) { | ||
| if (isdecl) rGC += "size_t "; |
There was a problem hiding this comment.
size_t needs to be reviewed
| SOFIE::OperatorKind::SIGMOID, | ||
| SOFIE::OperatorKind::TANH, | ||
| SOFIE::OperatorKind::SOFTMAX, | ||
| SOFIE::OperatorKind::LEAKYRELU, |
| std::string inputTensor; ///< input tensor name of the first op | ||
| std::string outputTensor; ///< output tensor name of the last op | ||
| size_t numElements = 0; | ||
| std::string lengthExpr; ///< element count: literal for static tensors, runtime expression for dynamic |
Implements #43
Adds dynamic shape support to the GPU (alpaka) code generator: models whose input dimensions are symbolic (
N,n_pf,n_sv) now generate, compile and run, instead ofhaving their shapes baked in at codegen time.The operator set, tests and results are driven by particle-net.onnx, the target dynamic model for this work.
The dynamic buffer fix
Generating a dynamic model on the GPU path failed to compile. The dynamic intermediate tensor buffers were emitted with a type alias built on the runtime device object
devAccwhere a type is needed, declared under the name bufDev_while every operator referencesdeviceBuf_(so the buffer was undefined at use) and allocated into a discardedautolocal in the constructor with a hardcoded float type, so the member was never set. After fixing those, a second error showed up becausealpaka::Buf` has no default constructor, so the bare member declaration would not compile.The fix declares the dynamic buffers as Session members with the correct Buf type and
deviceBuf_name according to the tensor dtype, default initializes each member to a one element placeholder buffer to satisfy the no default constructor requirement and allocates the real buffer in the constructor sized by the runtime length onceNis known.Operator changes:
Extended for dynamic shapes: Tile, Transpose, Concat, Gather, Slice, Reduce, Conv, BatchNormalization, BasicBinary, Comparision, Range. Each keeps a dual
size_t/Dimrepresentation mirroring the ROOT's SOFIE cpu operator with a dynamicInitializebranch that registers the output as a dynamic tensor without materializing it and index math driven by runtime dimensions.BatchNormalization: the scale/variance fusion produces a per-channel
[C]array rather than materializing weights to the full[N,C,...]tensor, which would hardcode in the batch size and block any dynamic shape. Batch and spatial dims are handled by the kernel's index math instead.New GPU kernels for two operators that previously had none:
kstays a compile-time constant so the buffer is a fixed register array, while the axis length is a runtime argument.[32,1024], falling back to 256 for a dynamic axis.Other operators:
range_sizevariable that collided across ops.Max/Min/Sum/Mean): had no GPU codegen at all; the base returned "" silently, so the output was never computed on device. Added a generic elementwise kernel with per-input template types (inputs can have mixed dtypes) and per-input index decomposition for multidirectional broadcast, so differently-shaped inputs are read through their own strides instead of a shared flat index._infer_implargument order fix. The definition interleaves dynamic params with inputs, but the two call sites passed all params first. Those orders coincide when a single input introduces every symbol, so the bug only appears on multi-symbol models.Kernel argument convention
Index math kernels that reference a runtime dimension receive the model shape parameters (for example
N) assize_tkernel arguments, supplied at thecreateTaskKernelcallsite.GetGPUDynParams()computes that parameter list once per operator and is called by both the kernel signature generation and the launch, so the two cannot changeKnown gaps
Skipped deliberately with a comment rather than emitting wrong code:
Rangewith a fully run time size: the length expression dereferences the scalar inputs host-side, which on GPU would need a device to host read.Tests
30 new gtests, each constructed and run at two different sizes and compared against an independent host reference:
Results
140/140 alpaka gtests pass on an NVIDIA H100

Verified end to end on
particle-net.onnx, the dynamic target model: generates with 0 failures, compiles under nvcc and runs.Memory reported by the built-in profiler:
Per-operator timings,
N=1, n_pf=100, n_sv=10, averaged over 100 runs:Conv dominates. The profiler synchronizes after every operator, so its overall figure is not a throughput measurement; the per-op breakdown is the meaningful part.
Note on multi-size sessions
A Session registers its cuBLASLt layouts at construction size, so each test here constructs a fresh Session per size. With ML4EP/sofieBLAS#11 a single Session serves multiple sizes: verified by running one ParticleNet Session at
n_pf/n_svof 100/10, 50/5 and 128/16, each returning a valid softmax.