Skip to content

Initial dynamic input support for gpu codegen - #45

Open
harz05 wants to merge 26 commits into
ML4EP:gpu/alpakafrom
harz05:feat/dynamic-input-alpaka
Open

Initial dynamic input support for gpu codegen#45
harz05 wants to merge 26 commits into
ML4EP:gpu/alpakafrom
harz05:feat/dynamic-input-alpaka

Conversation

@harz05

@harz05 harz05 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

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 devAcc where 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 once N is known.

Operator changes:

Extended for dynamic shapes: Tile, Transpose, Concat, Gather, Slice, Reduce, Conv, BatchNormalization, BasicBinary, Comparision, Range. Each keeps a dual size_t/Dim representation mirroring the ROOT's SOFIE cpu operator with a dynamic Initialize branch 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:

  • TopK": one thread per slice with a K-sized insertion-sorted register buffer; k stays a compile-time constant so the buffer is a fixed register array, while the axis length is a runtime argument.
  • Softmax: block-per-row online softmax (a single fused max/sum pass, then a shared-memory tree reduction). Threads per row is the axis length rounded up to a power of two, clamped to [32,1024], falling back to 256 for a dynamic axis.

Other operators:

  • Range: output length derived symbolically from a shape tensor instead of an opaque range_size variable that collided across ops.
  • Gather: a Gather indexing a shape tensor now produces a shape tensor whose value is written host-side and copied to the device buffer, so a shape tensor consumed by a compute kernel has its value available on device.
  • BasicNary (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.
  • RModel_ALPAKA shape tensor declarations, and an _infer_impl argument 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) as size_t kernel arguments, supplied at the createTaskKernel callsite. GetGPUDynParams() computes that parameter list once per operator and is called by both the kernel signature generation and the launch, so the two cannot change

Known gaps

Skipped deliberately with a comment rather than emitting wrong code:

  • Range with 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:

area tests
shape / indexing Transpose, Concat, Tile, Gather, Slice, Range, RangeMul
reduce ReduceSumLast, ReduceMeanMid, ReduceMaxFirst, ReduceSumMulti
conv Conv1D, Conv1DNoBias, Conv2DNoBias
normalization BatchNorm4D, BatchNormDynSpatialRelu, BatchNorm2D
elementwise AddBroadcast, Equal, NegRelu
linear Linear (Gemm+Relu fusion)
new operators TopK, Softmax 1D/2D/3D/4D
BasicNary broadcast Max/Min/Mean/SumMultidirectionalBroadcast

Results

140/140 alpaka gtests pass on an NVIDIA H100
image

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:

image

Per-operator timings, N=1, n_pf=100, n_sv=10, averaged over 100 runs:

operator class count total
Conv 15 ~594 us
BatchNormalization 17 ~182 us
TopK 3 ~153 us
ReduceMean 3 ~149 us
ReduceSum 6 ~65 us
MatMul 3 ~53 us
Gemm 2 ~26 us

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_sv of 100/10, 50/5 and 128/16, each returning a valid softmax.

@harz05
harz05 force-pushed the feat/dynamic-input-alpaka branch from 2946253 to 78eb28e Compare July 5, 2026 17:38
@harz05
harz05 force-pushed the feat/dynamic-input-alpaka branch from 01ddd4c to e8623a1 Compare July 8, 2026 16:15
@harz05
harz05 marked this pull request as draft July 10, 2026 18:10
@harz05
harz05 marked this pull request as ready for review July 18, 2026 09:30
@sanjibansg

Copy link
Copy Markdown
Member

/runtest h100

@github-actions

Copy link
Copy Markdown

/runtest (h100): triggered - view run

@sanjibansg

Copy link
Copy Markdown
Member

/runtest h100-47gb

@github-actions

Copy link
Copy Markdown

/runtest (h100-47gb): triggered - view run

@github-actions

Copy link
Copy Markdown

/runtest (h100-47gb): GPU Unit Tests ✅ passed - view run

@sanjibansg sanjibansg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {}; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should keep the blas routines here anyway, since they might be needed for the cpu inference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a bit more descriptive comment here, i.e. signalling where is this computed and for what purpose.

Comment thread core/inc/SOFIE/ROperator_Reduce.hxx Outdated
}
// find shape of Y and add it in the list of intermediate tensors
fShapeY = ShapeInference({fShapeX})[0];
fShapeY = DoShapeInference(fShapeX);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of a differently named method, can we have an overloaded one?

Comment thread core/inc/SOFIE/ROperator_Softmax.hxx Outdated
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";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gpu/alpaka now has SOFIE_DEVICE_exp macro, so would it be better to use that here instead of simple exp??

Comment thread core/inc/SOFIE/ROperator_Softmax.hxx Outdated
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";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
op += SP + SP + SP + SP + "Y[idx] = alpaka::math::log(acc, e);\n";
op += SP + SP + SP + SP + "Y[idx] = log(acc, e);\n";

Comment thread core/inc/SOFIE/ROperator_Softmax.hxx Outdated
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";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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";

Comment thread core/inc/SOFIE/ROperator_Softmax.hxx Outdated
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";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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";

Comment thread core/inc/SOFIE/ROperator_TopK.hxx Outdated
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we instead create a task kernel and enqueue it?

Comment thread core/inc/SOFIE/ROperator_TopK.hxx Outdated
// 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if cases are critical in GPUs, can we have a workaround here to avoid branching?

// 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()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as line 93

Comment thread core/src/SOFIE_common.cxx
Comment on lines +573 to +574

void EmitOutputCoords(std::string &op, const std::string &indent,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better descp for its use case in Tile, Comparison, etc

Comment thread core/src/SOFIE_common.cxx
Comment on lines 558 to +559

UTILITY::SliceInfo UTILITY::ComputeSliceInfo(const std::vector<Dim> & shape, size_t axis) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

topK, softmax use case descp as a comment

Comment on lines -713 to +723
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";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restore to unsigned int

Comment on lines -733 to -735
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";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as L713

}

// shape of output tensors given input tensors
std::vector<std::vector<size_t>> ShapeInference(std::vector<std::vector<size_t>> input) override {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ";

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

size_t needs to be reviewed

SOFIE::OperatorKind::SIGMOID,
SOFIE::OperatorKind::TANH,
SOFIE::OperatorKind::SOFTMAX,
SOFIE::OperatorKind::LEAKYRELU,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check once

Comment thread core/inc/SOFIE/RModel.hxx
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

review once

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants