Skip to content

make_buffer_copy leaves an unfoldable gap after skipped extent-1 dimensions #9563

Open

Description

@derek-gerstmann

Description

make_buffer_copy leaves an unfoldable gap after skipped extent-1 dimensions ... this breaks WebGPU 8/16-bit copies

Environment: Halide 22.0.0.dev444 (pip wheel), WebGPU backend via Dawn 74c5edcc (ABI-matched to mini_webgpu.h), macOS 15 arm64 (Dawn on Metal). The bug is in shared runtime code and affects every GPU backend; it is only incorrect on WebGPU (elsewhere it is a performance bug).

Summary

make_buffer_copy() in src/runtime/device_buffer_utils.h inserts non-trivial dimensions into the device_copy sorted by destination stride, but bounds the insertion search by the source dimension index i instead of by the number of dimensions inserted so far. When an extent-1 dimension is skipped before a later dimension, the later dimension is inserted after an unused slot (extent = 1, stride = 0). That slot sits at position 0, so the fold loop (chunk_size == src_stride_bytes[0]) never fires and a dense buffer is copied one element (or one row) at a time instead of as a single chunk.

Root cause

for (int i = 0; i < dst->dimensions; i++) {
    if (dst->dim[i].extent == 1) continue;           // skipped -> slot i stays {1, 0, 0}
    ...
    for (insert = 0; insert < i; insert++) {          // BUG: should be < number already inserted
        if (dst_stride_bytes < c.dst_stride_bytes[insert] && dst_stride_bytes != 0) break;
    }
    // empty slots have stride 0, so the comparison is false and insert == i

Example: a width-1 single-channel image {x: extent 1, stride 1}, {y: extent H, stride 1}. Dim 0 is skipped, dim 1 is inserted at slot 1, slot 0 is {extent 1, stride 0}. chunk_size (1 byte) != src_stride_bytes[0] (0), so nothing folds and the copy issues H separate 1-byte copies. Any dense buffer whose extent-1 dimension precedes a non-trivial one (e.g. a single-row {x: W}, {y: 1}, {c: 3} planar buffer) hits the same path.

Why it is a correctness bug on WebGPU

WebGPU requires buffer-to-buffer copy offsets and sizes to be multiples of 4 bytes. The WebGPU runtime rounds the copy size up to 4, so each per-element copy of a u8/s8/u16/s16 value either fails validation or writes the neighbouring 1–3 bytes, corrupting the output. A single folded chunk would be aligned (or at least only have one tail).

Separately, the WebGPU backend should handle genuinely non-dense 8/16-bit copies (e.g. crops) by staging through an aligned span rather than issuing unaligned per-element copies.

Reproducing case

## Reproducer
Any pipeline on the `webgpu` target with an 8- or 16-bit output, copied back with `copy_to_host()`/`halide_copy_to_host`, where the buffer has an extent-1 dimension that precedes a non-trivial one, e.g. a `Buffer<uint8_t>(1, 64)` (width 1) or a planar `Buffer<uint8_t>(64, 1, 3)` (one row). Output bytes differ from the CPU target / Dawn reports `Copy size ... is not a multiple of 4`.

## Expected vs actual
- Expected: dense buffer is copied as one contiguous chunk regardless of extent-1 dimensions.
- Actual: one copy per element/row; on WebGPU, misaligned copies that fail or corrupt adjacent bytes.

How did you get Halide?

pip (halide)

Halide version

22.0.0.dev444

Halide commit (if known)

No response

Target

arm-64-macos-arm_dot_prod-arm_fp16-webgpu

Operating system

MacOS Tahoe 26.6.2

Additional context

Suggested fix

Track the number of inserted dimensions and bound the search by it:

int inserted = 0;
for (int i = 0; i < dst->dimensions; i++) {
    if (dst->dim[i].extent == 1) continue;
    ...
    int insert;
    for (insert = 0; insert < inserted; insert++) { ... }
    for (int j = inserted; j > insert; j--) { /* shift */ }
    ... ; inserted++;
}

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions