Handle non-trivially-copyable types in Loop/Scan output concatenation - #29397
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes CPU Loop scan-output concatenation for non-trivially-copyable element types (notably std::string) by avoiding byte-wise copying/zeroing that can corrupt string objects, and adds unit tests to validate correct behavior for string scan outputs and loop-carried variables.
Changes:
- Update Loop CPU scan-output concatenation to use
std::copyfortensor(string)outputs instead of byte-wise copying. - Update Scan
OutputIterator::ZeroOutCurrentto skip zeroing for string tensors (sincememsetis unsafe forstd::string). - Add Loop CPU provider tests covering string scan outputs (single/multi-element), loop-carried strings, and zero-iteration behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| onnxruntime/core/providers/cpu/controlflow/loop.cc | Use proper element-wise copy semantics when concatenating string scan outputs. |
| onnxruntime/core/providers/cpu/controlflow/scan_utils.h | Avoid memset-style zeroing for string tensors in Scan output iteration. |
| onnxruntime/test/providers/cpu/controlflow/loop_test.cc | Add unit tests validating Loop behavior with string scan outputs and loop-carried strings (including zero-iteration cases). |
ConcatenateCpuOutput byte-copied tensor data via gsl::copy over a span<const std::byte> cast, which is incorrect for std::string elements (not trivially copyable). Add an IsDataTypeString() branch that uses DataAsSpan and std::copy with proper copy-assignment semantics. Similarly, OutputIterator::ZeroOutCurrent used memset(0) which would corrupt std::string objects. Since the strings are already default- constructed (empty) from placement-new, simply return early. Add tests exercising string scan outputs (single and multi-element), string loop-carried variables, and zero-iteration edge case.
62c7529 to
4cadf9d
Compare
tianleiwu
left a comment
There was a problem hiding this comment.
Review Summary
The change correctly fixes a real correctness bug: Loop/Scan output concatenation previously byte-copied tensor payloads unconditionally, which is undefined behavior for non-trivially-copyable element types like std::string (it would copy raw pointers/SSO bytes and lead to double-frees or corruption). Routing string tensors through std::copy over DataAsSpan<std::string>() and skipping the memset-based ZeroOutCurrent for strings are both correct.
Strengths
- Correct handling of heap-allocated string payloads (SSO-exceeding strings are explicitly exercised in tests).
output_spanis now constructed once outside the loop for the non-string path, which also resolves the earlier reviewer note about per-iteration span re-creation.- The
ZeroOutCurrentskip is safe because string tensor buffers are placement-new default-constructed to empty strings; the comment documents this clearly. - Strong test coverage: single/multi-element string scan outputs, loop-carried string variable, and the zero-iteration empty-output edge case.
Non-blocking suggestions (see inline)
- Consider
std::moveinstead ofstd::copyfor the per-iteration string payloads, sinceper_iteration_outputis discarded after concatenation.
Overall this looks good to merge. Only minor, optional nits below.
per_iteration_output is discarded after concatenation, so use std::move to transfer string payloads without re-allocation. Hoist Shape().Size() as elements_per_iteration above the loop.
…#29397) This pull request improves the handling of string tensors in the CPU implementation of the Loop operator, ensuring correct memory management and copy semantics for non-trivially-copyable types like `std::string`. It also adds comprehensive unit tests to verify these behaviors, especially for cases involving string scan outputs and loop-carried variables. **Enhancements for string tensor support:** * Updated `ConcatenateCpuOutput` in `loop.cc` to properly detect string tensors and use `std::copy` for concatenation, ensuring correct handling of heap-allocated string payloads and avoiding unsafe byte-wise copying. [[1]](diffhunk://#diff-2c8478657254a53c4ce09684960c925593395336e00c43ef672d9427722e3ff7R276-L282) [[2]](diffhunk://#diff-2c8478657254a53c4ce09684960c925593395336e00c43ef672d9427722e3ff7R289-R304) * Modified `OutputIterator::ZeroOutCurrent` in `scan_utils.h` to skip zeroing for string tensors, as their elements are already default-constructed and cannot be safely set with `memset`. **New and extended unit tests for string tensor scenarios:** * Added tests to `loop_test.cc` covering: - String scan outputs, including long strings that exceed the small-string-optimization threshold and multi-element outputs. - Loop-carried string variables to ensure they are copied correctly. - Zero-iteration cases to confirm empty string scan outputs are handled without errors.
This pull request improves the handling of string tensors in the CPU implementation of the Loop operator, ensuring correct memory management and copy semantics for non-trivially-copyable types like
std::string. It also adds comprehensive unit tests to verify these behaviors, especially for cases involving string scan outputs and loop-carried variables.Enhancements for string tensor support:
ConcatenateCpuOutputinloop.ccto properly detect string tensors and usestd::copyfor concatenation, ensuring correct handling of heap-allocated string payloads and avoiding unsafe byte-wise copying. [1] [2]OutputIterator::ZeroOutCurrentinscan_utils.hto skip zeroing for string tensors, as their elements are already default-constructed and cannot be safely set withmemset.New and extended unit tests for string tensor scenarios:
loop_test.cccovering: