Skip to content

build(deps): raise the declared Python floor to 3.12 - #172

Merged
ashtonchew merged 2 commits into
mainfrom
build/python-floor-3-12
Aug 17, 2026
Merged

ashtonchew merged 2 commits into
mainfrom
build/python-floor-3-12

Conversation

@ashtonchew

@ashtonchew ashtonchew commented Aug 17, 2026 •

Copy link
Copy Markdown
Collaborator

Review this as a dependency upgrade, not a metadata fix. The CUDA cutover it triggers is explained in full below.

Why

requires-python = ">3.10" claimed support for 3.11 and newer. The lockfile cannot deliver that: 3.13 fails building greenlet 3.0.3 from source (playwright 1.44.0 via libwebarena pins it, no cp313 wheel), and 3.14 fails because ray publishes no cp314 wheel. #161 pinned the working interpreter with .python-version but left the declaration false.

Three places state the Python floor. This makes all three agree at 3.12.

before after
requires-python >3.10 (false) >=3.12,<3.13
Ruff target-version py310, hand-maintained deleted — inferred
mypy python_version 3.10 3.12

Ruff's target-version is deleted rather than raised because its docs prefer inferring from requires-python — "it's based on Python packaging standards, and will be respected by other tools" — so the value can no longer drift from the declaration. ruff check --show-settings reports unresolved_target_version = 3.12.

The dependency move

In the default install profile, exactly one package moves:

torch  2.9.1 → 2.11.0

numpy (2.2.6), pandas, scipy, transformers (4.57.5), ray (2.53.0) and playwright (1.44.0) all hold. Outside the default profile, vllm 0.16.0 → 0.23.0 and xgrammar 0.1.29 → 0.2.4 affect only the cua optional extra, and triton 3.5.1 → 3.6.0 is Linux-only.

The move is a consequence of the fork collapse rather than an upgrade decision. The old lock carried torch==2.9.1 ; python_full_version < '3.13' beside torch==2.11.0 ; python_full_version >= '3.13'; narrowing the range to 3.12 collapses the fork, and uv keeps the higher pin. uv.lock goes 413 → 360 packages.

Numerics

torch 2.11 documents no change to default dtypes, matmul/TF32 precision, RNG, or determinism; its headline features are opt-in and marked API-unstable. Two properties of this tree make drift unlikely regardless: all dtype selection is explicit bfloat16, and no first-party code calls torch.manual_seed or use_deterministic_algorithms — the seeds that exist govern dataset sampling only. No reproducibility guarantee that previously held is broken.

The CUDA cutover

torch 2.11 changes which CUDA build the default PyPI wheel carries: CUDA 13 replaces CUDA 12 as the default variant, on x86_64 and ARM alike. That has two consequences, and only one of them applies here.

Architecture support — does not apply. CUDA 13 removes Maxwell, Pascal and Volta, everything below Turing (sm_75). The hosts this project provisions are A100 (sm_80) and H100 (sm_90), per docs/HANDOFF_CUA_EVAL_AWARENESS.md and docs/HANDOFF_GUI_OWL_PROBE_RESULTS.md. Both sit above the new floor, so no host loses support.

Driver floor — applies, and has already bitten this project. CUDA 13.0 requires NVIDIA driver ≥ 580.65.06 on Linux, against ≥ 525.60.13 for CUDA 12.0. A host below that cannot use the default wheel, and the failure mode is the dangerous one — silent:

torch.cuda.is_available() returned False and the model silently loaded on CPU

That is quoted from docs/HANDOFF_GUI_OWL_PROBE_RESULTS.md, recording a pod on driver 565.57.01. No exception is raised; the run simply completes on the wrong device.

What this PR changes is the blast radius, not the failure. That gotcha previously arrived only through the [cua] extras, where vllm pulled torch 2.11.0+cu130. With torch 2.11 in the default dependencies it now reaches every GPU host, whether or not vLLM is installed.

Mitigations, in order of preference:

  1. Provision GPU hosts with driver ≥ 580.65.06. The clean fix, and the direction PyTorch is moving.
  2. Reinstall torch from the cu126 index on a host that cannot be re-driven — the recipe already in the handoff. This breaks vLLM serving; probe extraction does not need it.
  3. Pin a CUDA 12 index in [tool.uv.sources]. Deliberately not done here: it would tie the whole project to a CUDA generation in order to accommodate hosts that can be re-driven instead.

This is not a merge blocker. CPU-only environments — CI, laptops, everything that runs Ruff, Black and mypy — are unaffected, since the CUDA variant only matters where a GPU is present. GPU hosts are provisioned per-run from the handoffs, and this PR corrects those: HANDOFF_GUI_OWL_PROBE_RESULTS.md previously named the [cua] extras as the trigger and "≥570.x" as the driver fix, and both were wrong after this change.

The StrEnum conversions

(str, Enum) renders str(member) as ClassName.MEMBER; StrEnum renders the value. Since this repo's output is experiment results, each was checked rather than autofixed:

enum finding action
AwarenessClassification no reference outside its own definition — grep repo-wide returns one line converted
ReasoningCategory ~40 sites, all ==/in/dict-key/.value; both persistence writes use .value explicitly converted, invariant recorded in the docstring
TokenSelector one f-string, in an unreachable error branch converted

ReasoningCategory was the one that could have bitten — its values differ sharply from its names (ALIGNMENT_EVAL = "Alignment Evaluation"), so the rendering difference would have been visible in any persisted string that used str() rather than .value.

Verification

uv lock --check              → Resolved 360 packages
uv sync --locked --group dev → Checked 250 packages
uv run ruff check .          → All checks passed!
uv run black --check .       → 96 files would be left unchanged
uv run mypy                  → Success: no issues found in 96 source files
ruff --show-settings         → unresolved_target_version = 3.12

Not established

vllm 0.16 → 0.23 was not reviewed to the same depth — the intervening release notes are poorly indexed and 0.23.0 alone spans 408 commits. It sits behind an opt-in extra and does not affect the default profile, but anyone installing .[cua] should treat it as an unreviewed jump.

`requires-python = ">3.10"` claimed support for 3.11 and newer, which the
lockfile could not deliver: 3.13 fails building `greenlet` 3.0.3 from source
(`playwright` 1.44.0 via `libwebarena` pins it, and it publishes no cp313
wheel), and 3.14 fails because `ray` publishes no cp314 wheel. #161 pinned the
working interpreter with `.python-version` but left the declaration false.

Make the declaration true, and let the two gate targets follow it.

- `requires-python = ">=3.12,<3.13"`, with `uv.lock` regenerated. 413 packages
  become 360 as the per-version forks collapse.
- Ruff's `target-version` is deleted rather than raised. Ruff's documentation
  prefers inferring it from `requires-python`, so the value can no longer drift
  from the declaration. `ruff check --show-settings` confirms 3.12.
- mypy's `python_version` becomes 3.12. Measured at no new errors, including
  with #160's `check_untyped_defs` and `strict_equality` now on.
- 22 findings that appear once Ruff infers py312: UP017 x10 (`datetime.UTC`),
  UP042 x3 (`enum.StrEnum`), UP047 x1 (PEP 695 type parameters), plus 8
  cascading F401 and I001 fixes from the UP017 rewrites.

THE DEPENDENCY MOVE. In the default install profile exactly one package moves:
`torch` 2.9.1 -> 2.11.0. `numpy`, `pandas`, `scipy`, `transformers`, `ray` and
`playwright` all hold. `vllm` 0.16.0 -> 0.23.0 and `xgrammar` 0.1.29 -> 0.2.4
affect only the `cua` optional extra; `triton` 3.5.1 -> 3.6.0 is Linux-only.
The old lock carried `torch==2.9.1` for Python < 3.13 beside `torch==2.11.0`
for >= 3.13, so collapsing the fork kept the higher version.

torch 2.11 documents no change to default dtypes, matmul precision, RNG, or
determinism. All dtype selection in this tree is explicit `bfloat16`, and no
first-party code calls `torch.manual_seed` or `use_deterministic_algorithms`,
so no reproducibility guarantee is broken. The deployment change is the one to
watch: CUDA 13 becomes the default wheel, and those wheels drop Volta, Pascal
and Maxwell. This project pins no PyTorch wheel index.

The three `StrEnum` conversions were checked per enum rather than applied
blindly, because `(str, Enum)` renders `str(member)` as `ClassName.MEMBER`
while `StrEnum` renders the value. `AwarenessClassification` has no reference
outside its own definition. `ReasoningCategory` persists through explicit
`.value` at both write sites, an invariant now recorded in its docstring.
`TokenSelector`'s only interpolation is an unreachable error message.
The gotcha recorded during the GUI-Owl probe run named the `[cua]` extras as
the trigger and a >=570.x driver as the fix. Raising the Python floor moves
`torch` 2.11 into the default dependencies, so the first is no longer true,
and CUDA 13.0 requires >=580.65.06 on Linux, so the second was never right.

The architecture drop in CUDA 13 is recorded as not applying: the hosts these
handoffs provision are A100 and H100, both above the new Turing floor.
@ashtonchew
ashtonchew merged commit 1adf7f5 into main Aug 17, 2026
5 checks passed
ashtonchew added a commit that referenced this pull request Aug 17, 2026
Resolves one gap and records a larger one.

