Skip to content

Export: stream frames through a shared CPU/GPU work queue, convert to YUV 4:2:0 on the GPU, and tune to the hardware - #898

Closed
MatejKozlovsky wants to merge 10 commits into
storytold:mainfrom
MatejKozlovsky:gpu-export-scheduler-standalone
Closed

MatejKozlovsky wants to merge 10 commits into
storytold:mainfrom
MatejKozlovsky:gpu-export-scheduler-standalone

Conversation

@MatejKozlovsky

@MatejKozlovsky MatejKozlovsky commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Closes #897. This branch merges cleanly onto current main and does not depend on #688. It works with or without #688; with #688 (GPU-resident effect stacks, texture reuse) it gets most of the 4K gain measured below. GpuContext::set_upload_budget (default unchanged) is also added by #688. Its code here (the upload_budget field and initialiser, set_upload_budget/upload_budget, and the budget check and one-time "upload cache full" warning in upload_buf) is written line for line as in #688, so git applies it once in either merge order. Merging #688 into this branch, or this branch into #688, both on top of main, gives the same tree with no conflicts and no duplicate field.

On the test PC (RTX 3090), a 3 s 4K comp exports with --gpu in 49.7 s instead of 201.9 s on main (median of 3 runs each).

What this PR does

  • Streaming scheduler (crates/export/src/stream.rs, pipeline.rs). Main sends a batch of up to 16 frames to the GPU at once. This PR has gpu-frames GPU workers and adaptive CPU workers claim frames from a shared queue, and a bounded reorder buffer feeds the in-order encoder. A CPU worker is added only while measured throughput improves and never when a CPU frame is slow enough to stall the encoder. Every GPU decline is logged with its reason.
  • GPU YUV 4:2:0 and async readback (crates/gpu/src/yuv.rs, shaders/yuv.wgsl). A compute kernel does the export's to_rgba8_over and rgba_to_yuv420, so a 4K frame reads back 12 MB instead of 133 MB of RGBA f32. The copy goes through the context's bounded readbacks and an 8-buffer staging ring, and the GPU worker records the next frame while it runs. H.264 only; other formats take RGBA as before.
  • Hardware-adaptive tuning (crates/export/src/tuning.rs, hw.rs, TUNING.md). Each option resolves independently: CLI, then EFFECTCRAFT_EXPORT_<OPTION>, then a --tuning-config file, then auto. Auto reads VRAM, vendor, unified memory, core kinds and RAM, and render --print-tuning prints every value with its source and reason. The tuned GPU path is automatic only on NVIDIA discrete and Apple GPUs. AMD, Intel, integrated and software adapters keep the stock path unless the user opts in with --backend gpu or individual options. VRAM and RAM are detected without unsafe (nvidia-smi, CIM on Windows, sysfs/procfs on Linux, sysctl on macOS). When a verified GPU's VRAM is not reported, auto keeps the stock path. No hardware encoder.
  • Layer cache size. Main's export layer cache is LayerCache::default(), 1 GiB. This PR sets it to 15% of RAM, at most 4096 MB (cache-mb). This changes CPU-only exports too; see "Why CPU-only 4K is faster".
  • Out-of-memory recovery (crates/gpu/src/readback.rs, lib.rs, stream.rs). wgpu reports a failed allocation to the device's uncaptured-error handler. While an export frame records, that error now declines only that frame (reason out of memory) instead of retiring the device. The GPU worker waits for its frames in flight, keeps one fewer in flight (at least 1, logged once) and retries on the GPU. A frame that fails again renders on the CPU. Outside an export frame, for example in the viewer or the app's preview, any GPU error still retires the context as on main.
  • VRAM plan. The per-frame estimate is 1,332 B per output pixel plus two RGBA f32 frames per further frame in flight, calibrated from peaks measured without GPU: resident effect stacks, in-frame texture reuse, upload-cache budget, stable footprint keys (#687) #688. When one frame does not fit, auto picks the CPU up front and logs one warning that names GPU: resident effect stacks, in-frame texture reuse, upload-cache budget, stable footprint keys (#687) #688. GPU: resident effect stacks, in-frame texture reuse, upload-cache budget, stable footprint keys (#687) #688 cuts the working set (the M4 rendered 4K on the GPU with GPU: resident effect stacks, in-frame texture reuse, upload-cache budget, stable footprint keys (#687) #688 stacked), so lower this estimate once GPU: resident effect stacks, in-frame texture reuse, upload-cache budget, stable footprint keys (#687) #688 lands.
  • Accelerator hooks (crates/render). Defaulted trait methods (adapter_info, set_memory_budgets, comp_frame_yuv420, poll_readbacks) and gpu_flags, so the GPU crate reads no environment.

There are four commits, one change each: render hooks, GPU YUV and readback, export scheduler and tuning, CLI flags. The PR exposes no inert options. #688's texture-reuse and resident-fx switches are left out because without #688 they control nothing. They come back as a small follow-up after #688: two GpuFlags fields, two Overrides/ExportTuning fields with their rules and --print-tuning rows (next to precomp-resident), two TUNING.md rows, two CLI flags and #688's two checks in crates/gpu.

Behaviour changes outside --gpu

  • CPU-only exports produce the same bytes as main in every format tested (H.264, ProRes, GIF, PNG on the test PC). They can be faster because of the larger layer cache, and use up to 3 GB more RAM for it.
  • With --gpu and an audio track, the streamed H.264 path muxes audio after every frame instead of after every batch. The MP4 differs in layout from a CPU export; the video and the decoded audio are identical (tests/scheduler.rs).

Tests

  • yuv::tests::yuv420_matches_cpu: the GPU 4:2:0 conversion is byte-identical to the CPU path.
  • stream::tests: in-order output with out-of-order workers, the buffer bound, CPU-worker adaptation.
  • tests/scheduler.rs: with a mock accelerator that renders or declines every frame, a scheduled export matches the CPU export byte for byte in H.264, ProRes, WebM, GIF, PNG and EXR without audio, and in video and decoded audio with an audio track.
  • tuning::tests: precedence, vendor gating, unified memory, the RAM cap, and the calibrated estimate (8 GB at 4K picks the CPU with the warning, 24 GB at 4K plans 3 in flight, 8 GB at 1080p plans 3).
  • readback::oom_tests: a failed allocation and the validation errors it causes decline the frame without retiring the device; a validation error on its own still retires it; outside an export frame every error retires it. stream::inflight_tests: out of memory lowers frames in flight once, never below 1, and retries on the GPU.
  • On the test PC (Windows, RTX 3090): cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --release -p effectcraft-export -p effectcraft-gpu -p effectcraft-render (GPU tests on real hardware), effectcraft-ui-egui --test ui_live_preview and cargo xtask wasm pass. tests_auto::auto_backend_keeps_fast_accelerators (main code, not touched here) failed once under full-suite load and passed 3 of 3 alone.

Measured

All runs export the same comp (Bench60F, 3 s, 90 frames, H.264) with no RAYON_NUM_THREADS override. 1080p is the same comp at --resolution half. GPU utilisation and VRAM come from nvidia-smi every 250 ms and include other processes. "Output vs CPU" compares with main's CPU render.

Test PC: Ryzen 7 5800X (16 threads), RTX 3090 24 GB, 64 GB RAM, Windows 10

Median of 3 runs per row, range in brackets. Runs were alternated between main and this PR. The third round used builds on 67434e9 (main moved by a CI-only change); its outputs are MD5-identical to the earlier rounds.

run 4K wall 1080p wall GPU util avg/max (4K / 1080p) VRAM peak (4K / 1080p) CPU total avg (4K) frames on GPU (4K) output vs CPU (4K / 1080p)
main CPU 171.9 s (171.6-175.2) 28.2 s n/a n/a 99% 0 reference
main --gpu 201.9 s (198.3-202.9) 30.1 s 9%/90% / 9%/100% 23,825 / 23,825 MiB 97% 0 (silent fallback) MD5-identical / PSNR 50.0 dB
this PR --gpu 49.7 s (49.5-49.8) 10.0 s (9.8-10.2) 51%/100% / 38%/68% 17,300 / 9,750 MiB 26% 86 of 90, 0 declined PSNR 51.0 dB / 49.3 dB

The 1080p main rows are from an earlier session (same binary).

  • At 4K this PR is about 4x faster than main --gpu and 3.5x faster than main on the CPU. Main --gpu renders every 4K frame on the CPU without logging it: its output is MD5-identical to the CPU render.
  • The 4K bound is the GPU (utilisation reaches 100%; 4 frames render on the CPU compositor because the GPU path does not handle them). The adaptive scheduler tried one CPU worker, measured 2.19 fps against 2.31 fps without it, and dropped it.
  • GPU outputs differ from the CPU render because the frames are rendered on the GPU (PSNR about 51 dB at 4K, 49 dB at 1080p). The 1080p output is MD5-identical across runs (1F79371D0DE8F77B7CBFD061EA0E911F), and so is the 4K output.
  • With GPU: resident effect stacks, in-frame texture reuse, upload-cache budget, stable footprint keys (#687) #688 stacked, an earlier build of this series took 9.7 s at 4K and 4.2 s at 1080p (one run each).

Why CPU-only 4K is faster

On the test PC, CPU-only 4K took 171.9 s on main (171.6-175.2) and 117.8 s with this PR (117.3-118.7), with MD5-identical output. With --cache-mb 1024, main's cache size, this PR took 173.1 s (172.4-173.2; 3 runs, alternated with the others). The whole gain is the layer cache: at 4K a cached layer buffer is about 133 MB, so 1 GiB holds about 7 and 4 GB about 30, and static layers stop re-rendering. The scheduler is not involved; a CPU-only export keeps main's parallel batches (16 frames on this machine). The laptop shows the same effect (466.7 s on main, 318.6 s with this PR; one run each).

Test laptop: i7-7700HQ (4C/8T), GTX 1070 8 GB, 32 GB RAM, Windows 10, NVIDIA 582.78

One run per row.

run 4K wall 1080p wall VRAM peak (4K / 1080p) frames on GPU (4K / 1080p) output vs CPU (4K / 1080p)
main CPU 466.7 s 74.2 s n/a 0 / 0 reference
main --gpu 525.2 s 74.1 s 7,888 / 7,888 MiB 0 (silent fallback) / not reported MD5-identical / PSNR 49.9 dB
this PR --gpu 318.6 s, CPU chosen up front 16.8 s 846 / 4,976 MiB 0 / 90 of 90 MD5-identical / PSNR 49.3 dB
this PR + #688 --gpu 23.1 s 9.0 s 5,980 / 2,678 MiB 90 of 90 / 90 of 90 PSNR 50.8 dB / 49.3 dB
  • 4K on an 8 GB GPU needs GPU: resident effect stacks, in-frame texture reuse, upload-cache budget, stable footprint keys (#687) #688. Without it, one 4K frame of this comp needs about 10.5 GB of working VRAM against a 5,043 MB budget. Auto renders 4K on the CPU from the start and says why. An earlier build without the estimate planned 2 frames in flight, and 85 of 90 frames ran out of memory and fell back one by one (312.9 s).
  • At 1080p this PR renders every frame on the GPU, 4.4x faster than main --gpu. The 318.6 s 4K run is faster than main's CPU run because of the larger layer cache.
  • The SM clock held 1708-1847 MHz; the GPU reached 87 °C and 126 W with no thermal throttle flag. CPU throttling was not measured.

Test Mac: Apple M4, 16 GB

1080p only, one run per row, with an earlier build stacked on #688 (this PR alone has not been measured on the Mac). Main CPU 18 s; main --gpu 16 s; this PR + #688 --gpu 6 s with all 90 frames on the GPU and the same PSNR against the CPU as main --gpu (49.7 dB). With #688 stacked, the M4 also rendered 4K on the GPU.

Box without a hardware GPU (llvmpipe)

Bench4K 0.5 s at half resolution, one run each: main --gpu was killed by the OOM killer after 16.8 s; this PR with --gpu --backend gpu completed in 14.9 s with no declines. llvmpipe rasterises on the CPU, so this shows bounded memory, not speed.

@SakithaSamarathunga33

Copy link
Copy Markdown

Tested #688 + #898 together on an 8 GB NVIDIA card on Windows, as asked in #879. Short version: every frame stays on the GPU, VRAM stays flat, output matches the CPU, and the 4K auto rule is now far too conservative for 8 GB cards.

Setup

Results (H.264, 144 frames, render --gpu, VRAM is the system-wide memory.used peak, idle ≈ 1.7 GB)

case tuning time (whole job) render summary VRAM peak
1920×1080 (--resolution half) auto 6.4 s 144 on the GPU, 0 on the CPU (40.3 fps render) +1.17 GB
3840×2160 --backend gpu (forced) 15.6 s 144 on the GPU, 0 on the CPU, 0 declined (11.8 fps render) +2.69 GB

For comparison, the stock CPU path took about 2.4 s per 4K frame on this machine (~6 min for the comp).

GPU vs CPU pixels (6 frames at 4K, --format png --start 1.5 --end 1.75, --backend gpu vs --backend cpu): PSNR 90.7–95.1 dB, max difference 2/255, 0.000 % of pixels differ by more than 2.

The 4K auto rule
--print-tuning at 4K says:

one 3840x2160 frame needs ~10537 MB of working memory and the budget leaves 5008 MB … rendering on the CPU up front … (GPU memory savings such as … #688, would let it render on the GPU)

With #688 in the build, the measured peak for that frame size was +2.69 GB, about a quarter of the estimate, and all 144 frames rendered on the GPU. So on 8 GB cards the auto rule currently sends 4K exports to the CPU when they fit. It may be worth recalibrating the ~1,332 B/pixel figure once #688 lands (or checking whether texture reuse is on). One data point only; content-dependent, of course.

Merging the two PRs (in case it saves someone time): #898 conflicts with #688 in crates/gpu/src/context.rs (the Pool struct, set_pool_budget, and the upload-budget check in upload_buf; I kept both sides). Git also auto-merges a duplicate upload_budget field into GpuContext (both PRs add it, one in the struct and one in the constructor), which fails with E0124/E0062. Removing #898's copy builds.

Measured on my machine with the help of Claude (an AI assistant), which wrote the test scripts; all numbers come from this PC.

@SakithaSamarathunga33

SakithaSamarathunga33 commented Oct 13, 2026 •

Copy link
Copy Markdown

Follow-up on the 4K estimate: I re-measured GPU_FRAME_BYTES_PER_PIXEL with #688 in the build, using the same method as the doc comment in tuning.rs (peak over idle at two output sizes; the fixed part cancels in the difference). Same machine and build as above (RTX 5060 8 GB, 0.9.2 + #688 + #898).

Every run: render --gpu --backend gpu --gpu-inflight 1, H.264, nvidia-smi -lms 100, 3 runs per row (the runs agreed within a few MB), every frame on the GPU, 0 declined. The two demo comps are in --demo, so anyone can repeat this.

comp sizes compared peak over idle (MiB) bytes per output pixel
EffectCraft Intro (--demo) 1920×1080 vs 960×540 2646 vs 1620 692
3D Showcase (--demo) 1920×1080 vs 960×540 1370 vs 854 348
my 4K comp from the comment above 3840×2160 vs 1920×1080 2712–2800 vs 1172 260–274

So with #688 the per-pixel cost is about 2–5× below 1,332, but it also varies about 2.5× with content, so a single constant can't be both safe and tight:

  • A safe recalibration (worst case 692 + 25 % ≈ 870 B/px) still sends 4K to the CPU on an 8 GB card: about 6.9 GB for the frame against 5,008 MB that the plan leaves free. My comp actually peaked at +2.8 GB in total at 4K, including the caches and the fixed part.
  • Part of the gap is the up-front reservation: the plan subtracts the full 1,536 MB upload budget, the 407 MB pool and 384 MB before the frame. That is a cap, not what the comp used.
  • Since Export: stream frames through a shared CPU/GPU work queue, convert to YUV 4:2:0 on the GPU, and tune to the hardware #898 already recovers from a GPU OOM (retry with fewer in flight, then that frame on the CPU, logged), an optimistic choice on GPUs near the line, or one that measures the first frame, might fit better than a tighter constant. That's your call, of course. For now I export with EFFECTCRAFT_EXPORT_BACKEND=gpu, which works well here.

Measured on my machine with the help of Claude (an AI assistant), which wrote the test scripts; all numbers come from this PC.

@MatejKozlovsky

Copy link
Copy Markdown
Contributor Author

@SakithaSamarathunga33 thank you for testing this so thoroughly on the RTX 5060. The real 4K comp, the GPU-vs-CPU PSNR check and the per-pixel re-measurement with reproducible --demo runs are all very useful.

The duplicate-field clash is fixed in 7c29ed9. #898's upload-budget code is now identical to #688's, and set_pool_budget and the Pool budget field have moved away from #688's hunks. Both merge orders (#688 into #898 and #898 into #688, on top of main) now merge without conflicts, with a single upload_budget field, and build.

You're right about the 4K estimate. With #688 your numbers give about 692 B/px (Intro), 348 B/px (3D Showcase) and 260–274 B/px (your 4K comp), against the 1,332 B/px constant. They also vary about 2.5× with content, so no single constant is both safe and tight. Once #688 lands I'll revisit this in a follow-up: plan from the first frame's measured peak rather than a constant. Until then, EFFECTCRAFT_EXPORT_BACKEND=gpu is the right workaround.

h-a-n-a-s-h-i pushed a commit that referenced this pull request Oct 13, 2026
With a GPU accelerator every frame of a batch rendered in parallel, each
holding its own working textures, so a 30-frame 1080p batch could take
the whole card (20+ GB on an RTX 3090) and run shared-memory GPUs out of
memory. Cx::frames now renders GPU frames in consecutive chunks of 3
(EFFECTCRAFT_GPU_INFLIGHT overrides it), the same cap as the export
scheduler in #898; output order and the CPU path are unchanged.

Fixes #879
h-a-n-a-s-h-i pushed a commit that referenced this pull request Oct 13, 2026
Three frames at once still filled an 8 GB card on the #879 project at
1080p (7.9 GB, the same as no cap); one frame peaked at 6.3 GB and two at
7.0 to 7.8 GB. One matches how the export scheduler in #898 renders image
sequences (one GPU worker, synchronous readback). EFFECTCRAFT_GPU_INFLIGHT
still raises it.
h-a-n-a-s-h-i pushed a commit that referenced this pull request Oct 13, 2026
- The cap applied whenever an accelerator was attached, so Software Only
  projects (which render on the CPU) rendered one frame at a time. It
  now needs the project's GPU acceleration too, as #898 gates it.
- Only the render is capped: each window of max(cap, threads) frames
  renders in chunks of the cap, then encodes in parallel, holding at
  most one window of images.
- An invalid EFFECTCRAFT_GPU_INFLIGHT (0, negative, not a number, too
  large) logs a warning before using the default.
- The default stays one frame: the GPU backend reports no free or total
  device memory portably, so it is not sized from VRAM.
…can convert an export frame to 8-bit YUV 4:2:0 with an asynchronous readback (storytold#897)

Defaulted Accelerator hooks for the export: adapter_info, set_memory_budgets, comp_frame_yuv420 (declines by default, so the caller renders on the CPU) and poll_readbacks. gpu_flags carries the export's switches (resident precomps, unified memory) and the reason the accelerator last declined a frame on this thread, so the GPU crate reads no environment.
@echelon

echelon commented Oct 13, 2026

Copy link
Copy Markdown
Member

Thank you for this, @MatejKozlovsky!

Thanks for the detailed tests (byte-identical YUV and scheduler output). We're holding it for a maintainer decision, and #1184 and #688 landed first, so it needs a rebase. Things to change:

  • crates/export/src/hw.rs spawns nvidia-smi, sysctl/vm_stat or powershell.exe from the library on every export. Please move hardware detection, the EFFECTCRAFT_EXPORT_* environment variables and the config file into the CLI, and pass the results in.
  • The CPU export layer cache quietly grows from 1 GiB to up to 4 GiB.
  • The new thread-priority dependency needs maintainer sign-off.

If you are not on our Discord yet, please join us: https://discord.gg/artcraft

…chronously through a staging ring (storytold#897)

A compute kernel does the export's to_rgba8_over + rgba_to_yuv420 (byte-identical to the CPU, tested), so a 4K frame reads back 12 MB of 8-bit 4:2:0 instead of 133 MB of RGBA f32, through the context's bounded readbacks. The context reports its adapter and takes upload and pool budgets (set_upload_budget / set_pool_budget; defaults unchanged); resident precomps follow the export's switch (on by default).
…ue with a bounded reorder buffer, adaptive CPU workers and hardware-adaptive tuning, instead of handing the GPU a batch of frames at once (storytold#897)

The batch-parallel export sent a whole batch (up to 16 frames) to the GPU at once; the frames filled VRAM and fell back to the CPU without a word. A tuned plan now keeps gpu-frames on the GPU, adds CPU workers only while measured throughput improves (and never ones slow enough to stall the in-order encoder), and logs why the GPU declined a frame. H.264/HEVC/AV1/WebM take the GPU's YUV 4:2:0 frames, read back asynchronously so the GPU worker records the next frame during the copy.

Every option is set independently: CLI > EFFECTCRAFT_EXPORT_<OPTION> > config file > auto. Auto reads the GPU (VRAM, vendor, unified memory) and CPU (threads, performance/efficiency cores, RAM). The tuned pipeline is automatic only on NVIDIA discrete and Apple GPUs; other vendors keep the stock behaviour unless enabled. The RAM budget caps the CPU path's batch. CPU-only exports are unchanged. See crates/export/TUNING.md.
… for more, and the GPU worker runs at normal priority (drops thread-priority and gpu-priority / gpu-ideal-cpu: off by default, no measured gain) (storytold#897)
…FECTCRAFT_EXPORT_* variables and the tuning config file, and passes them in; the export library spawns no processes and reads no system files (storytold#897)
@MatejKozlovsky
MatejKozlovsky force-pushed the gpu-export-scheduler-standalone branch from 7c29ed9 to 5aa3db0 Compare October 13, 2026 13:44
@MatejKozlovsky

Copy link
Copy Markdown
Contributor Author

Thanks @echelon, rebased onto current main (with #688 and #1184) and addressed all three points:

  • Hardware detection, env and config moved to the CLI. The export library no longer spawns nvidia-smi, sysctl/vm_stat or powershell.exe or reads /proc/sysfs. The CLI measures the host (RAM, core kinds, VRAM), reads the EFFECTCRAFT_EXPORT_* variables and the config file, and passes them in through tuning::Request. Other callers pass their own values or none (conservative plan).
  • Layer cache back to 1 GiB. A larger cache is opt-in via cache-mb. For the record, on a heavy comp (test PC, RTX 3090 24 GB, 1080p, 60 frames, H.264) 1 GiB takes 60 s vs 47 s with cache-mb 4096, so it may be worth a separate discussion, but it's no longer silent.
  • thread-priority dropped. The gpu-priority / gpu-ideal-cpu options were off by default with no measured gain, so the dependency and both options are gone.

One more change: the scheduler now keeps 1 frame in flight per GPU worker by default. With more, a frame that runs out of GPU memory is retried, and the retry plus a few frames after it don't render exactly like a clean run. With 1 in flight, 3 runs are byte-identical and it's still faster than main. gpu-inflight N remains available. I'll file the retry issue separately.

#1212 overlaps with this PR's budgeting (cpu-batch / ram-budget-mb); whichever lands second should adopt the other's rule rather than add a second budget.

Tests: effectcraft-gpu + effectcraft-export pass (317 passed, 0 failed).

@MatejKozlovsky

Copy link
Copy Markdown
Contributor Author

Wrong layers after an absorbed GPU out-of-memory: fix proposed (3 small commits on top of 5aa3db0)

What broke. On a test PC (GTX 1070 8 GB), a GPU export of a 40-layer effects comp at half resolution rendered the wrong layers. Some layers appeared several times and others were missing: about 217k px differ on frame 0, by up to 255 levels. That frame was already rendered on the CPU after the GPU declined it. main and v0.9.2 are correct. main + this PR alone gives the same wrong bytes. --cache-mb 0 hides it.

Why. With this PR, an uncaptured OOM inside an armed frame no longer retires the device; the frame is declined if Oom::count changed. Two gaps:

  1. Validation errors from the resources the failed allocation left invalid (e.g. BindGroup … is invalid at Queue::submit) were absorbed while tainted without changing the count, so work recorded after the OOM never noticed them.
  2. While the GPU frame records, layers that fall back to the CPU renderer run their effect chains through Accelerator::effects → Enc::download. When the copy was dropped, read_texture_into still mapped its pooled staging buffer and returned that buffer's previous contents, i.e. another layer's pixels. The renderer then stored them in the CPU layer cache. Declining the frame clears only the GPU upload cache, so the CPU re-render (and later cache hits) used the poisoned entries.

Why only on cards that really run out. The path is taken only on real allocation failures. On a 24 GB GPU every variant is byte-identical to main, including --vram-budget-mb 8192 and 6144: the budgets only change the tuning, not what the driver allocates. The 8 GB card sits at about 7.9 GB and hits a few hundred OOMs per run.

Fix.

  • Count validation errors absorbed while tainted (readback::Oom::on_error).
  • Enc::read_texture_into returns None if the count changed during the readback. The effect chain then falls back to the CPU, and nothing invalid reaches the layer cache.
  • A unit test: validation after a failed allocation invalidates a readback that started later, and no longer counts once settled. The existing OOM test's expected counts are updated.

Tests.

  • cargo test --release -p effectcraft-gpu -p effectcraft-render -p effectcraft-export: all pass (535 passed, 0 failed, 3 ignored, the ignores already present), including the new validation_after_a_failed_allocation_invalidates_later_readbacks.
  • Test PC (GTX 1070 8 GB), comp c, half res: frames 0–5 match the --cache-mb 0 reference except for 1-level GPU rounding on 66–142 px per frame. Without the fix, frame 0 has 216,936 px wrong. The run still absorbs about 2,475 OOM errors and discards 191 readbacks, and it now declines and CPU-renders only frame 0.
  • 24 GB GPU: output unchanged against 5aa3db0 on 11 frames each of three bench comps (effects, text, blur/glow), byte-identical.

Possible follow-ups, not in this patch:

  • The same check for the other synchronous readbacks (adv3d, particles).
  • Per-Enc arming. Today an error from another thread's unarmed work is absorbed while any frame is armed.

@MatejKozlovsky

Copy link
Copy Markdown
Contributor Author

Closing this one. It grew way too big and mixes too many things. I'm splitting it up:

The OOM absorption bit isn't coming back in this form. It shared state across frames in flight, which is racy. That should get one proper recovery design instead. The scheduler/tuning part may come back later as its own smaller PR, without the env vars, with the knobs on the CLI.

Thanks for all the review here, it shaped the split.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GPU export: render --gpu sends the GPU a whole batch of frames, runs out of memory and falls back to the CPU without logging it

3 participants