Skip to content

Full-system freeze / forced restart during a render session (GPU wedge suspected, not confirmed) #4

Description

@thetalkingdrum

Summary

On 2026-08-15 around 23:18–23:35 local time, NegPy (packaged .app, v0.51.0) was open and being used to compare two loaded negatives. The whole machine — not just NegPy — stopped responding to any input and had to be recovered with a forced hardware restart (power button held). Confirmed by a macOS kernel panic log with panic string btn_rst (the panic macOS logs specifically for a user-forced restart), timestamped 9 seconds before the next NegPy session booted.

This issue is the evidence trail from reconstructing what happened, for whoever picks this up — I'm not equipped to dig further into the WebGPU/Metal side myself.

Environment

  • NegPy 0.51.0, packaged /Applications/NegPy.app (ad-hoc signed), not a dev run
  • macOS 15.7.9 (24G830)
  • MacBook Pro 18,1 (M1 Pro, 16 GB unified memory)
  • GPU backend: Apple M1 Pro (IntegratedGPU) via Metal (rendering.backend = "metal" in override.toml)
  • override.toml had [performance] cpu_parallel = true — see "Ruled out" below

Timeline (from ~/Documents/NegPy/negpy.log and macOS's unified log, cross-referenced by PID)

Time Event
23:18:23 New NegPy instance launches (app<...>:PID 8155). CPU parallel kernels: enabled (override=True, platform=darwin)
23:18:32–23:18:36 Loads/renders two files fine: c2025-04-13-0002.tif and Kodak Gold 200_Frame028_R_linear.tiff (last successful first_render logged at 23:18:36)
23:18:36 Last line NegPy's Python logger ever writes for this session. No exception, no traceback, nothing.
23:18:36 – 23:33:55 negpy.log: total silence. macOS's runningboardd, however, keeps reporting PID 8155 as running-active the whole time — the process is alive at the OS level, just producing no application-level activity. Last such state update: 23:33:55.610.
23:25:00 – 23:34:59 System-wide unified log stays busy the entire time — thousands of lines/minute from kernel, WiFi, Bluetooth, mDNSResponder, etc. (checked explicitly to rule out a full kernel lockup — see "Ruled out" below).
23:35:14 Kernel panic, panic(cpu 1 caller ...): btn_rst — logged to /Library/Logs/DiagnosticReports/forceReset-full-2026-08-15-233514.0002.diag. This is macOS's panic string for a user-forced hardware restart (power button held), not an automatic fault/watchdog panic.
23:35:18 New NegPy launch request (likely auto-relaunch via macOS's "reopen windows on login" / PersistentAppsSupport, seen in loginwindow logs)
23:35:23 Fresh NegPy session boots, negpy.log resumes
23:35:57 WARNING desktop.main: CPU parallel kernels disabled after an unclean shutdown — the app's own self-check (main.py:_offer_to_disable_cpu_parallel) correctly detected the prior session never called clean_shutdown, and the user accepted the dialog offering to turn cpu_parallel off

No .ips/.crash report for NegPy itself exists anywhere in ~/Library/Logs/DiagnosticReports or /Library/Logs/DiagnosticReports for this window — only the system-level forceReset-full / ResetCounter pair from the forced restart.

What was loaded

The file most likely in view when things went silent: Kodak Gold 200_Frame028_R_linear.tiff, 216 MB, 7968×5320, 16-bit RGB, Adobe-deflate compressed, unusually small strips (RowsPerStrip=5, 1064 strips), source SONY ILCE-7RM2. It had loaded successfully many times before and after this incident (both in this same session and the recovery session right after), so the file by itself isn't reliably reproducing a hang — something about the sequence of actions around 23:18:36 is implicated, not just "open this file."

Ruled out: Numba CPU-parallel abort

negpy/kernel/system/parallel.py documents a known failure mode: Numba's workqueue threading layer hard-aborts the process with no Python exception and nothing written to the log if two threads enter a parallel=True kernel concurrently. override.toml had cpu_parallel = true forced on macOS, despite the code's own comment that this is unverified there ("Defaults to true except on macOS, where it defaults to false while crash reports are investigated"). The silent-log symptom initially looked like a strong match.

Ruled out because:

  • That abort kills a single process almost immediately — it would not explain 15 minutes of the process still showing as running-active to runningboardd, and would not require a hardware forced restart to recover from.
  • The system-wide unified log (kernel, WiFi, Bluetooth, daemons) kept logging normally through the entire silent window, which is inconsistent with a process-fatal abort but consistent with "one process's screen output froze while the rest of the OS kept running."

I still turned cpu_parallel off in override.toml (real, separate, documented risk per the module's own comments), but it's very unlikely to be the cause of this incident.

Leading theory: GPU/display-pipeline wedge (unconfirmed)

The pattern — one process's window stops updating/responding while background kernel and daemon activity continues normally, ending in a state only a forced hardware restart recovers from, with no automatic GPU-watchdog panic ever firing on its own — matches a wedged Metal/AGX GPU command queue. macOS's WindowServer shares the GPU with every app's Metal/WebGPU submissions; if one app's GPU work jams that shared queue, the whole screen can lock solid (no cursor movement, Force Quit unreachable) while unrelated kernel subsystems (which don't touch the GPU) keep running fine underneath — exactly what the per-minute unified-log activity shows.

NegPy's GPUEngine (negpy/services/rendering/gpu_engine.py, WGSL via wgpu) was the last thing doing GPU work before the silence. Candidate contributing factors, none confirmed:

  • The image is ~127 megapixels; the pipeline runs a dozen-plus GPU compute stages per render, each potentially allocating a full-resolution intermediate buffer.
  • The machine has 16 GB unified memory, shared between CPU and GPU — a large enough working set across pipeline stages could hit real memory pressure, which is one known trigger class for Apple Silicon GPU driver wedges.
  • The file's unusual strip layout (RowsPerStrip=5, 1064 strips) is not obviously GPU-relevant (that's a CPU-side TIFF decode detail), but is flagged here in case it correlates with something on the decode → upload path.

No automatic GPU-watchdog panic preceded the forced one, so macOS's own driver-level recovery never engaged — meaning either the wedge was too deep for that mechanism, or the freeze wasn't purely a self-contained GPU-driver bug at all. I don't have a way to distinguish those from the outside.

Open questions for whoever investigates next

  1. Can this be reproduced deliberately with a large (>100 MP) source and heavy GPU-stage usage (e.g. defect repair / textural analysis, which the log shows was active — textural=True — on this file shortly before other loads that session)?
  2. Does GPUEngine's per-stage buffer lifecycle release intermediate textures between stages, or could VRAM/unified-memory usage accumulate across repeated loads of a huge file within one session?
  3. Is there any bound today on max_texture_size for a file this large, or does "auto" let a 7968×5320 image go through uncapped?
  4. Worth adding a client-side watchdog (e.g. a heartbeat timer that detects the GPU submission queue not returning within N seconds) so a wedge surfaces as a recoverable error dialog instead of a silent full-system freeze?

Evidence

Everything above was reconstructed from:

  • ~/Documents/NegPy/negpy.log (session logging, self-diagnostic messages)
  • ~/Documents/NegPy/override.toml (the cpu_parallel = true setting)
  • macOS unified log (log show), filtered by PID and by system-wide per-minute activity
  • /Library/Logs/DiagnosticReports/forceReset-full-2026-08-15-233514.0002.diag and the paired ResetCounter-2026-08-15-233521.diag
  • tifffile inspection of the source TIFF

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions