Repository navigation
Conversation
…ws offload RAM leak) Every generation ran service_generate on a fresh threading.Thread. On Windows, PyTorch allocates CPU tensors with mimalloc, which abandons a thread's heap when the thread exits; with the DiT offloaded to the CPU, each generation's thread re-allocated the DiT's CPU copy and that memory was never reused once the thread had exited. Private memory grew by 4-12 GB per generation with the XL DiT until the process crashed. service_thread.run_on_service_thread runs each call on an idle long-lived daemon thread and starts a new one only when all are busy, so sequential generations reuse one thread while concurrency and the ACESTEP_GENERATION_TIMEOUT behaviour stay as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… thread The worker kept `fn` bound while waiting for its next job, so the finished generation's inputs, outputs and any exception frames stayed alive until the next generation; a fresh thread used to drop them on exit. Also cover the caller's timeout branch, let the timeout test wait for the pool to settle, and update the stale join() comment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Worker reuse and lifecycle acestep/core/generation/handler/service_thread.py, acestep/core/generation/handler/service_thread_test.py |
The new runner reuses idle daemon workers and starts a worker when none is idle. Tests cover sequential reuse, timeout behavior, callable cleanup, and exception logging. |
Music generation integration acestep/core/generation/handler/generate_music_execute.py, acestep/core/generation/handler/generate_music_execute_test.py |
Music generation uses the worker runner with the existing timeout. If the runner returns False, the helper raises TimeoutError; the test also checks that progress stops. |
Priority:
Estimated code review effort: 3 (Moderate) | ~20 minutes
Change: Bug fix
Sequence Diagram(s)
sequenceDiagram
participant Generation as generate_music_execute
participant Runner as run_on_service_thread
participant Worker as service worker
Generation->>Runner: Submit service callable and generation timeout
Runner->>Worker: Enqueue callable
Worker->>Worker: Execute callable
alt Callable completes before timeout
Worker-->>Runner: Signal completion
Runner-->>Generation: Return true
else Caller reaches timeout
Runner-->>Generation: Return false
Worker->>Worker: Continue running callable
end
Possibly related issues
ace-step/ACE-Step-1.5, issue 1350: The issue concerns work that continues after aservice_generatetimeout. This change retains that timeout behavior, so it does not address the issue.
Suggested reviewers: uni404x64
Merge Risk: 
Minimal · up to 2422e
-
No actionable issue was found in worker reuse or its music-generation integration. The change is ready to merge after normal checks.
Pre-merge checks: 
4 · 
1
-
Failed checks: 1 warning Passed checks: 4 passed-
Full details: Title check
-
Explanation
The title names the Windows RAM leak fix and the mechanism (reusing service_generate threads). The changeset adds run_on_service_thread, which reuses idle daemon worker threads for service_generate calls, and updates generate_music_execute.py to use it. The title is specific and matches the main change. It does not name the DiT offload trigger, but the title still covers the primary change. The title also follows the conventional commit prefix style.
-
-
Finishing Touches
-
🧪 Generate unit tests (beta)
-
- Create a new PR
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
A rabbit sends a task to run,
The worker hums beneath the sun.
If time runs out, the task may stay,
The caller leaves without delay.
Then idle threads await the day.
Comment @coderabbitai help to get the list of available commands.
Summary
On Windows with
ACESTEP_OFFLOAD_DIT_TO_CPU=true, the server's private memory grows by 4–12 GB per generation until the process dies. This is very likely what #1338 reported (an access violation inc10.dllafter ~8–9 offloaded generations on Windows, closed as stale).Cause.
_run_generate_music_service_with_progressstarts a newthreading.Threadfor every generation. PyTorch's Windows builds allocate CPU tensors with mimalloc (compiled intoc10.dll), which keeps one heap per thread and abandons it when the thread exits. Each generation's thread allocates the offloaded DiT's new CPU copy. The next generation frees that copy from a different thread, after the owning thread has exited, and the memory is not reused. Every generation leaves one DiT-sized block behind.#947 handled the Linux side of this with
mallopt/malloc_trim. Windows has nothing equivalent to call, because mimalloc lives insidec10.dlland exports nothing.Fix. A new
service_thread.run_on_service_thread(fn, timeout)runs each call on an idle long-lived daemon thread. It starts a new thread only when every existing one is busy. Sequential generations therefore always run on the same thread, so the DiT's CPU copy is allocated and freed in a live heap and gets reused.generate_music_execute.pycalls it instead of creating a thread. The timeout check and the error it raises are unchanged.Evidence
Setup: RTX 5070 Ti Laptop (12 GB), 64 GB RAM, Windows 11, Python 3.12.10, torch 2.7.1+cu128, REST API server,
acestep-v15-xl-turbowithacestep-5Hz-lm-1.7B(ptbackend),ACESTEP_OFFLOAD_DIT_TO_CPU=true.Server, back-to-back 60 s generations:
Causes ruled out before this one:
HeapCompact/_heapminchanged nothing.MIMALLOC_PURGE_DELAY=0andMIMALLOC_ABANDONED_PAGE_PURGEchanged nothing.Plain-PyTorch repro, no ACE-Step code. A 6 GB bf16 module is moved CPU↔CUDA; private memory is shown per cycle.
The same repro against this PR's module. A 3 GB module is moved CPU→CUDA→CPU once per "take", like one offloaded generation.
run_on_service_threadThe server numbers come from a first version of this change that used a single worker thread. For sequential generations, the final version takes the same path: every call runs on one reused thread, which
test_sequential_calls_reuse_one_threadcovers. The last table was measured with the final module.Repro script (Windows, CUDA)
Scope
acestep/core/generation/handler/service_thread.py(new, 73 lines): the long-lived threads.acestep/core/generation/handler/service_thread_test.py(new): 4 tests. They cover reusing one thread for sequential calls, the timeout result with the next call not blocked, logging an exception and keeping the thread, and not keeping a finished call's closure alive.acestep/core/generation/handler/generate_music_execute.py: callsrun_on_service_threadinstead of starting athreading.Threadper call.acestep/core/generation/handler/generate_music_execute_test.py: 1 new test for the timeout branch (raisesTimeoutErrorand stops the progress estimator).Out of scope:
Risk and compatibility
service-generate) are unchanged. The only difference is that a call may run on a thread that ran an earlier call. Gating this to Windows would keep two code paths for no functional gain, but I'm happy to gate it if you prefer.ACESTEP_API_WORKERS> 1) each get their own thread immediately, as before. Nothing queues behind another call, so timeouts are measured exactly as before.ThreadPoolExecutorworkers would block it.service_generatepath sets no thread-local state that would persist on a reused thread:inference_modeis restored on exit, and the path has noset_device,set_stream, autocast,threading.localorContextVar.Regression checks
uv run python -m unittest acestep.core.generation.handler.service_thread_test acestep.core.generation.handler.generate_music_execute_test acestep.core.generation.handler.generate_music_test acestep.core.generation.handler.service_generate_test: 20 tests, OK (Windows 11, Python 3.12.10).service_thread_testrun 200 times in one process: 0 failures, and only 2service-generatethreads alive at the end.acestep/core/generation/handlerhas the same 5 pre-existing test errors on main and on this branch. The affected files areinit_service_test,io_audio_test,reference_audio_validation_test,service_generate_flow_edit_testandvae_decode_chunks_test.Reviewer notes
service_generatecall moves. Offloads outside it, such as the VAE context in the decode step, run on the caller's thread.gen_threadblock, to poll it for progress. If it lands first, I'll rebase and let it poll the call's completion event instead.🤖 Generated with Claude Code
Summary by CodeRabbit