Bound tensorization process-pool shutdown at interpreter exit - #855
Merged
Conversation
The shared spawn-based ProcessPoolExecutor behind art.tokenize/tensorize was never shut down by ART, so concurrent.futures owned the final join during threading._shutdown(). Its manager thread only sends the worker shutdown sentinels once no work item is pending, so a worker still busy with an item nobody can consume anymore (for example after SIGINT-driven cleanup cancelled the awaiting task) blocked interpreter exit for as long as that item ran, leaving drivers alive with idle-looking children. Register a threading exit hook that runs before the stdlib join: release the global pool, shut it down without waiting, give workers a short grace period to leave via the sentinel, then terminate and finally kill any survivor. Idle pools still exit through the normal sentinel path, worker reuse during the process lifetime is unchanged, and the thread fallback and broken-pool handling are untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_artifact_config fetched the W&B artifact on every call, so tokenizing with a wandb-artifact:/// model paid an API round trip per trajectory in every tensorization worker; only the base model name was cached. Cache the resolved _TokenizerConfig per model string (bounded, single-flight, exceptions not cached) and hand callers a copy, since _tokenizer_config adjusts the config when base_model is given. The metadata describes the checkpoint collection and does not change between versions, the same assumption the base-model cache already makes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ProcessPoolExecutordown explicitly at interpreter exit, beforeconcurrent.futuresjoins its workers, so the join is boundedwandb-artifact:///checkpoint's tokenizer configuration once per process instead of making a W&B API request on every tokenizationRoot cause of the shutdown hang
concurrent.futures.process._python_exitruns insidethreading._shutdown()and joins the pool's manager thread. The manager only sends shutdown sentinels oncepending_work_itemsis empty, so any worker still executing an item (for example a tensorization whose awaiting task was cancelled by SIGINT-driven cleanup) blocks interpreter exit for as long as that item runs. ART never shut the global pool down itself, so the stdlib owned the final join.Reproduced with a worker holding one in-flight item at exit: on
mainthe driver never exits (faulthandler: main thread inprocess._python_exit→join, manager thread inwait_result_broken_or_wakeup); with this change the same script exits in 5.8 s with no surviving children. The idle-pool path (normal exit, cancellation, worker exception, SIGINT) already exited in ~2 s onmainand still does. The exact item that kept the 046 workers busy was not captured (py-spy cannot attach on that host), so the fix bounds the join regardless of cause.Checkpoint tokenizer configs
_artifact_configfetched the artifact from W&B on every call; only the base model name was cached, and only alias-independently. Experiment 046 passesmodel=<wandb-artifact checkpoint>toart.tensorize, so every trajectory tokenized in every worker paid a 0.2–0.5 s API round trip (measured against the 046 checkpoint artifact). The resolved_TokenizerConfigis now cached per model string (bounded, single-flight, exceptions not cached), and callers receive a copy because_tokenizer_configmutates the config whenbase_modelis given. The metadata describes the checkpoint collection, so it is stable across versions and aliases, the same assumption the existing base-model cache already made.Validation
test_interpreter_exit_is_bounded_after_process_tokenization: real spawn pool, twoart.tokenizecalls reuse the same worker PIDs, then one never-finishing item plus a cancelled tokenization at exit; asserts exit code 0, exit within 30 s of the last result (observed ~2 s beyond the 1 s test grace), and no surviving worker PIDs. Againstmainthe same test hits a 150 s timeout.test_process_exit_hook_runs_before_stdlib_joins_workerstest_checkpoint_tokenizer_configs_are_resolved_once_per_process: one API call across repeated and concurrent resolutions, independent copies,base_modeloverride does not leak into the cachepytest tests/unit/trajectories/test_tokenize.py tests/unit/trajectories/test_parallel_tokenize.py(253 passed)ty check src testsunchanged frommain🤖 Generated with Claude Code