Skip to content

Commit 767dbff

Browse files
authored
Fix stack() operator-operand composition (#1203)
* Fix stack() operator-operand composition Three bugs that only surface when a stack() expression is materialized via run() with an operator (non-tensor) operand: (A) The stack() factory stored a local `first = pp_get<0>(ts...)` used only to call the static Rank() method, so its value was never read. Under -Werror=unused-but-set-variable this fails to compile when the first operand has a trivial destructor (e.g. any expression operator). (B) StackOp inherited the no-op BaseOp::PreRun/PostRun and never forwarded them to its variadic operands. When an operand is a transform operator that allocates temporary storage during PreRun, that temporary was never allocated or computed, so operator() read uninitialized memory. (C) StackOp::Size() computed Size(dim-1) on an operand whose Size() indexes a fixed-size cuda::std::array. GCC's -Werror=array-bounds analysis cannot prove dim-1 >= 0 (axis_ is a runtime int) and rejects it. The existing Stack test only reads operator() elements directly and never materializes a stack expression, so none of these were triggered. Fixes: (A) Call ::Rank() from a type alias for the type of first. The alias includes a [[maybe_unused]] because Release builds elide the assertion and do not reference the local alias. (B) Add PreRun/PostRun to StackOp that forward to every operand via cuda::std::apply, guarded by is_matx_op<>(). (C) Compute the operand dim once and clamp it to a valid non-negative index so the fixed-size-array access is provably in-bounds. Adds StackOperatorInput test to verify Pre/PostRun forwarding. Signed-off-by: Thomas Benson <tbenson@nvidia.com>
1 parent 0edba71 commit 767dbff

2 files changed

Lines changed: 101 additions & 9 deletions

File tree

include/matx/operators/stack.h

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -332,12 +332,17 @@ namespace matx
332332
{
333333
if(dim==axis_)
334334
return sizeof...(Ts);
335-
else if (dim < axis_) {
336-
return cuda::std::get<0>(ops_).Size(dim);
337-
} else {
338-
// remove axis_ dim from dim.
339-
return cuda::std::get<0>(ops_).Size(dim-1);
335+
// Map the requested output dimension to the operand dimension by dropping
336+
// the stacked axis. For any valid query op_dim is already >= 0 (a dim > axis_
337+
// implies dim >= 1, and the only legitimate dim==0 query is the axis itself,
338+
// handled above). The clamp is therefore unreachable for valid input and
339+
// exists solely to give the compiler a provable lower bound for
340+
// -Werror=array-bounds.
341+
int op_dim = (dim < axis_) ? dim : dim - 1;
342+
if (op_dim < 0) {
343+
op_dim = 0;
340344
}
345+
return cuda::std::get<0>(ops_).Size(op_dim);
341346
}
342347

343348
__MATX_INLINE__ __MATX_HOST__ int32_t DynRank() const {
@@ -353,6 +358,35 @@ namespace matx
353358
else return Rank();
354359
}
355360

361+
// Forward PreRun/PostRun to every stacked operand. An operand may itself
362+
// be a transform operator that allocates and fills temporary storage during
363+
// PreRun, so its PreRun must be invoked before operator() reads from it.
364+
template <typename ShapeType, typename Executor>
365+
__MATX_INLINE__ void PreRun(ShapeType &&shape, Executor &&ex) const noexcept
366+
{
367+
cuda::std::apply([&](const auto&... ops) {
368+
auto prerun_one = [&](const auto &op) {
369+
if constexpr (is_matx_op<cuda::std::decay_t<decltype(op)>>()) {
370+
op.PreRun(shape, ex);
371+
}
372+
};
373+
(prerun_one(ops), ...);
374+
}, ops_);
375+
}
376+
377+
template <typename ShapeType, typename Executor>
378+
__MATX_INLINE__ void PostRun(ShapeType &&shape, Executor &&ex) const noexcept
379+
{
380+
cuda::std::apply([&](const auto&... ops) {
381+
auto postrun_one = [&](const auto &op) {
382+
if constexpr (is_matx_op<cuda::std::decay_t<decltype(op)>>()) {
383+
op.PostRun(shape, ex);
384+
}
385+
};
386+
(postrun_one(ops), ...);
387+
}, ops_);
388+
}
389+
356390
~StackOp() = default;
357391
StackOp(const StackOp &rhs) = default;
358392

@@ -451,9 +485,8 @@ namespace matx
451485
template <typename... Ts>
452486
__MATX_INLINE__ __MATX_HOST__ auto stack(int axis, const Ts&... ts)
453487
{
454-
auto first = detail::pp_get<0>(ts...);
455-
456-
MATX_ASSERT_STR(axis <= first.Rank(),matxInvalidDim, "stack must take an axis less than or equal to the the rank of the operators");
488+
using first_type [[maybe_unused]] = cuda::std::tuple_element_t<0, cuda::std::tuple<Ts...>>;
489+
MATX_ASSERT_STR(axis <= first_type::Rank(),matxInvalidDim, "stack must take an axis less than or equal to the the rank of the operators");
457490
return detail::StackOp<Ts...>{axis, ts...};
458491
}
459492
} // end namespace matx

test/00_operators/stack_test.cu

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "matx.h"
33
#include "test_types.h"
44
#include "utilities.h"
5+
#include "prerun_tester.h"
56

67
using namespace matx;
78
using namespace matx::test;
@@ -47,4 +48,62 @@ TYPED_TEST(OperatorTestsNumericAllExecs, Stack)
4748
}
4849

4950
MATX_EXIT_HANDLER();
50-
}
51+
}
52+
53+
// Verifies that stack() correctly forwards PreRun()/PostRun() to its operands
54+
// when a stack expression is materialized via run(). Each variadic operand is
55+
// wrapped in a PreRunTesterOp lifecycle probe: stack()'s PreRun/PostRun fold
56+
// must forward to every operand exactly once (an unforwarded operand leaves
57+
// prerun_count == 0). The probe is a transparent pass-through, so the
58+
// materialized result is still the correct stacked output and is checked against
59+
// a reference. The cumsum() operands are real transforms whose temporaries are
60+
// only allocated/filled if PreRun is forwarded.
61+
TYPED_TEST(OperatorTestsFloatNonComplexNonHalfAllExecsWithoutJIT, StackOperatorInput)
62+
{
63+
MATX_ENTER_HANDLER();
64+
using TestType = cuda::std::tuple_element_t<0, TypeParam>;
65+
using ExecType = cuda::std::tuple_element_t<1, TypeParam>;
66+
67+
ExecType exec{};
68+
69+
auto a = make_tensor<TestType>({5});
70+
auto b = make_tensor<TestType>({5});
71+
auto c = make_tensor<TestType>({5});
72+
a.SetVals({1, 2, 3, 4, 5}); // cumsum(a) = {1, 3, 6, 10, 15}
73+
b.SetVals({10, 20, 30, 40, 50}); // leaf operand
74+
c.SetVals({2, 4, 6, 8, 10}); // cumsum(c) = {2, 6, 12, 20, 30}
75+
76+
// Reference: materialize the transform operands into tensors first.
77+
auto ca = make_tensor<TestType>({5});
78+
auto cc = make_tensor<TestType>({5});
79+
(ca = cumsum(a)).run(exec);
80+
(cc = cumsum(c)).run(exec);
81+
auto out_ref = make_tensor<TestType>({3, 5});
82+
(out_ref = stack(0, ca, b, cc)).run(exec);
83+
84+
// Under test: wrap each stacked operand in its own lifecycle probe. A mix of
85+
// two transform operands and a leaf exercises the variadic fold over more than
86+
// two operands.
87+
PreRunLifecycle s0, s1, s2;
88+
auto out_test = make_tensor<TestType>({3, 5});
89+
(out_test = stack(0,
90+
make_prerun_tester(cumsum(a), s0),
91+
make_prerun_tester(b, s1),
92+
make_prerun_tester(cumsum(c), s2))).run(exec);
93+
94+
exec.sync();
95+
96+
// Correctness preserved (probe is a transparent pass-through).
97+
for (int i = 0; i < 3; i++) {
98+
for (int j = 0; j < 5; j++) {
99+
ASSERT_EQ(out_test(i, j), out_ref(i, j)) << "mismatch at (" << i << "," << j << ")";
100+
}
101+
}
102+
103+
// Lifecycle: stack() forwarded a balanced PreRun/PostRun to every operand.
104+
ExpectLifecycleClean(s0, "cumsum(a)");
105+
ExpectLifecycleClean(s1, "b");
106+
ExpectLifecycleClean(s2, "cumsum(c)");
107+
108+
MATX_EXIT_HANDLER();
109+
}

0 commit comments

Comments
 (0)