Skip to content

GPU compilation cache incorrectly handles tombstoned entries #9564

Open

Description

@derek-gerstmann

Description

Summary

GPUCompilationCache::release_context_already_locked() in src/runtime/gpu_context_common.h selects entries with kernel_id > kInvalidId. Deleted (tombstoned) entries have kernel_id == kDeletedId == 1, which also satisfies that test. Their module_state was already set to nullptr, so the cache calls the free function with nullptr and decrements count a second time for the same slot. For WebGPU, this crashes Dawn.

Environment: Halide 22.0.0.dev444 (pip wheel), WebGPU backend via Dawn 74c5edcc, macOS 15 arm64. The bug is in shared GPUCompilationCache code; whether it crashes depends on the backend's free function.

Root cause

static constexpr uintptr_t kInvalidId{0};
static constexpr uintptr_t kDeletedId{1};
...
if (compilations[i].kernel_id > kInvalidId &&            // true for kDeletedId too
    (all || compilations[i].context == context) &&
    compilations[i].use_count == 0) {
    f(compilations[i].module_state);                       // f(nullptr) for tombstones
    compilations[i].module_state = nullptr;
    compilations[i].kernel_id = kDeletedId;
    count--;                                               // count corrupted
}

A tombstone is created by an earlier release (e.g. halide_webgpu_device_release -> delete_context). If any live entry keeps count > 0, the table is not freed, and the next release_context/release_all (for WebGPU, the halide_webgpu_cleanup destructor calling shader_cache.release_all(nullptr, wgpuShaderModuleRelease)) revisits the tombstone.

Impact

  • WebGPU: wgpuShaderModuleRelease(nullptr) dereferences null inside Dawn, so the process segfaults at exit after the work has completed successfully (test harnesses report a crash).
  • All backends: count underflows/drifts, so release_all may skip free(compilations) or later free logic misbehaves. Backends whose free function tolerates null (e.g. cuModuleUnload returns an error) silently hide it.

Reproducing case

## Reproducer
Run two or more distinct WebGPU kernels in one process, release the device (`halide_device_release` / `halide_webgpu_device_release`) while another kernel's compilation is cached under a different context or still has `use_count > 0`, then exit. Observed reliably with a test binary that runs many channel-conversion kernels in sequence: all checks pass, then SIGSEGV in `wgpuShaderModuleRelease` from `halide_webgpu_cleanup`.

## Expected vs actual
- Expected: each cached module is released exactly once; tombstones are skipped.
- Actual: tombstones are "released" again with `nullptr`; Dawn crashes; `count` is wrong.

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

Skip tombstones explicitly:

if (compilations[i].kernel_id > kDeletedId && ...)

(and/or guard if (compilations[i].module_state != nullptr) before calling f). A null check in the WebGPU free path would also be prudent.

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