unsloth/tests/python
Daniel Han 827d25931b
Stop 19 test files racing on one PowerShell startup cache (#9371)
* Stop 19 test files racing on one PowerShell startup cache

Backend CI run 32341628757 on `1c3dde199` finished `284 failed, 8498 passed`. Every
one of the 284 was a pwsh subprocess ending `died with <Signals.SIGABRT: 6>`, across
19 files that all read as Windows-installer regressions. None of them were. 222 of
the aborts land inside a two-second window, 88 at 07:09:30 and 133 at 07:09:31,
which is a mass kill of every live pwsh rather than independent per-test flakiness.

The cause
------------------------------------------------------------------------
Every `-NonInteractive` startup reads and rewrites an ~83 KB
`$XDG_CACHE_HOME/powershell/StartupProfileData-NonInteractive`, and XDG_CACHE_HOME
defaults to `$HOME/.cache`. Under `-n 4` all four xdist workers share one HOME, so
the whole job's pwsh processes race on one file and a startup that deserialises a
half-written one dies before it reaches our script. `Stack overflow.` is .NET's
failfast, which cannot unwind a blown stack, so it prints one line and calls
abort(); that is the SIGABRT (PowerShell/PowerShell#24461).

Measured twice, independently, 4000 startups per arm:

  run 1  shared cache dir     7/4000 died  {-11: 3, -6: 4}
         private cache dirs   0/4000
  run 2  shared cache dir    11/4000 died  {-11: 10, -6: 1}
         private cache dirs   0/4000

Three distinct crash shapes appeared, and each names the torn file rather than our
scripts: `Stack overflow.`, `System.IO.FileLoadException: The given assembly name`,
and `System.ArgumentException: String cannot have zero length.` 18 deaths in 8000
shared startups, 0 in 8000 private.

CI agrees from the other direction. Of the pwsh-heavy files in that run, exactly one
had zero failures, tests/test_windows_amd_gpu_scan_fallback.py, and it is the only
one that hands its child a private HOME, across roughly 80 startups where the run's
own rate predicts about 16 failures.

What is NOT established
------------------------------------------------------------------------
Neither experiment reproduces CI's rate. Roughly 20% of pwsh startups died there
against 0.2 to 0.3% here, and at CI's actual `-n 4` on this box I measured 0/1200 in
both arms: the race needed 48-way concurrency before it appeared at all. The likely
reason is that four workers on a 4-core runner are in real contention while four
threads on a 192-core box almost never overlap in the critical section, but that is
reasoning and not a measurement, so treat the mechanism as proven and the magnitude
as unexplained. That is also why this does not stop at removing the shared file.

Three layers, in order
------------------------------------------------------------------------
1. Remove the contended resource. One cache directory per xdist worker, fresh per
   session. Workers run their tests one at a time, so within a worker the startups
   stay sequential and the cache still does its job warm; across workers the
   directories are disjoint and there is nothing left to race on. Fresh rather than a
   stable path, because a cache torn by an earlier run would otherwise poison every
   later session on the same box.
2. Retry a run that produced no verdict. Three attempts, unslept, because the trigger
   is process startup rather than a resource that frees up.
3. Attribute what is left. A crash raises PwshInterpreterCrash naming the interpreter.

Layer 1 is the fix; 2 and 3 exist because of the unexplained magnitude above.

Deliberately NOT done: bounding pwsh concurrency with a lock, or giving up `-n 4`.
The workflow records 806.1s to 219.7s from that flag, and the contended resource can
be removed rather than rationed.

The rule that keeps this honest
------------------------------------------------------------------------
A signal is not a verdict, so retrying it papers over nothing: the script never ran
to its end. A normal exit is returned untouched on the first attempt whatever its
code, so a pwsh that runs and gives the WRONG answer still fails with its own
message. Getting that second half wrong would turn this into a way to retry real
regressions into green, which is worse than the bug it fixes, so both directions are
executed in tests/studio/test_pwsh_interpreter_crash_attribution.py against a real
SIGABRT rather than reviewed.

Mutation-tested: relaxing the crash test from `returncode < 0` to `returncode != 0`
fails test_a_clean_run_with_the_wrong_answer_still_fails_with_its_own_message and
test_a_clean_run_is_not_retried, which are exactly the two that guard that direction.

This also generalises `_run_pwsh` from tests/studio/test_install_phase_timing.py,
added earlier today for a second, signal-free shape: pwsh printing its "The
PowerShell process will exit" banner and exiting normally with empty stdout. That one
cannot be seen in the exit status, so it stays a text match.

Verified
------------------------------------------------------------------------
tests/python/test_windows_xformers_installer.py, tests/studio/test_install_phase_timing.py,
tests/studio/install/ and the new guard: 2635 passed, 3 skipped.
Guard alone: 5 passed.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Drop the subprocess import the pwsh conversion left behind

Source lint's import-hoist check is right: every subprocess.run in
test_windows_xformers_installer.py became run_pwsh, so `import subprocess` has no
references left except the one inside a comment explaining why run_pwsh is used
instead. Its wording names the shape exactly -- "was used before, now unused
(references re-pointed)" -- which is what a mechanical call-site rewrite leaves
behind.

Swept the other 18 converted files the same way with an AST pass rather than by
eye. This was the only real one: the remaining hits are `from __future__ import
annotations`, which every such scan reports, and a PropertyMock in
test_rocm_support.py that is present on main unchanged.

42 passed.

* Suppress the core dump on the forged SIGABRT

tests/test_deliberate_crashes_suppress_cores.py caught this: the abort child had no
PR_SET_DUMPABLE=0, so each of these aborts piped a multi-MB core to apport before the
child could be reaped. The guard is right and its message names the fix.

The child still exits -6 and PR_GET_DUMPABLE reads 0, so all five verdicts are
unchanged. Linux-only and non-fatal elsewhere: Windows has no CDLL(None) and pipes no
core, so arming it there would trade a no-op for a lost test.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: danielhanchen <unslothai@gmail.com>
2026-08-20 04:22:41 -07:00
..
__init__.py Consolidate dual venvs and separate install from update (#4530) 2026-03-25 05:24:21 -07:00
conftest.py Formatting: ruff line-length 100, kwarg-spacing passes, drop blank after short local imports (#6079) 2026-06-08 04:24:13 -07:00
test_bitsandbytes_kernel_readiness.py Anchor the bnb bind assertion on the symbol, not the module alias (#7590) 2026-07-28 21:35:04 -07:00
test_change_system_message.py Escape the system message spliced into predefined chat templates (#7746) 2026-08-02 07:18:09 -07:00
test_conftest_bitsandbytes_preimport.py Tests: import bitsandbytes before the GPU-free harness spoofs CUDA (#7582) 2026-07-28 18:52:25 -07:00
test_construct_chat_template_validation.py Render a static prefix when default_system_message is None (#8117) 2026-08-08 14:57:17 +02:00
test_cpo_processor_text_tokenizer.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_cross_platform_parity.py Stub the two helpers the sliced harnesses started importing (#9342) 2026-08-19 22:06:51 -07:00
test_dpo_vision_processor_passthrough.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_e2e_no_torch_sandbox.py Studio: apply base.txt on the install.sh and install.ps1 paths (#8195) 2026-08-11 00:06:23 -07:00
test_fast_language_model_text_only.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_fast_model_config_passthrough.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_fast_sentence_transformer_embedding_parity.py Fix FastSentenceTransformer Qwen embedding preprocessing (#6939) 2026-07-09 01:46:22 -07:00
test_fast_sentence_transformer_redirect_lifecycle.py Reduce and tighten comments and docstrings across the test suite (#6429) 2026-06-18 01:07:09 -07:00
test_flash_attn_install_python_stack.py Studio: verify the flash-attn import after installing it (#8465) 2026-08-12 05:43:51 -07:00
test_get_chat_template_escaping.py Escape the system message spliced into predefined chat templates (#7746) 2026-08-02 07:18:09 -07:00
test_get_lora_parameters_bias_fp8_block_size.py Propagate fp8 block_size before the early return in get_lora_parameters_bias (#7189) 2026-07-17 16:30:45 -07:00
test_get_lora_parameters_fp8_block_size.py Propagate fp8 block_size before the early return in get_lora_parameters_bias (#7189) 2026-07-17 16:30:45 -07:00
test_gpu_init_ldconfig_guard.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_grpo_ddp_model_config.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_import_without_bitsandbytes.py Keep import unsloth working when bitsandbytes is absent (#7502) 2026-07-28 08:03:54 -07:00
test_install_python_stack.py Studio: propagate required backend version to repair pipeline (#8610) (#8670) 2026-08-19 19:12:22 +01:00
test_install_uv_override_space.py Installer: make UV_OVERRIDE space-safe on Apple Silicon (#6503) (#6639) 2026-06-24 17:34:18 -07:00
test_mlx_public_trainer_api.py Bound dataset_num_proc by memory, and stop treating 1 as "no multiprocessing" (#7831) 2026-08-05 05:31:27 -07:00
test_module_entry_point.py Windows: stop depending on the generated unsloth.exe console script (#8592) 2026-08-13 07:54:51 -07:00
test_no_torch_filtering.py Run the repo CPU test suite on all four runner cores (#9019) 2026-08-16 21:46:01 -07:00
test_orpo_processor_text_tokenizer.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_pad_token_fix.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_patch_trl_rl_trainers_defensive.py Reduce and tighten comments and docstrings across the test suite (#6429) 2026-06-18 01:07:09 -07:00
test_remove_special_tokens_no_bos.py fix: guard remove_special_tokens against tokenizers without a BOS token (#7048) 2026-07-10 14:55:11 -03:00
test_revision_forwarding.py Forward revision to the config, weight and tokenizer loads (#4222) 2026-08-02 22:35:19 -07:00
test_rl_config_pickling.py Keep checkpoint saves working, and portable, once a TRL config is patched (#8344) 2026-08-10 20:49:40 -07:00
test_studio_import_no_torch.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_studio_runtime_gate.py Repair seven tests left behind by their own subjects (#8132) 2026-08-07 19:43:27 -07:00
test_to_sharegpt_optional_none.py Fix to_sharegpt optional block rendering "None" for missing extra columns (#6827) 2026-07-08 17:57:41 -03:00
test_tokenizers_and_torch_constraint.py Studio: drop the mlx-lm 0.31.3 exclusion so current mlx-vlm resolves (#7061) 2026-08-07 02:41:04 -03:00
test_torchcodec_torch_compat.py Gate the torchcodec audio extras to platforms that have a wheel (#7587) 2026-07-28 20:56:11 -07:00
test_unsloth_run_tool_policy_resolver.py Studio: honor a request's enable_tools: false instead of overriding it (#8547) 2026-08-12 06:26:58 -07:00
test_v100_fullft_precision.py Export ACCELERATE_MIXED_PRECISION when bf16/fp16 is set explicitly (fixes #4891) (#7534) 2026-07-29 06:39:18 -07:00
test_virustotal_scan.py Reduce antivirus false positives in the desktop installers (#8586) 2026-08-13 07:02:18 -07:00
test_vision_lora_targeting.py tests: read checked-in files as UTF-8 instead of the platform default (#7438) 2026-07-26 23:31:56 -07:00
test_windows_arm64_python_choice.py Stop 19 test files racing on one PowerShell startup cache (#9371) 2026-08-20 04:22:41 -07:00
test_windows_git_gate.py Stop 19 test files racing on one PowerShell startup cache (#9371) 2026-08-20 04:22:41 -07:00
test_windows_installer_addtype_fallback.py Stop 19 test files racing on one PowerShell startup cache (#9371) 2026-08-20 04:22:41 -07:00
test_windows_installer_concurrency_guard.py Install Studio on a Windows host whose C# compiler cannot run (#9178) 2026-08-19 08:03:21 -07:00
test_windows_no_torch_setup.py Stop 19 test files racing on one PowerShell startup cache (#9371) 2026-08-20 04:22:41 -07:00
test_windows_python_313_8_screen.py Stop 19 test files racing on one PowerShell startup cache (#9371) 2026-08-20 04:22:41 -07:00
test_windows_python_venv_hardening.py Stop 19 test files racing on one PowerShell startup cache (#9371) 2026-08-20 04:22:41 -07:00
test_windows_setup_output_encoding.py Stop 19 test files racing on one PowerShell startup cache (#9371) 2026-08-20 04:22:41 -07:00
test_windows_studio_update_launcher.py Windows: stop depending on the generated unsloth.exe console script (#8592) 2026-08-13 07:54:51 -07:00
test_windows_vcredist_download_tls.py Stop 19 test files racing on one PowerShell startup cache (#9371) 2026-08-20 04:22:41 -07:00
test_windows_xformers_installer.py Stop 19 test files racing on one PowerShell startup cache (#9371) 2026-08-20 04:22:41 -07:00
test_windows_xformers_wheel_match.py Install a CUDA-matched xFormers on Windows instead of whatever PyPI serves (#8156) 2026-08-09 00:20:55 -07:00