Closed: the Qwen2.5-VL encoder-cudagraph cluster (#40830, #41234, #42288,
#42796) is gated behind `cudagraph_mm_encoder`, and that plus
`compile_mm_encoder`, `video_pruning_rate` and `mm_encoder_attn_backend`
are all off/None at both tags. The #42796 padding change this note
previously left unresolved is therefore unreachable, as is the FP8 ViT
work and every EVS fix.

Still open, and moved into B7: the ViT attention backend *selector*
changed from a hardcoded two-way choice to an ordered four-way scan.
FLASH_ATTN is tried first in both, so it is probably a no-op on A100/H100
-- but the fallback diverged (TORCH_SDPA -> TRITON_ATTN) and whether
FlashAttention is accepted in the deployed image is not verifiable from
source.

New A4: `vllm serve` compiles by default at both tags (`optimization_level
= O2` -> `VLLM_COMPILE`), so the torch 2.9.1 -> 2.11.0 move runs through
two different Inductor versions. #172's clearance covered torch's
documented defaults -- dtype, TF32, RNG, determinism -- and holds there;
it does not cover codegen. Recorded as an unquantified lever, not a
finding, and the recommendation now asks for an end-to-end 0.16.0 vs
0.23.0 comparison rather than only the sampler ablation.

Also records that the text decoder was never enumerated. That is a real
gap: the decoder does most of the arithmetic.
ashtonchew added a commit that referenced this pull request Aug 17, 2026
…umerics (#174)

* docs(research): review the unreviewed vLLM 0.16 -> 0.23 upgrade for numerics

PR #172 moved `vllm` 0.16.0 -> 0.23.0 as a side effect of collapsing a
lockfile fork. The move was never reviewed. It sits behind the `cua`
optional extra, so CI and the default install profile are unaffected --
but `.python-version` pins 3.12, and the old lock served 0.16.0 to 3.12,
so every `uv pip install -e '.[cua]'` on the supported interpreter now
gets 0.23.0.

Answers the question that matters for published results: does anything
between the two versions move a number?

Sampling *defaults* did not change -- `SamplingParams` is identical at
both tags and the generation_config mechanism is byte-identical. Two
things one layer down did, and this tree takes both defaults:

  * cascade attention flipped on -> off at 0.18.0 (#36318), which vLLM
    made opt-in expressly "to avoid potential numerical issues";
  * the top-k/top-p sampler moved to a Triton kernel (0.17.0, ungated)
    and then to FlashInfer by default (0.21.0), whose output upstream
    documents as only "statistically equivalent".

The second reaches GUI-Owl but not OpenCUA, because GUI-Owl ships
top_k/top_p in its generation_config and OpenCUA does not.

Separately: vLLM shipped a ~30% relative Qwen3-VL accuracy regression at
0.20.0 and fixed it at 0.23.0, so both endpoints are clean but the
interior is not; and `vllm>=0.6` is unpinned while 0.24.0 removes the
transformers v4 path this lock depends on.

Docs-only. No behaviour change.

* docs(research): add the third exposure and name the deepstack regressor

Two additions from reading the remaining vision-tower commits, neither of
which the release notes surface as a numerics change.

  * #37948 (v0.19.0) replaces Qwen3-VL's bilinear position-embedding
    interpolation with a fused Triton kernel, collapsing ~25 eager ops
    into one. It is default-on for every Qwen3-VL variant, the PyTorch
    fallback applies only when Triton is missing, and there is no flag.
    That is a third default-path exposure for GUI-Owl, recorded as A3.
    Its magnitude is not quantified and the note says so.

  * #40145 (v0.20.0) is the change that introduced the deepstack
    regression #43617 later fixed -- it added the
    `deepstack_input_embeds_num_tokens` tracking whose zero-check became
    the faulty early return. It shipped as an optimisation, which is why
    it survived four releases.

Also records the triage boundary: 30 commits touched qwen3_vl.py and 14
touched qwen2_5_vl.py in the window; the accuracy/kernel ones were read,
the rest classified from titles. That is a judgement call, not a proof.

* docs(research): close the ViT cudagraph doubt, open the Inductor one

Resolves one gap and records a larger one.

Closed: the Qwen2.5-VL encoder-cudagraph cluster (#40830, #41234, #42288,
#42796) is gated behind `cudagraph_mm_encoder`, and that plus
`compile_mm_encoder`, `video_pruning_rate` and `mm_encoder_attn_backend`
are all off/None at both tags. The #42796 padding change this note
previously left unresolved is therefore unreachable, as is the FP8 ViT
work and every EVS fix.

Still open, and moved into B7: the ViT attention backend *selector*
changed from a hardcoded two-way choice to an ordered four-way scan.
FLASH_ATTN is tried first in both, so it is probably a no-op on A100/H100
-- but the fallback diverged (TORCH_SDPA -> TRITON_ATTN) and whether
FlashAttention is accepted in the deployed image is not verifiable from
source.

New A4: `vllm serve` compiles by default at both tags (`optimization_level
= O2` -> `VLLM_COMPILE`), so the torch 2.9.1 -> 2.11.0 move runs through
two different Inductor versions. #172's clearance covered torch's
documented defaults -- dtype, TF32, RNG, determinism -- and holds there;
it does not cover codegen. Recorded as an unquantified lever, not a
finding, and the recommendation now asks for an end-to-end 0.16.0 vs
0.23.0 comparison rather than only the sampler ablation.

Also records that the text decoder was never enumerated. That is a real
gap: the decoder does most of the arithmetic.

* docs(research): order the finding sections A, B, C, D

The 'changed and changed back' section sat between B and C because it was
written last. No content change.
ashtonchew added a commit that referenced this pull request Aug 18, 2026
`vllm>=0.6` bounded nothing, which is how an unreviewed 0.16.0 -> 0.23.0
move rode in on the lockfile fork collapse in #172.

Measured on 8804de4, `uv lock --upgrade` with the unbounded spec resolves
*backwards* to vllm 0.20.2 -- inside the Qwen3-VL deepstack regression
window (introduced #40145 at 0.20.0, fixed #43617 at 0.23.0). It cannot go
forward: 0.24.0 requires transformers>=5.5.3 against a lock at 4.57.5, and
0.27.1 pins torch==2.13.0 against this tree's 2.11.0.

transformers<5 is separate: 0.16.0 carried that ceiling, 0.23.0 dropped it
and admits >=5.5.1, and v5 stops OpenCUA at startup. Upstream fix #47438
is still a draft.

No package versions move; the lock records only the declared requirements.
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.

1 participant