From e89df9ac6b736136c1a3e323c9d30cc20c2a5c03 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Wed, 29 Jul 2026 09:50:15 +0000 Subject: [PATCH 1/4] build(deps): port worker to transformers 5.14.1 1 of 2. The docs-only follow-up (PR 2) refreshes the prose this PR leaves stale; see the note at the end. v5 unified the three eager causal seams a2d patched. GPT-2 no longer bakes causality into a per-layer `self.bias` buffer, the RoPE family no longer builds its mask in a per-model `_update_causal_mask` method, and Gemma 2/3 no longer apply their sliding window inside each local decoder layer's forward. Every family now routes causality through `transformers.masking_utils`, which builds the 4D additive mask via `ALL_MASK_ATTENTION_FUNCTIONS[config._attn_implementation]`. Re-target all three onto that one seam. `install_mask_anneal` registers a mask function (and the matching attention function) under a key private to its `AnnealState` and points only that model's config at it, so isolation is per model rather than per process - the D13 identity gate's un-patched reference copy keeps HF's own causal mask. The registered function calls HF's `eager_mask` and re-reveals every cell that mask masked for a real, non-padded key, so alpha=0 is bit-identical to base by construction and alpha=1 opens the future, Mistral's single-mask window and Gemma 2/3's separate sliding mask alike. `attn.full` / `attn.gqa` / `attn.swa` stay distinct capabilities (detect's contract, the handler registry) but are now three structural gates on one install: `resolve_capabilities` reads `config.layer_types` for `sliding_attention`, then `num_key_value_heads`, then `GPT2Attention`. A registry key that HF silently ignores would leave the model fully causal at every alpha, so add `test_install_routes_the_model_and_only_it_through_the_annealed_seam` asserting the key is live in `ALL_MASK_ATTENTION_FUNCTIONS`, that a sibling model stays on `eager`, and that only the patched one opens at alpha=1. Also: `from_pretrained(torch_dtype=)` -> `dtype=`. KNOWN-STALE PROSE, fixed in PR 2: module docstrings and comments in transform/attention.py, transform/gqa_attention.py, transform/swa_attention.py, transform/apply.py, transform/handlers/{gqa,swa}_attention.py, worker.py, tests/conftest.py, tests/test_{gqa,swa}_attention.py, the root pyproject.toml mypy override comment and AGENTS.md still describe 4.x internals (`_update_causal_mask`, `self.bias`, per-layer `is_sliding`). The code is the truth in this PR; those docstrings are not. --- packages/a2d-worker-hf/pyproject.toml | 9 +- .../src/a2d_core/transform/apply.py | 8 +- .../src/a2d_core/transform/attention.py | 145 +++++----- .../src/a2d_core/transform/gqa_attention.py | 103 +------ .../src/a2d_core/transform/swa_attention.py | 130 +-------- packages/a2d-worker-hf/tests/test_anneal.py | 25 +- packages/a2d-worker-hf/tests/test_bidir.py | 33 +++ .../a2d-worker-hf/tests/test_gqa_attention.py | 4 +- .../a2d-worker-hf/tests/test_smoke_convert.py | 8 +- .../a2d-worker-hf/tests/test_swa_attention.py | 18 +- uv.lock | 271 +++++++++--------- 11 files changed, 307 insertions(+), 447 deletions(-) diff --git a/packages/a2d-worker-hf/pyproject.toml b/packages/a2d-worker-hf/pyproject.toml index f9850a1..f1d65b3 100644 --- a/packages/a2d-worker-hf/pyproject.toml +++ b/packages/a2d-worker-hf/pyproject.toml @@ -6,10 +6,11 @@ dependencies = [ "a2d-contracts", "pydantic>=2", "torch", - # Pinned: Decision 2 patches HF's eager attention seams directly. 4.51.3 is the - # first pin that ships Gemma 3 (`gemma3_text`); the GPT-2 `eager_attention_forward` - # signature and the Gemma 1 `_update_causal_mask` seam are unchanged from 4.48.3. - "transformers==4.51.3", + # Pinned: Decision 2 patches HF's eager attention seams directly, so a minor bump + # can silently move them. v5 replaced the per-model `_update_causal_mask` method + # and GPT-2's `self.bias` buffer with the `masking_utils` mask-interface; see + # `transform/gqa_attention.py` and `transform/attention.py` for the v5 seams. + "transformers==5.14.1", "accelerate", "safetensors", ] diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/apply.py b/packages/a2d-worker-hf/src/a2d_core/transform/apply.py index 26fac79..3c60f8f 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/apply.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/apply.py @@ -13,7 +13,7 @@ from typing import Any from a2d_core.transform.attention import AnnealState -from a2d_core.transform.gqa_attention import _find_causal_mask_owner +from a2d_core.transform.gqa_attention import is_rope_family from a2d_core.transform.handlers import TRANSFORM from a2d_core.transform.swa_attention import has_sliding_window_seam @@ -29,7 +29,7 @@ def load_model(model_dir: str | Path, dtype: str = "float32") -> tuple[Any, Any] from a2d_core.device import select_dtype model = AutoModelForCausalLM.from_pretrained( - str(model_dir), attn_implementation="eager", torch_dtype=select_dtype(dtype) + str(model_dir), attn_implementation="eager", dtype=select_dtype(dtype) ).eval() tokenizer = AutoTokenizer.from_pretrained(str(model_dir)) return model, tokenizer @@ -87,13 +87,13 @@ def resolve_capabilities(model: Any) -> list[str]: """ if has_sliding_window_seam(model): return ["attn.swa"] - if _find_causal_mask_owner(model) is not None: + if is_rope_family(model): return ["attn.gqa"] if any(type(m).__name__ == "GPT2Attention" for m in model.modules()): return ["attn.full"] raise ValueError( "no supported attention seam found on model " - f"{type(model).__name__!r} (neither _update_causal_mask nor GPT2Attention)" + f"{type(model).__name__!r} (neither RoPE-family nor GPT2Attention)" ) diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/attention.py index d6d0ae5..dea8b6c 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/attention.py @@ -31,11 +31,16 @@ from __future__ import annotations import math +import sys from dataclasses import dataclass from typing import Any import torch +# Prefix of the per-``AnnealState`` implementation key registered in HF's attention and +# mask registries; also how an already-installed model is recognised on re-install. +_KEY_PREFIX = "a2d_annealed_eager_" + @dataclass class AnnealState: @@ -59,97 +64,81 @@ def schedule(step: int, anneal_steps: int, kind: str = "linear") -> float: return max(0.0, min(step / anneal_steps, 1.0)) -def annealed_additive_mask( - q_len: int, k_len: int, alpha: float, dtype: torch.dtype, device: torch.device -) -> torch.Tensor: - """Build the ``[q_len, k_len]`` additive pre-softmax mask for one attention call. +def _reveal_penalty(alpha: float, dtype: torch.dtype) -> float: + """Additive pre-softmax penalty for a revealed masked cell at this ``alpha``. - ``0`` on/below the causal diagonal, ``clamp(log(alpha), finfo.min)`` strictly - above it. At ``alpha=0`` the future penalty is exactly ``finfo(dtype).min`` so - ``score + penalty`` reproduces base's ``torch.where``-to-``finfo.min`` to the bit. + ``finfo(dtype).min`` at ``alpha<=0`` (matches the base mask exactly, so the identity + gate is bit-identical); ``log(alpha)`` clamped to ``finfo.min`` otherwise; ``0`` at + ``alpha=1`` (fully bidirectional). """ finfo_min = torch.finfo(dtype).min - # Match base's slice (module.bias[:, :, k-q:k, :k]) so the offset/cached q Any: - """Drop-in for ``eager_attention_forward``: inject the annealed additive mask. - - Only acts on modules tagged with ``_a2d_anneal`` (whose ``self.bias`` we have - neutralized to all-True); every other module delegates unchanged, so a base - reference model sharing this process keeps genuine causal attention. - """ - state: AnnealState | None = getattr(module, "_a2d_anneal", None) - if state is None: - return _original_eager( - module, query, key, value, attention_mask, head_mask=head_mask, **kwargs - ) - add = annealed_additive_mask( - query.size(-2), key.size(-2), state.alpha, query.dtype, query.device - ) - if attention_mask is not None: - # Fold any real (padding) mask in; base adds it after its where, we add once. - add = add + attention_mask - return _original_eager(module, query, key, value, add, head_mask=head_mask, **kwargs) + if alpha <= 0.0: + return finfo_min + return max(math.log(alpha), finfo_min) -def _ensure_global_patch() -> None: - """Install the process-global eager-attention replacement exactly once.""" - global _original_eager - if _original_eager is not None: - return - import transformers.models.gpt2.modeling_gpt2 as modeling_gpt2 +def annealed_eager_mask(state: AnnealState, **kwargs: Any) -> Any: + """``ALL_MASK_ATTENTION_FUNCTIONS`` entry: HF's eager mask with the annealed reveal. - _original_eager = modeling_gpt2.eager_attention_forward - modeling_gpt2.eager_attention_forward = _patched_eager + Works for every mask flavour built through this interface - the causal mask, and the + sliding-window mask Gemma 2/3 and Mistral additionally request - because both arrive + here as a 4D additive mask whose masked cells are exactly ``finfo(dtype).min``. + """ + from transformers.masking_utils import eager_mask + + base = eager_mask(**kwargs) + if base is None: + return None + dtype = base.dtype + reveal = base == torch.finfo(dtype).min + # Preserve genuine padding: only reveal a masked cell whose key is a real token, so + # a padded key stays finfo.min at every alpha. + padding = kwargs.get("attention_mask") + if isinstance(padding, torch.Tensor) and padding.dim() == 2: + reveal = reveal & (padding != 0)[:, None, None, : base.shape[-1]] + penalty = torch.full((), _reveal_penalty(state.alpha, dtype), dtype=dtype, device=base.device) + return torch.where(reveal, penalty, base) + + +def _a2d_eager_attention(module: Any, *args: Any, **kwargs: Any) -> Any: + """``ALL_ATTENTION_FUNCTIONS`` entry: the attention module's own eager forward. + + Both registries are keyed by ``config._attn_implementation``, so pointing a model at + a custom key also moves it off HF's built-in ``"eager"`` attention path. Resolving + ``eager_attention_forward`` from the module that defines the attention class keeps + each family's own eager maths (Gemma 2 logit softcapping, GPT-2's attention scaling) + exactly as base computes it - only the additive mask is ours. + """ + return sys.modules[type(module).__module__].eager_attention_forward(module, *args, **kwargs) -def install_anneal_patch(model: Any, state: AnnealState) -> None: - """Route ``model``'s eager attention through the annealed additive mask. +def install_mask_anneal(model: Any, state: AnnealState) -> None: + """Route ``model``'s mask construction through the annealed reveal. - Requires ``attn_implementation="eager"`` (Decision 2); forces ``use_cache=False``, - neutralizes each ``GPT2Attention``'s causal ``self.bias`` (all-True) so only the - additive mask governs causality, and tags each attention module with the shared - ``state``. + Requires ``attn_implementation="eager"`` (Decision 2); forces ``use_cache=False`` + (diffusion decodes the full canvas, ARCHITECTURE.md §7). The implementation key is + derived from ``state``, which the registry entry keeps alive, so the key is stable + and unique per anneal state: re-installing the same state is idempotent, while + installing a fresh state swaps in a new key rather than being silently ignored. """ - from transformers.models.gpt2.modeling_gpt2 import GPT2Attention + from transformers import AttentionInterface + from transformers.masking_utils import AttentionMaskInterface - if model.config._attn_implementation != "eager": + implementation = model.config._attn_implementation + if implementation != "eager" and not implementation.startswith(_KEY_PREFIX): raise ValueError( "anneal patch requires attn_implementation='eager', got " - f"{model.config._attn_implementation!r} (Decision 2)" + f"{implementation!r} (Decision 2)" ) + key = f"{_KEY_PREFIX}{id(state)}" + AttentionMaskInterface.register(key, lambda **kwargs: annealed_eager_mask(state, **kwargs)) + AttentionInterface.register(key, _a2d_eager_attention) model.config.use_cache = False - patched_any = False - for module in model.modules(): - if isinstance(module, GPT2Attention): - n = module.bias.shape[-1] - module.bias = torch.ones((1, 1, n, n), dtype=torch.bool, device=module.bias.device) - module._a2d_anneal = state # dynamic tag read back in _patched_eager - patched_any = True - if not patched_any: + model.set_attn_implementation(key) + + +def install_anneal_patch(model: Any, state: AnnealState) -> None: + """``attn.full`` install: the shared mask anneal, gated on GPT-2's dense attention.""" + if not any(type(m).__name__ == "GPT2Attention" for m in model.modules()): raise ValueError("anneal patch found no GPT2Attention modules (not a GPT-2 eager model?)") - _ensure_global_patch() + install_mask_anneal(model, state) diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py index 85885c5..e19840b 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py @@ -39,104 +39,27 @@ from __future__ import annotations -import math -import types from typing import Any -import torch +from a2d_core.transform.attention import AnnealState, install_mask_anneal -from a2d_core.transform.attention import AnnealState -# Sentinel stashing the original bound ``_update_causal_mask`` on the decoder module -# once patched; also guards against double-wrapping (which would capture our own -# wrapper as the "original" and anneal twice). -_ORIG_ATTR = "_a2d_gqa_orig_update_causal_mask" +def is_rope_family(model: Any) -> bool: + """True iff ``model`` is a RoPE-family decoder (the ``attn.gqa`` structural signal). -# The live ``AnnealState`` on the decoder module, read by the wrapper on every call -# and re-assigned by every install (parity with the GPT-2 seam's ``_a2d_anneal`` tag), -# so re-installing with a fresh state swaps it in instead of being silently ignored. -_STATE_ATTR = "_a2d_anneal" - - -def _reveal_penalty(alpha: float, dtype: torch.dtype) -> float: - """Additive pre-softmax penalty for a revealed masked cell at this ``alpha``. - - ``finfo(dtype).min`` at ``alpha<=0`` (matches the base causal mask exactly, so the - identity gate is bit-identical); ``log(alpha)`` clamped to ``finfo.min`` otherwise; - ``0`` at ``alpha=1`` (fully bidirectional). - """ - finfo_min = torch.finfo(dtype).min - if alpha <= 0.0: - return finfo_min - return max(math.log(alpha), finfo_min) - - -def _find_causal_mask_owner(model: Any) -> Any | None: - """The decoder submodule (the ``*Model``) that defines ``_update_causal_mask``. - - For ``*ForCausalLM`` this is ``model.model``; found structurally (by the class - that actually defines the method) so no family module names are hardcoded. + Keyed off ``num_key_value_heads``, which every model in this family declares and + GPT-2 does not - the same attribute detect reads to emit the capability - so no + family module names are hardcoded. MQA, GQA and full-attention variants all declare + it, which is exactly the set this seam covers. """ - for module in model.modules(): - if type(module).__dict__.get("_update_causal_mask") is not None: - return module - return None + return getattr(model.config, "num_key_value_heads", None) is not None def install_gqa_anneal_patch(model: Any, state: AnnealState) -> None: - """Route ``model``'s causal-mask construction through the annealed reveal. - - Requires ``attn_implementation="eager"`` (Decision 2, parity with the GPT-2 seam); - forces ``use_cache=False`` (diffusion decodes the full canvas, ARCHITECTURE.md §7); - wraps the decoder's ``_update_causal_mask`` and tags the owner with ``state``, - re-assigned on every install so a later install swaps in its fresh state. - """ - if model.config._attn_implementation != "eager": - raise ValueError( - "anneal patch requires attn_implementation='eager', got " - f"{model.config._attn_implementation!r} (Decision 2)" - ) - owner = _find_causal_mask_owner(model) - if owner is None: + """``attn.gqa`` install: the shared mask anneal, gated on the RoPE-family signal.""" + if not is_rope_family(model): raise ValueError( - "GQA anneal patch found no _update_causal_mask seam (not a RoPE-family causal model?)" + "GQA anneal patch found no RoPE-family causal mask seam on " + f"{type(model).__name__!r} (no num_key_value_heads)" ) - model.config.use_cache = False - setattr(owner, _STATE_ATTR, state) - - if hasattr(owner, _ORIG_ATTR): - return # already wrapped; wrapping again would double-anneal - orig = owner._update_causal_mask # bound original, captured before we shadow it - setattr(owner, _ORIG_ATTR, orig) - - def _annealed_update_causal_mask( - self: Any, - attention_mask: Any, - input_tensor: Any, - cache_position: Any, - *args: Any, - **kwargs: Any, - ) -> Any: - base_mask = orig(attention_mask, input_tensor, cache_position, *args, **kwargs) - # Eager always returns a 4D additive mask; None only appears on sdpa/flash - # fast paths we never take. Nothing to anneal if it is absent. - if base_mask is None: - return None - dtype = base_mask.dtype - kv_len = base_mask.shape[-1] - # Every cell the base mask masked (exactly finfo.min): the strictly-future - # causal cells, plus - for single-mask windowed families like Mistral v0.1 or - # Qwen2 with an active sliding window - the far-past out-of-window cells. - reveal = base_mask == torch.finfo(dtype).min - # Preserve genuine padding: only reveal a masked cell whose key is real, so a - # padded key stays finfo.min at every alpha (parity with the GPT-2 seam). - if isinstance(attention_mask, torch.Tensor) and attention_mask.dim() == 2: - real_key = (attention_mask != 0)[:, None, None, :kv_len] - reveal = reveal & real_key - live_state: AnnealState = getattr(self, _STATE_ATTR) - penalty = torch.full( - (), _reveal_penalty(live_state.alpha, dtype), dtype=dtype, device=base_mask.device - ) - return torch.where(reveal, penalty, base_mask) - - owner._update_causal_mask = types.MethodType(_annealed_update_causal_mask, owner) + install_mask_anneal(model, state) diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py index 103b7eb..6379736 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py @@ -42,144 +42,30 @@ from __future__ import annotations -import inspect -import types -from collections.abc import Iterator from typing import Any -import torch - -from a2d_core.transform.attention import AnnealState -from a2d_core.transform.gqa_attention import _reveal_penalty, install_gqa_anneal_patch - -# Stash of the original bound decoder-layer ``forward`` (guards double-wrapping) and -# the live ``AnnealState`` tag read by the wrapper, re-assigned on every install so a -# fresh state swaps in (parity with the gqa/GPT-2 seams' per-install re-tag). -_ORIG_ATTR = "_a2d_swa_orig_forward" -_STATE_ATTR = "_a2d_anneal" - - -def _iter_sliding_decoder_layers(model: Any) -> Iterator[Any]: - """Yield the decoder-layer modules that window their attention. - - Gemma 2/3 tag BOTH the decoder layer and its attention submodule with - ``is_sliding``; only the decoder layer owns the per-layer window re-mask in its - ``forward``, and it is the one that also holds a ``self_attn`` submodule - so we - key off that pair. Found structurally, no family module names hardcoded. - """ - for module in model.modules(): - if getattr(module, "is_sliding", False) and hasattr(module, "self_attn"): - yield module +from a2d_core.transform.attention import AnnealState, install_mask_anneal def has_sliding_window_seam(model: Any) -> bool: - """True iff the model has at least one sliding-window (local) decoder layer. + """True iff at least one decoder layer takes the sliding-window mask. This is the ``attn.swa`` structural signal ``resolve_capabilities`` dispatches on, - mirroring how ``_find_causal_mask_owner`` signals ``attn.gqa``. + read from ``config.layer_types`` - v5's per-layer mask selector - so no family + module names are hardcoded. Mistral-style models fold their window into the single + model-level mask and declare no ``layer_types``; they are the ``attn.gqa`` seam. """ - return next(_iter_sliding_decoder_layers(model), None) is not None - - -def _anneal_window( - layer: Any, attention_mask: Any, cache_position: Any, last_cache_position: int -) -> Any: - """Mirror the Gemma 2/3 decoder layer's sliding re-mask but reveal the far-past - per alpha. - - Reproduces HF's exact eager (4D) window logic (identical in both families) - the - ``tril(diagonal=-window)`` far-past selection and the - ``[offset : offset + effective_seq_len]`` slice - only swapping the hard - ``finfo(dtype).min`` fill for the annealed penalty. - - The reveal is restricted to far-past cells that are currently *attendable* (mask - ``== 0``), i.e. real past keys. Cells already at ``finfo.min`` are keys the base - mask masked for another reason - padding folded in by ``_update_causal_mask``, or - the future-reveal patch leaving a padded key masked - and must STAY masked at every - alpha (parity with the GPT-2/GQA seams' padding preservation). Since the far-past - region lives strictly below the diagonal it never overlaps the future cells the - first patch anneals, so ``== 0`` cleanly separates real past keys from padded ones. - - At ``alpha=0`` the penalty IS ``finfo.min``: real far-past cells go to ``finfo.min`` - (windowed) and padded ones are untouched (already ``finfo.min``), which is exactly - HF's ``where(sliding_window_mask, min_dtype, mask)`` - bit-identical to base. At - ``alpha=1`` the penalty is ``0``, opening the window for real keys while padding - stays masked. - """ - if attention_mask is None: - return attention_mask - dtype = attention_mask.dtype - effective_seq_len = max(cache_position.shape[0], layer.sliding_window) - far_past = torch.tril( - torch.ones_like(attention_mask, dtype=torch.bool), diagonal=-layer.sliding_window - ) - reveal = far_past & (attention_mask == 0) # real (attendable) far-past keys only - state: AnnealState = getattr(layer, _STATE_ATTR) - penalty = torch.full( - (), _reveal_penalty(state.alpha, dtype), dtype=dtype, device=attention_mask.device - ) - attention_mask = torch.where(reveal, penalty, attention_mask) - offset = max(0, last_cache_position - effective_seq_len) - return attention_mask[:, :, :, offset : offset + effective_seq_len] - - -def _make_wrapped_forward(orig: Any) -> Any: - """Build the decoder-layer ``forward`` replacement bound around ``orig``. - - Signature-agnostic: the call is re-bound against ``orig``'s own signature (Gemma 2 - takes one ``position_embeddings`` pair where Gemma 3 takes a global/local pair), - so it works whether the layer is called with keywords (the model's own forward) or - positionally (gradient checkpointing). Only ``attention_mask`` is replaced - with - the annealed window mask - and every other argument passes through unchanged. - ``orig`` then runs with ``is_sliding`` temporarily off so HF does not re-apply its - own hard window mask. - """ - signature = inspect.signature(orig) - - def _wrapped(self: Any, *args: Any, **kwargs: Any) -> Any: - bound = signature.bind(*args, **kwargs) - bound.apply_defaults() - bound.arguments["attention_mask"] = _anneal_window( - self, - bound.arguments.get("attention_mask"), - bound.arguments.get("cache_position"), - int(bound.arguments.get("last_cache_position") or 0), - ) - was_sliding = self.is_sliding - self.is_sliding = False # skip HF's hard finfo.min window re-mask for this call - try: - return orig(*bound.args, **bound.kwargs) - finally: - self.is_sliding = was_sliding - - return _wrapped + return "sliding_attention" in (getattr(model.config, "layer_types", None) or ()) def install_swa_anneal_patch(model: Any, state: AnnealState) -> None: - """Route ``model``'s causal+sliding-window masks through the annealed reveal. + """``attn.swa`` install: the shared mask anneal, gated on sliding-window layers. - Two coordinated patches on one shared ``state``: the RoPE/GQA future-reveal on the - single full causal mask (reused from ``install_gqa_anneal_patch`` - this also - enforces ``attn_implementation='eager'`` and ``use_cache=False`` and tags the mask - owner), plus a far-past reveal wrapped onto every sliding decoder layer's forward. The seam is validated up front so a failed install leaves the model untouched. - Re-installing swaps in the fresh state without double-wrapping. """ if not has_sliding_window_seam(model): raise ValueError( "SWA anneal patch found no sliding-window decoder layers " f"on {type(model).__name__!r} (not a Gemma 2/3 sliding-window model?)" ) - - # Patch 1: reveal strictly-future cells on the model-level causal mask (global - # layers + the base mask sliding layers receive). - install_gqa_anneal_patch(model, state) - - # Patch 2: reveal the strictly-far-past on each sliding (local) decoder layer. - for layer in _iter_sliding_decoder_layers(model): - setattr(layer, _STATE_ATTR, state) # live state; re-install swaps it in - if hasattr(layer, _ORIG_ATTR): - continue # already wrapped; wrapping again would double-anneal - orig = layer.forward # bound original, captured before we shadow it - setattr(layer, _ORIG_ATTR, orig) - layer.forward = types.MethodType(_make_wrapped_forward(orig), layer) + install_mask_anneal(model, state) diff --git a/packages/a2d-worker-hf/tests/test_anneal.py b/packages/a2d-worker-hf/tests/test_anneal.py index a0917aa..76c9a67 100644 --- a/packages/a2d-worker-hf/tests/test_anneal.py +++ b/packages/a2d-worker-hf/tests/test_anneal.py @@ -4,7 +4,24 @@ import pytest import torch -from a2d_core.transform.attention import AnnealState, annealed_additive_mask, schedule +from a2d_core.transform.attention import AnnealState, annealed_eager_mask, schedule + + +def _mask(alpha: float, q: int = 5, k: int = 5) -> torch.Tensor: + """The annealed 4D mask HF's eager mask interface would hand every layer.""" + from transformers.masking_utils import causal_mask_function + + out: torch.Tensor = annealed_eager_mask( + AnnealState(alpha), + batch_size=1, + q_length=q, + kv_length=k, + mask_function=causal_mask_function, + attention_mask=None, + dtype=torch.float32, + device=torch.device("cpu"), + ) + return out[0, 0] def test_schedule_endpoints_and_monotone() -> None: @@ -32,7 +49,7 @@ def test_state_defaults_to_causal() -> None: def test_mask_at_alpha0_matches_causal_finfo_min() -> None: finfo_min = torch.finfo(torch.float32).min q = k = 5 - mask = annealed_additive_mask(q, k, 0.0, torch.float32, torch.device("cpu")) + mask = _mask(0.0, q, k) assert mask.shape == (q, k) causal = torch.tril(torch.ones(q, k, dtype=torch.bool)) # on/below diagonal == 0; strictly-future == finfo.min (the base causal pattern) @@ -41,13 +58,13 @@ def test_mask_at_alpha0_matches_causal_finfo_min() -> None: def test_mask_at_alpha1_is_fully_bidirectional() -> None: - mask = annealed_additive_mask(4, 4, 1.0, torch.float32, torch.device("cpu")) + mask = _mask(1.0, 4, 4) assert bool(torch.all(mask == 0.0)) def test_mask_intermediate_alpha_is_log_penalty() -> None: alpha = 0.3 - mask = annealed_additive_mask(3, 3, alpha, torch.float32, torch.device("cpu")) + mask = _mask(alpha, 3, 3) causal = torch.tril(torch.ones(3, 3, dtype=torch.bool)) assert bool(torch.all(mask[causal] == 0.0)) assert torch.allclose(mask[~causal], torch.tensor(math.log(alpha), dtype=torch.float32)) diff --git a/packages/a2d-worker-hf/tests/test_bidir.py b/packages/a2d-worker-hf/tests/test_bidir.py index 5a75754..fb6248d 100644 --- a/packages/a2d-worker-hf/tests/test_bidir.py +++ b/packages/a2d-worker-hf/tests/test_bidir.py @@ -39,3 +39,36 @@ def test_future_token_reaches_earlier_positions_only_when_bidirectional( bidir_earlier_perturbed = model(perturbed).logits[:, earlier_pos, :] assert not torch.equal(bidir_earlier, bidir_earlier_perturbed) assert float((bidir_earlier - bidir_earlier_perturbed).abs().max().item()) > 1e-6 + + +def test_install_routes_the_model_and_only_it_through_the_annealed_seam( + tiny_gpt2: Callable[..., Any], +) -> None: + """HOOK GUARD: the v5 seam is a registry key, so a silently-not-applied patch is the + dangerous failure mode - the model would keep HF's own causal mask and every alpha + would look like alpha=0. Assert the model really is routed through a2d's registered + mask function, and that a sibling model in the same process is NOT (the identity + gate's reference copy must stay causal). + """ + from transformers.masking_utils import ALL_MASK_ATTENTION_FUNCTIONS + + patched = tiny_gpt2(0) + sibling = tiny_gpt2(0) + state = AnnealState() + install_anneal_patch(patched, state) + + key = patched.config._attn_implementation + assert key.startswith("a2d_annealed_eager_") + assert key in ALL_MASK_ATTENTION_FUNCTIONS # HF accepted the key, not silently ignored + assert sibling.config._attn_implementation == "eager" + + ids = torch.randint(0, 64, (1, 8)) + perturbed = ids.clone() + perturbed[:, 6] = (perturbed[:, 6] + 1) % 64 + state.alpha = 1.0 + with torch.no_grad(): + for model, opens in ((patched, True), (sibling, False)): + shift = ( + (model(ids).logits[:, 2, :] - model(perturbed).logits[:, 2, :]).abs().max().item() + ) + assert (float(shift) > 1e-6) is opens diff --git a/packages/a2d-worker-hf/tests/test_gqa_attention.py b/packages/a2d-worker-hf/tests/test_gqa_attention.py index 4bef44f..51cad60 100644 --- a/packages/a2d-worker-hf/tests/test_gqa_attention.py +++ b/packages/a2d-worker-hf/tests/test_gqa_attention.py @@ -255,8 +255,8 @@ def test_mistral_windowed_patched_at_alpha0_is_bit_identical_to_base( base = tiny_mistral() patched = tiny_mistral() patched.load_state_dict(base.state_dict()) # guarantee identical weights - # Sanity: no per-layer sliding seam, so dispatch stays on the shared mask seam. - assert not any(getattr(m, "is_sliding", False) for m in patched.modules()) + # Sanity: no per-layer sliding mask, so dispatch stays on attn.gqa, not attn.swa. + assert getattr(patched.config, "layer_types", None) is None assert resolve_capabilities(patched) == ["attn.gqa"] state = AnnealState() install_gqa_anneal_patch(patched, state) diff --git a/packages/a2d-worker-hf/tests/test_smoke_convert.py b/packages/a2d-worker-hf/tests/test_smoke_convert.py index 57133bc..25c4a9f 100644 --- a/packages/a2d-worker-hf/tests/test_smoke_convert.py +++ b/packages/a2d-worker-hf/tests/test_smoke_convert.py @@ -77,12 +77,14 @@ def test_broken_patch_aborts_before_training( # A zero additive mask leaves the model fully bidirectional even at alpha=0, so # patched@0 != base and the identity gate MUST reject the patch (Decision 2). - def broken_mask(q_len: int, k_len: int, alpha: float, dtype: Any, device: Any) -> Any: + def broken_mask(state: Any, **kwargs: Any) -> Any: import torch + from transformers.masking_utils import eager_mask - return torch.zeros(q_len, k_len, dtype=dtype, device=device) + base = eager_mask(**kwargs) + return None if base is None else torch.zeros_like(base) - monkeypatch.setattr(attn, "annealed_additive_mask", broken_mask) + monkeypatch.setattr(attn, "annealed_eager_mask", broken_mask) monkeypatch.setattr(sys, "stdin", io.StringIO(convert_setup.build_job())) rc = worker.main() diff --git a/packages/a2d-worker-hf/tests/test_swa_attention.py b/packages/a2d-worker-hf/tests/test_swa_attention.py index 958ee02..60fcbee 100644 --- a/packages/a2d-worker-hf/tests/test_swa_attention.py +++ b/packages/a2d-worker-hf/tests/test_swa_attention.py @@ -27,6 +27,11 @@ from a2d_core.transform.swa_attention import install_swa_anneal_patch +def _sliding(model: Any) -> list[bool]: + """Which decoder layers take the sliding-window mask, per ``config.layer_types``.""" + return [kind == "sliding_attention" for kind in model.config.layer_types] + + def _shift( model: Any, state: AnnealState, ids: torch.Tensor, q: int, k: int, alpha: float ) -> float: @@ -49,7 +54,7 @@ def test_swa_patched_at_alpha0_is_bit_identical_to_base(tiny_gemma3: Callable[.. patched = tiny_gemma3() patched.load_state_dict(base.state_dict()) # guarantee identical weights # Sanity: the default stack really does mix local and global layers. - kinds = [layer.is_sliding for layer in patched.model.layers] + kinds = _sliding(patched) assert any(kinds) and not all(kinds), f"need both local and global layers, got {kinds}" base_vocab = int(base.config.vocab_size) @@ -89,7 +94,7 @@ def test_swa_opens_window_at_alpha1_not_alpha0(tiny_gemma3: Callable[..., Any]) the test is not vacuous).""" # 1 sliding layer (pattern 6 => layer 0 local), window 2: query 3 sees only cols {2, 3}. model = tiny_gemma3(num_hidden_layers=1, sliding_window=2, sliding_window_pattern=6) - assert all(layer.is_sliding for layer in model.model.layers) + assert all(_sliding(model)) state = AnnealState() install_swa_anneal_patch(model, state) ids = torch.randint(0, int(model.config.vocab_size), (1, 8)) @@ -203,7 +208,7 @@ def test_swa_gemma2_patched_at_alpha0_is_bit_identical_to_base( base = tiny_gemma2() patched = tiny_gemma2() patched.load_state_dict(base.state_dict()) # guarantee identical weights - kinds = [layer.is_sliding for layer in patched.model.layers] + kinds = _sliding(patched) assert any(kinds) and not all(kinds), f"need both local and global layers, got {kinds}" assert resolve_capabilities(patched) == ["attn.swa"] state = AnnealState() @@ -225,7 +230,7 @@ def test_swa_gemma2_opens_window_and_future_at_alpha1_not_alpha0( alpha=0 (non-vacuity), while a far-past out-of-window key AND a strictly-future key move them at alpha=1 only.""" model = tiny_gemma2(num_hidden_layers=1, sliding_window=2) - assert all(layer.is_sliding for layer in model.model.layers) + assert all(_sliding(model)) state = AnnealState() install_swa_anneal_patch(model, state) ids = torch.randint(0, int(model.config.vocab_size), (1, 8)) @@ -262,15 +267,14 @@ def test_swa_install_on_non_sliding_model_raises_and_leaves_model_unpatched( tiny_gqa: Callable[..., Any], ) -> None: """Atomic install: on a model with no sliding decoder layers the install must raise - BEFORE the shared mask patch runs, leaving use_cache and _update_causal_mask + BEFORE the shared mask patch runs, leaving use_cache and the attention implementation untouched.""" model = tiny_gqa("gemma", 0) with pytest.raises(ValueError, match="no sliding-window decoder layers"): install_swa_anneal_patch(model, AnnealState()) assert model.config.use_cache is True - assert "_update_causal_mask" not in vars(model.model) - assert not hasattr(model.model, "_a2d_gqa_orig_update_causal_mask") + assert model.config._attn_implementation == "eager" def test_swa_reinstall_with_fresh_state_takes_effect(tiny_gemma3: Callable[..., Any]) -> None: diff --git a/uv.lock b/uv.lock index 101428f..ac89f38 100644 --- a/uv.lock +++ b/uv.lock @@ -57,7 +57,7 @@ requires-dist = [ { name = "pydantic", specifier = ">=2" }, { name = "safetensors" }, { name = "torch", index = "https://download.pytorch.org/whl/cpu" }, - { name = "transformers", specifier = "==4.51.3" }, + { name = "transformers", specifier = "==5.14.1" }, ] [[package]] @@ -80,6 +80,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/db/253133d7e7cb40d3af384bb2f5c0b4a2b7fdcffbc95c688cc67a20a3c103/accelerate-1.14.0-py3-none-any.whl", hash = "sha256:e94390c2863b873be18f623f9df48a0d8fe5eff13ea7f1a00092b0a7904888c6", size = 389246, upload-time = "2026-06-11T13:45:50.477Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -89,6 +98,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + [[package]] name = "argcomplete" version = "3.7.0" @@ -185,95 +207,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] -[[package]] -name = "charset-normalizer" -version = "3.4.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, - { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, - { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, - { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, - { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, - { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, - { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, - { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, - { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, - { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, - { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, - { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, - { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, - { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, - { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, - { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, - { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, - { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, - { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, - { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, - { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, - { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, - { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, - { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, - { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, - { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, - { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, - { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, - { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, - { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, - { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, - { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, - { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, - { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, - { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, - { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, - { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, - { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, - { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, - { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, - { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, - { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, - { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, - { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, - { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, - { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, - { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, - { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, - { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, - { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, - { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, - { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, - { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, - { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, - { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, - { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, -] - [[package]] name = "click" version = "8.4.2" @@ -341,6 +274,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f8/5c/e226de133afd8bb267ec27eead9ae3d784b95b39a287ed404caab39a5f50/genson-1.3.0-py3-none-any.whl", hash = "sha256:468feccd00274cc7e4c09e84b08704270ba8d95232aa280f65b986139cec67f7", size = 21470, upload-time = "2024-05-15T22:08:47.056Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "hf-xet" version = "1.5.1" @@ -373,23 +315,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/fa/77453694888f03e5a8c8852d1514a0894d8e81c622d39edbaf308ea0dcf4/hf_xet-1.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e", size = 3855178, upload-time = "2026-06-08T23:02:52.452Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "huggingface-hub" -version = "0.36.2" +version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "click" }, { name = "filelock" }, { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, { name = "packaging" }, { name = "pyyaml" }, - { name = "requests" }, { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/50/db3771a6e4fad4bd28fb055d4363b51cb0ae98c1aa504b79d41fdcab5483/huggingface_hub-1.25.1.tar.gz", hash = "sha256:21129595ca7a753be479b319913e22cc8808361ac118bd76cc413db831b28a99", size = 928426, upload-time = "2026-07-27T09:24:10.117Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3f/21e816831c6d16f88a6c784974413fa0421ce8a5d04380c2666ed5b503e5/huggingface_hub-1.25.1-py3-none-any.whl", hash = "sha256:004d4e70350517e24c68a7dbb7dc5e40b2b6aefef8f94bf7a85f6f9835102ea5", size = 774909, upload-time = "2026-07-27T09:24:08.079Z" }, ] [[package]] @@ -519,6 +490,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/06/e6/42a475bfca683b0cd5366f6dd06580062b7e567bb8534d225c877c2f14f3/librt-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bca1472acbd473eff61059b4409f802c5a1bcb4cd0344d06f939df9c4c125d40", size = 104282, upload-time = "2026-06-30T16:14:09.29Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -593,6 +576,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "more-itertools" version = "11.1.0" @@ -1221,18 +1213,16 @@ wheels = [ ] [[package]] -name = "requests" -version = "2.34.2" +name = "rich" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, + { name = "markdown-it-py" }, + { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, ] [[package]] @@ -1293,6 +1283,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "sympy" version = "1.14.0" @@ -1307,27 +1306,28 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.21.4" +version = "0.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/2f/402986d0823f8d7ca139d969af2917fefaa9b947d1fb32f6168c509f2492/tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880", size = 351253, upload-time = "2025-07-28T15:48:54.325Z" } +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/98/c6/fdb6f72bf6454f52eb4a2510be7fb0f614e541a2554d6210e370d85efff4/tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133", size = 2863987, upload-time = "2025-07-28T15:48:44.877Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a6/28975479e35ddc751dc1ddc97b9b69bf7fcf074db31548aab37f8116674c/tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60", size = 2732457, upload-time = "2025-07-28T15:48:43.265Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8f/24f39d7b5c726b7b0be95dca04f344df278a3fe3a4deb15a975d194cbb32/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5", size = 3012624, upload-time = "2025-07-28T13:22:43.895Z" }, - { url = "https://files.pythonhosted.org/packages/58/47/26358925717687a58cb74d7a508de96649544fad5778f0cd9827398dc499/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6", size = 2939681, upload-time = "2025-07-28T13:22:47.499Z" }, - { url = "https://files.pythonhosted.org/packages/99/6f/cc300fea5db2ab5ddc2c8aea5757a27b89c84469899710c3aeddc1d39801/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9", size = 3247445, upload-time = "2025-07-28T15:48:39.711Z" }, - { url = "https://files.pythonhosted.org/packages/be/bf/98cb4b9c3c4afd8be89cfa6423704337dc20b73eb4180397a6e0d456c334/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732", size = 3428014, upload-time = "2025-07-28T13:22:49.569Z" }, - { url = "https://files.pythonhosted.org/packages/75/c7/96c1cc780e6ca7f01a57c13235dd05b7bc1c0f3588512ebe9d1331b5f5ae/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2", size = 3193197, upload-time = "2025-07-28T13:22:51.471Z" }, - { url = "https://files.pythonhosted.org/packages/f2/90/273b6c7ec78af547694eddeea9e05de771278bd20476525ab930cecaf7d8/tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff", size = 3115426, upload-time = "2025-07-28T15:48:41.439Z" }, - { url = "https://files.pythonhosted.org/packages/91/43/c640d5a07e95f1cf9d2c92501f20a25f179ac53a4f71e1489a3dcfcc67ee/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2", size = 9089127, upload-time = "2025-07-28T15:48:46.472Z" }, - { url = "https://files.pythonhosted.org/packages/44/a1/dd23edd6271d4dca788e5200a807b49ec3e6987815cd9d0a07ad9c96c7c2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78", size = 9055243, upload-time = "2025-07-28T15:48:48.539Z" }, - { url = "https://files.pythonhosted.org/packages/21/2b/b410d6e9021c4b7ddb57248304dc817c4d4970b73b6ee343674914701197/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b", size = 9298237, upload-time = "2025-07-28T15:48:50.443Z" }, - { url = "https://files.pythonhosted.org/packages/b7/0a/42348c995c67e2e6e5c89ffb9cfd68507cbaeb84ff39c49ee6e0a6dd0fd2/tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24", size = 9461980, upload-time = "2025-07-28T15:48:52.325Z" }, - { url = "https://files.pythonhosted.org/packages/3d/d3/dacccd834404cd71b5c334882f3ba40331ad2120e69ded32cf5fda9a7436/tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0", size = 2329871, upload-time = "2025-07-28T15:48:56.841Z" }, - { url = "https://files.pythonhosted.org/packages/41/f2/fd673d979185f5dcbac4be7d09461cbb99751554ffb6718d0013af8604cb/tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597", size = 2507568, upload-time = "2025-07-28T15:48:55.456Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, ] [[package]] @@ -1419,24 +1419,23 @@ wheels = [ [[package]] name = "transformers" -version = "4.51.3" +version = "5.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock" }, { name = "huggingface-hub" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, { name = "pyyaml" }, { name = "regex" }, - { name = "requests" }, { name = "safetensors" }, { name = "tokenizers" }, { name = "tqdm" }, + { name = "typer" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/11/7414d5bc07690002ce4d7553602107bf969af85144bbd02830f9fb471236/transformers-4.51.3.tar.gz", hash = "sha256:e292fcab3990c6defe6328f0f7d2004283ca81a7a07b2de9a46d67fd81ea1409", size = 8941266, upload-time = "2025-04-14T08:15:00.485Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/fb/2a2ba88f325e68a921d8b69ff63b477830b2e73ade9a3c8c8cab2f06d741/transformers-5.14.1.tar.gz", hash = "sha256:60d196c27781eacf8637e2b533f517582907ad6f9ae142046d6b69431a5b2173", size = 9295927, upload-time = "2026-07-16T09:41:57.773Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/b6/5257d04ae327b44db31f15cce39e6020cc986333c715660b1315a9724d82/transformers-4.51.3-py3-none-any.whl", hash = "sha256:fd3279633ceb2b777013234bbf0b4f5c2d23c4626b05497691f00cfda55e8a83", size = 10383940, upload-time = "2025-04-14T08:13:43.023Z" }, + { url = "https://files.pythonhosted.org/packages/6f/67/8d85ca2323233ae3c0365a659c4e52ee1f587b440e4bc577e7d8e4416d0f/transformers-5.14.1-py3-none-any.whl", hash = "sha256:9db974c4079ede2d1a3ea7ca5a240df33f2cc26fc2b36ba64c5f2a4f43b6e725", size = 11625234, upload-time = "2026-07-16T09:41:54.143Z" }, ] [[package]] @@ -1451,6 +1450,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/29/74eeb4d3f3ae61ca096b018ad486b3b3c74b17bec09ab4edab721cbefec3/typeguard-4.5.2-py3-none-any.whl", hash = "sha256:fcf9de18bd945cdb4c7b996e12b4c51ce83f92f191314a6d7cf1739586ec98cf", size = 36748, upload-time = "2026-05-14T12:59:39.473Z" }, ] +[[package]] +name = "typer" +version = "0.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/78/fda3361b56efc27944f24225f6ecd13d96d6fcfe37bd0eb34e2f4c63f9fc/typer-0.27.0.tar.gz", hash = "sha256:629bd12ea5d13a17148125d9a264f949eb171fb3f120f9b04d85873cab054fa5", size = 203430, upload-time = "2026-07-15T19:21:07.007Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/03/26a383c9e58c213199d1aad1c3d353cfc22d4444ec6d2c0bf8ad02523843/typer-0.27.0-py3-none-any.whl", hash = "sha256:6f4b27631e47f077871b7dc30e933ec0131c1390fbe0e387ea5574b5bac9ccf1", size = 122716, upload-time = "2026-07-15T19:21:05.553Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -1471,12 +1485,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac wheels = [ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, -] From c40e42c3ec8cbcba4f3b4eb09e6d7fa3ca9e6e5d Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Wed, 29 Jul 2026 10:28:27 +0000 Subject: [PATCH 2/4] no-mistakes(review): verify anneal seam install and reject reorder_and_upcast_attn --- packages/a2d-worker-hf/pyproject.toml | 7 +++---- .../a2d-worker-hf/src/a2d_core/transform/attention.py | 5 +++++ packages/a2d-worker-hf/tests/conftest.py | 3 ++- packages/a2d-worker-hf/tests/test_bidir.py | 9 +++++++++ 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/a2d-worker-hf/pyproject.toml b/packages/a2d-worker-hf/pyproject.toml index f1d65b3..ca87514 100644 --- a/packages/a2d-worker-hf/pyproject.toml +++ b/packages/a2d-worker-hf/pyproject.toml @@ -6,10 +6,9 @@ dependencies = [ "a2d-contracts", "pydantic>=2", "torch", - # Pinned: Decision 2 patches HF's eager attention seams directly, so a minor bump - # can silently move them. v5 replaced the per-model `_update_causal_mask` method - # and GPT-2's `self.bias` buffer with the `masking_utils` mask-interface; see - # `transform/gqa_attention.py` and `transform/attention.py` for the v5 seams. + # Pinned: Decision 2 patches HF's eager attention seam directly, so a minor bump can + # silently move it. v5 replaced the per-model `_update_causal_mask` and GPT-2's + # `self.bias` with the `masking_utils` mask-interface; see `transform/attention.py`. "transformers==5.14.1", "accelerate", "safetensors", diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/attention.py index dea8b6c..c886561 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/attention.py @@ -135,10 +135,15 @@ def install_mask_anneal(model: Any, state: AnnealState) -> None: AttentionInterface.register(key, _a2d_eager_attention) model.config.use_cache = False model.set_attn_implementation(key) + if model.config._attn_implementation != key: + raise ValueError(f"transformers refused attn_implementation={key!r}: alpha stays causal") def install_anneal_patch(model: Any, state: AnnealState) -> None: """``attn.full`` install: the shared mask anneal, gated on GPT-2's dense attention.""" if not any(type(m).__name__ == "GPT2Attention" for m in model.modules()): raise ValueError("anneal patch found no GPT2Attention modules (not a GPT-2 eager model?)") + if getattr(model.config, "reorder_and_upcast_attn", False): + # That path survives only a literal "eager" key: base keeps it, patched would not. + raise ValueError("anneal patch does not support reorder_and_upcast_attn=True") install_mask_anneal(model, state) diff --git a/packages/a2d-worker-hf/tests/conftest.py b/packages/a2d-worker-hf/tests/conftest.py index ad7b5e1..393739e 100644 --- a/packages/a2d-worker-hf/tests/conftest.py +++ b/packages/a2d-worker-hf/tests/conftest.py @@ -16,7 +16,7 @@ def tiny_gpt2() -> Callable[..., Any]: """Factory for a seeded tiny GPT-2 (eager, eval, no download). Same seed => bit-identical weights, so two calls give an aligned base/patched pair.""" - def _make(seed: int = 0) -> Any: + def _make(seed: int = 0, **overrides: Any) -> Any: import torch from transformers import AutoModelForCausalLM, GPT2Config @@ -30,6 +30,7 @@ def _make(seed: int = 0) -> Any: resid_pdrop=0.0, embd_pdrop=0.0, attn_pdrop=0.0, + **overrides, ) return AutoModelForCausalLM.from_config(config, attn_implementation="eager").eval() diff --git a/packages/a2d-worker-hf/tests/test_bidir.py b/packages/a2d-worker-hf/tests/test_bidir.py index fb6248d..68137ce 100644 --- a/packages/a2d-worker-hf/tests/test_bidir.py +++ b/packages/a2d-worker-hf/tests/test_bidir.py @@ -3,6 +3,7 @@ from collections.abc import Callable from typing import Any +import pytest import torch from a2d_core.transform.attention import AnnealState, install_anneal_patch @@ -72,3 +73,11 @@ def test_install_routes_the_model_and_only_it_through_the_annealed_seam( (model(ids).logits[:, 2, :] - model(perturbed).logits[:, 2, :]).abs().max().item() ) assert (float(shift) > 1e-6) is opens + + +def test_install_rejects_reorder_and_upcast_attn(tiny_gpt2: Callable[..., Any]) -> None: + """GUARD: reject by name, not as a bare max_abs_diff failure in the D13 gate.""" + model = tiny_gpt2(0, reorder_and_upcast_attn=True) + with pytest.raises(ValueError, match="reorder_and_upcast_attn"): + install_anneal_patch(model, AnnealState()) + assert model.config._attn_implementation == "eager" From 9c434ac64ef3a6992f7a360da3c20fce5b7c443f Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Wed, 29 Jul 2026 11:22:15 +0000 Subject: [PATCH 3/4] no-mistakes(document): resync worker docs to transformers v5 mask-interface seam --- AGENTS.md | 4 +- docs/CONCEPTS.md | 2 +- docs/SPEC-HANDOFF.md | 8 +-- .../src/a2d_core/transform/apply.py | 23 ++++--- .../src/a2d_core/transform/attention.py | 66 +++++++++++-------- .../src/a2d_core/transform/gqa_attention.py | 53 ++++++--------- .../transform/handlers/full_attention.py | 9 ++- .../transform/handlers/gqa_attention.py | 10 +-- .../transform/handlers/swa_attention.py | 15 +++-- .../src/a2d_core/transform/identity.py | 6 +- .../src/a2d_core/transform/swa_attention.py | 57 ++++++---------- packages/a2d-worker-hf/src/a2d_core/worker.py | 13 ++-- packages/a2d-worker-hf/tests/conftest.py | 26 ++++---- .../a2d-worker-hf/tests/test_gqa_attention.py | 13 ++-- .../a2d-worker-hf/tests/test_swa_attention.py | 37 ++++++----- pyproject.toml | 11 ++-- 16 files changed, 175 insertions(+), 178 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ea83902..789d9e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,8 @@ This file is the project's committed home for project-intrinsic agent knowledge: - **What a2d is + the extension playbook:** `docs/SPEC-HANDOFF.md` (esp. §3 open-closed model, §5 repo layout) and `docs/ARCHITECTURE.md` (the ML recipe: mask annealing, MDLM, D13 identity gate). Adding a model/capability is additive by policy — one file per quirk, never edit an existing adapter/handler. - **Detect (Rust, config-only, no GPU):** `crates/a2d-detect/`. New `model_type` quirk = one file in `src/adapters/` delegating to `generic::detect` + `inferred=false`, one `mod` line in `adapters/mod.rs`, and one `fixtures/configs//{config.json,expected.json}` (auto-picked up by `tests/corpus.rs`). Capabilities are the unit of support, not model names; the gate blocks only conversion-blocking caps. -- **Convert handlers (Python worker):** `packages/a2d-worker-hf/src/a2d_core/transform/`. Three eager causal seams — GPT-2 bakes causality into per-layer `self.bias` (`attn.full`, `attention.py`); the RoPE family (Llama/Qwen2/Gemma 1) routes it through the 4D mask `_update_causal_mask` builds (`attn.gqa`, `gqa_attention.py` - the reveal opens EVERY base-masked real-key cell, so single-mask windowed families like Mistral v0.1 or Qwen2 with active `use_sliding_window` also unwindow through this seam); Gemma 2/3 add a per-layer sliding window on their *local* decoder layers on top of that same full mask (`attn.swa`, `swa_attention.py` - anneals BOTH the full-causal future-reveal AND each sliding layer's far-past window via a signature-agnostic layer-forward wrap; `resolve_capabilities` checks SWA before GQA because Gemma 2/3 own both seams). `worker.py` picks the handler by structural introspection (`resolve_capabilities` in `apply.py`), not from job tags — the `ConversionJob` carries no capability set. D13 rule: patched@`alpha=0` must equal base logits bit-for-bit. `attn.swa` is a SUPPORTED, non-blocking capability (`crates/a2d-contracts/src/lib.rs` `blocking()`); `attn.sink`/`attn.mla`/SSM still reject. -- **HF transformers is pinned to `4.51.3`** (`packages/a2d-worker-hf/pyproject.toml`), the first pin shipping Gemma 3; the GPT-2 eager seam and Gemma 1 `_update_causal_mask` are unchanged from the old `4.48.3`. Two gotchas: 4.51.3 ships an (empty) `py.typed`, so the root `pyproject.toml` mypy override keeps `transformers`/`tokenizers` untyped via `follow_imports = "skip"` (not just `ignore_missing_imports`); and in 4.51.3 Gemma 3's sliding window is applied per-decoder-layer in `Gemma3DecoderLayer.forward`, NOT via a model-level mask-mapping dict (that is a later-transformers refactor) - verify the seam before assuming. +- **Convert handlers (Python worker):** `packages/a2d-worker-hf/src/a2d_core/transform/`. One seam, three gates. Since transformers v5 every mask is built through `masking_utils.ALL_MASK_ATTENTION_FUNCTIONS`, keyed by `config._attn_implementation`, so `attention.py`'s `install_mask_anneal` registers one annealed mask function per `AnnealState` (key `a2d_annealed_eager_`, registered in the mask _and_ attention interfaces since one config value selects both) and points only that model's config at it — isolation is per model, not per process, which is what lets the D13 gate hold an un-patched sibling on `"eager"` in the same process. The reveal derives FROM HF's own `eager_mask` output instead of rebuilding the mask, so `alpha=0` is bit-identical by construction (any dtype, cache offset, padding) and genuine 2D padding stays masked at every alpha. `attn.full`/`attn.gqa`/`attn.swa` stay DISTINCT capabilities (contract shared with the Rust detect crate; the handler registry is keyed on them) and are now three structural gates on that one install, checked in this order by `resolve_capabilities` (`apply.py`): `config.layer_types` containing `sliding_attention` (`attn.swa`, Gemma 2/3 — first, because they also carry the gqa signal), then `num_key_value_heads` (`attn.gqa`, Llama/Qwen2/Gemma 1/Mistral), then `GPT2Attention` (`attn.full`). Mistral folds its window into the single model-level mask and declares no `layer_types`, so it correctly stays `attn.gqa` and still unwindows at `alpha=1`. `worker.py` picks the handler by structural introspection, not from job tags — the `ConversionJob` carries no capability set. D13 rule: patched@`alpha=0` must equal base logits bit-for-bit. Because the seam is a registry key rather than a monkeypatch, a silently-not-applied patch is now the dangerous failure mode; `tests/test_bidir.py::test_install_routes_the_model_and_only_it_through_the_annealed_seam` is the guard that the key is live and the sibling is untouched. `attn.swa` is a SUPPORTED, non-blocking capability (`crates/a2d-contracts/src/lib.rs` `blocking()`); `attn.sink`/`attn.mla`/SSM still reject. +- **HF transformers is pinned to `5.14.1`** (`packages/a2d-worker-hf/pyproject.toml`) because Decision 2 patches HF's mask seam directly, so even a minor bump can silently move it. Gotchas: v5 deleted all three 4.x seams (GPT-2's per-layer `self.bias`, the per-model `_update_causal_mask`, Gemma 2/3's per-decoder-layer window re-mask) in favour of the mask interface, so any 4.x note about them is obsolete; `from_pretrained` takes `dtype=`, not `torch_dtype=`; and 5.14.1 still ships an (empty) `py.typed`, so the root `pyproject.toml` mypy override keeps `transformers`/`tokenizers` untyped via `follow_imports = "skip"` (dropping it reintroduces 68 errors, because v5's inline annotations disagree with the runtime for `**kwargs` config fields and `Trainer.compute_loss`). - **Hermetic tests only:** never download weights (Gemma is gated; CI is CPU/no-network). Build tiny random-weight configs in-process (see `tests/conftest.py` `tiny_gpt2`/`tiny_gqa`/`tiny_gemma2`/`tiny_gemma3`/`tiny_mistral`). - **Full CI gate before shipping:** `cargo fmt --all --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`; `uv run ruff check .`, `uv run ruff format --check .`, `uv run mypy` (strict), `uv run pytest`; contracts are generated — after touching `crates/a2d-contracts/` run `bash scripts/codegen.sh` (CI fails on `schema/`+`packages/a2d-contracts/` drift). diff --git a/docs/CONCEPTS.md b/docs/CONCEPTS.md index 771e57c..f182cae 100644 --- a/docs/CONCEPTS.md +++ b/docs/CONCEPTS.md @@ -24,7 +24,7 @@ Everything else in the vocabulary exists to decide *which models a2d will touch* - **Bidirectional** - a token sees left *and* right. What diffusion needs. - **MDLM** - the masked-diffusion objective a2d uses: mask a random fraction, predict them, repeat. - **Canvas** - the span of positions a diffusion model denoises (its workspace). -- **Anneal (`attn.full`, `attn.gqa`, `attn.swa`)** - a2d's trick: slowly turn the causal mask off so AR weights adapt to bidirectional. One transform per attention seam: GPT-2 bakes causality per layer (`attn.full`); the RoPE family (Gemma 1 / Qwen2 / Llama / Mistral) routes it through one shared mask (`attn.gqa`); Gemma 2/3 add a per-layer sliding window on top of that mask (`attn.swa`). +- **Anneal (`attn.full`, `attn.gqa`, `attn.swa`)** - a2d's trick: slowly turn the causal mask off so AR weights adapt to bidirectional. One transform per attention shape, all annealing the same HF mask seam: GPT-2's dense causal mask (`attn.full`); the RoPE family (Gemma 1 / Qwen2 / Llama / Mistral), which routes causality through one model-level mask (`attn.gqa`); Gemma 2/3, which add a sliding-window mask on their local layers (`attn.swa`). - **Identity gate** - hard correctness check: at `anneal=0` the patched model must match the base model's logits, or convert aborts. - **GQA** - grouped-query attention: fewer key/value heads than query heads (a memory trick). The mechanism rides along in HF's forward; the `attn.gqa` tag also names the RoPE-family anneal transform (see Anneal). - **RoPE** - rotary position encoding (modern position scheme). Passthrough. diff --git a/docs/SPEC-HANDOFF.md b/docs/SPEC-HANDOFF.md index 2a17cec..88ade07 100644 --- a/docs/SPEC-HANDOFF.md +++ b/docs/SPEC-HANDOFF.md @@ -216,10 +216,10 @@ a2d/ src/a2d_core/ ingest/ # ◄ EXTENSION POINT: format normalizers (copy-on-normalize) transform/ - attention.py # GPT-2 self.bias seam (attn.full) + AnnealState/schedule - gqa_attention.py # RoPE-family _update_causal_mask seam (attn.gqa) - swa_attention.py # Gemma 2/3 per-layer sliding-window seam (attn.swa) - apply.py # load model, resolve capabilities from its seam, apply handlers + attention.py # shared HF mask-interface seam (attn.full) + AnnealState/schedule + gqa_attention.py # RoPE-family gate: num_key_value_heads (attn.gqa) + swa_attention.py # Gemma 2/3 gate: config.layer_types sliding layers (attn.swa) + apply.py # load model, resolve capabilities from its structure, apply handlers handlers/ # ◄ EXTENSION POINT: capability handlers (attn.full, attn.gqa, attn.swa, ffn.moe, …) objectives/ # ◄ EXTENSION POINT: mdlm.py, bd3lm.py (corrupt/loss iface) data/ # local corpus readers + streaming, packing, noising collators diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/apply.py b/packages/a2d-worker-hf/src/a2d_core/transform/apply.py index 3c60f8f..4d929cf 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/apply.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/apply.py @@ -69,16 +69,19 @@ def resolve_mask_token(model: Any, tokenizer: Any, strategy: str = "grow") -> in def resolve_capabilities(model: Any) -> list[str]: """The attention-handler capabilities a loaded model needs, chosen by its eager - causal seam (Decision 2). Three disjoint seams exist in this scope: - - - Gemma 2/3 route causality through ``_update_causal_mask`` AND add a per-layer - sliding window on their local decoder layers -> ``attn.swa``. Checked FIRST, - because these models also own ``_update_causal_mask`` (the ``attn.gqa`` signal); - the swa handler subsumes the gqa future-reveal and additionally opens the window. - - The RoPE family (Llama/Qwen2/Gemma 1) routes causality through the 4D mask that - ``_update_causal_mask`` builds -> ``attn.gqa`` (covers GQA, MQA, and full-attn - RoPE alike; the mask is family-independent). - - GPT-2 bakes causality into a per-layer ``self.bias`` buffer -> ``attn.full``. + causal seam (Decision 2). ``transformers`` v5 builds all of them through one mask + interface, but they stay three distinct capabilities - the set is a contract shared + with the Rust detect crate - so they are three structural gates on one install: + + - Gemma 2/3 label their layers in ``config.layer_types``, so some layers take a + sliding-window mask on top of causality -> ``attn.swa``. Checked FIRST, because + these models also carry the ``attn.gqa`` signal; the swa install opens both the + future and the window. + - The RoPE family (Llama/Qwen2/Gemma 1/Mistral) declares ``num_key_value_heads`` and + routes causality through one model-level 4D mask -> ``attn.gqa`` (covers GQA, MQA, + and full-attn RoPE alike; the mask is family-independent). + - GPT-2 declares neither and is recognised by its ``GPT2Attention`` modules -> + ``attn.full``. The seam is read from the model itself, not from detect's tags: the ``ConversionJob`` does not carry the capability set, so the worker independently picks the correct, diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/attention.py index c886561..5a3e226 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/attention.py @@ -1,31 +1,40 @@ -"""Annealed causal->bidirectional attention patch for GPT-2's eager seam (Decision 2). - -``transformers==4.48.3`` GPT-2 bakes causality INSIDE the attention op: eager -``GPT2Attention`` masks scores with a lower-triangular ``self.bias`` buffer via -``torch.where(causal, scores, finfo(float32).min)``; the passed 4D -``attention_mask`` is padding-only. A model-level additive mask therefore does -NOT control causality, so this module patches the seam that does. - -The patch neutralizes ``self.bias`` (registers it all-True so ``torch.where`` -never masks) and re-supplies a single annealed additive mask driven by one shared -``AnnealState``: on/below the diagonal it is ``0``; strictly-future (``j>i``) it is -``clamp(log(alpha), finfo(float32).min)``. - -* At ``alpha=0`` a future entry is ``finfo.min``, and a finite pre-softmax score is - negligible against it in float32 (``score + finfo.min == finfo.min`` to the bit), - so patched-at-0 scores/logits are bit-identical to base. -* At ``alpha=1`` the penalty is ``log(1)=0`` and, with ``self.bias`` neutralized, - attention is fully bidirectional; intermediate ``alpha`` scales each future - position's pre-softmax mass by ``alpha`` (a smooth monotone reveal). +"""Annealed causal->bidirectional attention: the shared mask seam (Decision 2). + +``transformers==5.14.1`` builds every attention mask through ONE documented extension +point - ``masking_utils.ALL_MASK_ATTENTION_FUNCTIONS``, keyed by +``config._attn_implementation``. Both ``create_causal_mask`` and +``create_sliding_window_causal_mask`` resolve their builder there, so a single registered +mask function covers all three seams a2d converts: GPT-2 (which no longer bakes causality +into a per-layer ``self.bias`` buffer), the RoPE family's one model-level causal mask, and +the per-layer sliding-window mask Gemma 2/3 request for their local layers. + +``install_mask_anneal`` registers ``annealed_eager_mask`` under a key derived from the +``AnnealState`` (``a2d_annealed_eager_``) and points only that model's config at +it. Isolation is therefore per model, not per process: the D13 identity gate holds an +un-patched sibling copy of the same model in the same process, and it stays on ``"eager"`` +with HF's own causal mask. The key is registered in BOTH registries (mask and attention) +because one config value selects both. Rationale for a registry key over the alternatives: +``PretrainedConfig`` defines ``__eq__`` without ``__hash__``, so a ``WeakKeyDictionary`` +keyed by config is impossible, and a ``config`` attribute holding the state breaks +``save_pretrained`` (not JSON-serializable). + +The reveal is derived FROM HF's own ``eager_mask`` output rather than rebuilt: every cell +the base mask masked (exactly ``finfo(dtype).min``) is re-scored to +``clamp(log(alpha), finfo.min)``. + +* At ``alpha=0`` the penalty IS ``finfo.min`` - the value the base mask already carries - + so the returned tensor is bit-identical to base and patched@0 logits equal base to the + bit (the D13 identity gate), by construction and independent of dtype, cache offset or + padding. +* At ``alpha=1`` the penalty is ``log(1)=0``, so attention is fully bidirectional AND + unwindowed; intermediate ``alpha`` scales each masked position's pre-softmax mass by + ``alpha`` (a smooth monotone reveal). + +Genuine 2D padding is preserved: a masked cell is only revealed when its key is a real +token, so a padded key stays ``finfo.min`` at every alpha. This deliberately replaces the ML-recon's ``(1-alpha)*(-inf) + alpha*0`` blend, which is ``-inf`` for every ``alpha<1`` and thus not an anneal at all (Risk 4). - -The eager path is patched at the module-global ``eager_attention_forward`` that -``GPT2Attention.forward`` resolves at call time. The replacement is inert for any -attention module without a ``_a2d_anneal`` tag, so an unpatched base model (the -identity gate's reference copy) keeps its original causal attention even though -the global is process-wide. """ from __future__ import annotations @@ -140,7 +149,12 @@ def install_mask_anneal(model: Any, state: AnnealState) -> None: def install_anneal_patch(model: Any, state: AnnealState) -> None: - """``attn.full`` install: the shared mask anneal, gated on GPT-2's dense attention.""" + """``attn.full`` install: the shared mask anneal, gated on GPT-2's dense attention. + + ``reorder_and_upcast_attn=True`` is rejected by name rather than left to fail as a bare + ``max_abs_diff`` in the D13 gate: that GPT-2 path is only taken under the literal + ``"eager"`` key, so base would keep it and the patched model would not. + """ if not any(type(m).__name__ == "GPT2Attention" for m in model.modules()): raise ValueError("anneal patch found no GPT2Attention modules (not a GPT-2 eager model?)") if getattr(model.config, "reorder_and_upcast_attn", False): diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py index e19840b..4c883a3 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py @@ -1,40 +1,25 @@ """Annealed causal->bidirectional attention for the RoPE/GQA family (Decision 2). -Llama / Qwen2 / Gemma (``transformers==4.51.3``) do NOT bake causality into a -per-layer ``self.bias`` buffer the way GPT-2 does. Instead the decoder ``*Model`` -builds one 4D additive causal mask per forward in ``_update_causal_mask`` and hands +Llama / Qwen2 / Gemma 1 build ONE model-level 4D additive causal mask per forward and hand it down to every layer; eager attention just does ``scores + causal_mask``. Causality -therefore flows entirely through that mask, so annealing it (not a ``self.bias`` seam) -is what opens attention here. This is the ONE seam shared verbatim by all three -families, so a single handler covers Gemma (MQA), Qwen2/Llama (GQA), and even -full-attention RoPE models (Llama-2-7B): the mask is family-independent, and the GQA -group expansion (``repeat_kv``), RoPE, RMSNorm, and Gemma's sqrt(hidden) embedding -scaling all stay untouched in HF's own forward. - -The patch wraps the decoder's bound ``_update_causal_mask``: it calls the original -to get the exact base mask, then re-reveals every cell that mask masked for a real -(non-padded) key under one shared ``AnnealState``. For the full-attention RoPE -family those cells are exactly the strictly-future set; Mistral v0.1 and Qwen2 with -an active ``use_sliding_window`` fold their sliding window into this SAME model-level -mask (they have no per-layer window), so their far-past out-of-window cells reopen -through the identical anneal and ``alpha=1`` is fully non-causal AND unwindowed: - -* At ``alpha=0`` the penalty is exactly ``finfo(dtype).min`` - the same value the - base mask already carries at masked cells - so ``torch.where(reveal, penalty, - base)`` returns a tensor bit-identical to base, and patched@0 logits equal base to - the bit (the D13 identity gate). -* At ``alpha=1`` the penalty is ``log(1)=0`` so masked cells become attendable and - attention is fully bidirectional; intermediate ``alpha`` applies ``log(alpha)`` (a - smooth monotone reveal), mirroring the GPT-2 seam's semantics. - -Deriving the result FROM the base mask (rather than rebuilding it) is what makes the -``alpha=0`` no-op bit-identical by construction, independent of dtype, cache offset, -or padding. A genuine 2D padding mask is preserved: a masked cell is only revealed -when its key is a real (non-padded) token, so padding stays masked at every alpha. - -The override is installed on the model INSTANCE (it shadows the class method via the -instance ``__dict__``), so a sibling un-patched base model in the same process - the -identity gate's reference copy - keeps its original causal attention. +flows entirely through that mask, and in ``transformers==5.14.1`` the mask is built by +whatever ``ALL_MASK_ATTENTION_FUNCTIONS`` holds for ``config._attn_implementation``. The +anneal itself is therefore the shared ``install_mask_anneal`` seam (see ``attention.py``), +and this module owns only the structural gate that recognises the family. + +One gate covers Gemma (MQA), Qwen2/Llama (GQA) and full-attention RoPE models (Llama-2-7B) +alike: the mask is family-independent, and the GQA group expansion (``repeat_kv``), RoPE, +RMSNorm and Gemma's sqrt(hidden) embedding scaling all stay untouched in HF's own forward. +It also covers the single-mask windowed flavors - Mistral v0.1, Qwen2 with an active +``use_sliding_window`` - which fold their window into that SAME model-level mask and +declare no ``config.layer_types``: the shared reveal opens EVERY base-masked real-key cell, +so their far-past out-of-window cells reopen through the identical anneal and ``alpha=1`` +is fully non-causal AND unwindowed. + +``attn.gqa`` stays a capability distinct from ``attn.full``/``attn.swa`` even though v5 +collapsed all three onto one seam: the capability set is a contract shared with the Rust +detect crate, and the handler registry is keyed on it. They are now three structural gates +on one install. """ from __future__ import annotations diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/full_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/full_attention.py index ac3e13e..08f33ce 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/full_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/full_attention.py @@ -1,8 +1,11 @@ """``attn.full`` transform: install the annealed bidirectional attention patch. -GPT-2 dense causal attention -> annealed causal->bidirectional via the eager-seam -patch (Decision 2). The ``alpha=0`` identity gate and ``test_bidir`` together prove -the patch is both bit-identical to base and genuinely reaches GPT-2's causality. +GPT-2 dense causal attention -> annealed causal->bidirectional via the shared +mask-interface seam (Decision 2), behind a ``GPT2Attention`` structural gate that also +rejects ``reorder_and_upcast_attn=True`` by name. The ``alpha=0`` identity gate and +``test_bidir`` together prove the patch is both bit-identical to base and genuinely +routed - the model's ``_attn_implementation`` really is a2d's registered key, and a +sibling model in the same process is not. """ from __future__ import annotations diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/gqa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/gqa_attention.py index 125c3fb..ffffe3c 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/gqa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/gqa_attention.py @@ -1,10 +1,10 @@ """``attn.gqa`` transform: annealed bidirectional attention for the RoPE/GQA family. -Llama / Qwen2 / Gemma route causality through the 4D mask that ``_update_causal_mask`` -builds (not GPT-2's per-layer ``self.bias``), so this handler installs the mask-seam -anneal patch. It covers GQA, MQA (Gemma's ``num_key_value_heads=1``), full-attention -RoPE models, and the single-mask windowed families (Mistral v0.1, Qwen2 with an active -sliding window) whose far-past window lives in that same mask, leaving RoPE, the +Llama / Qwen2 / Gemma route causality through one model-level 4D mask, so this handler +installs the shared mask-interface anneal seam behind the RoPE-family structural gate +(``num_key_value_heads``). It covers GQA, MQA (Gemma's ``num_key_value_heads=1``), +full-attention RoPE models, and the single-mask windowed families (Mistral v0.1, Qwen2 with +an active sliding window) whose far-past window lives in that same mask, leaving RoPE, the KV-group expansion, RMSNorm, and Gemma's embedding scaling to HF's own forward. The ``alpha=0`` identity gate and the ``alpha=1`` bidir test together prove the patch is bit-identical to base yet genuinely reaches future tokens. diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/swa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/swa_attention.py index 14391b8..54af2b5 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/swa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/swa_attention.py @@ -1,13 +1,14 @@ """``attn.swa`` transform: annealed bidirectional attention for the sliding-window Gemma family (Gemma 2/3). -Gemma 2/3 route causality through the same model-level 4D mask as the RoPE/GQA family -(``_update_causal_mask``) but ADD a per-layer sliding window on their local layers. -This handler installs the two-part SWA anneal: the shared future-reveal on the full -causal mask plus a far-past reveal on every sliding decoder layer, so at ``alpha=0`` -the model is bit-identical to base (identity gate) and at ``alpha=1`` attention is -fully non-causal AND unwindowed. RoPE, qk-norm, the KV-group layout, RMSNorm, and -embedding scaling are left to HF's own forward. +Gemma 2/3 route causality through a model-level 4D mask like the RoPE/GQA family but ADD a +sliding-window mask on the layers ``config.layer_types`` labels ``"sliding_attention"``. +Both masks are built through the same mask interface, so this handler installs the shared +anneal seam behind the sliding-window structural gate: one install reveals the +strictly-future cells on every layer and the far-past out-of-window cells on the local +ones, so at ``alpha=0`` the model is bit-identical to base (identity gate) and at +``alpha=1`` attention is fully non-causal AND unwindowed. RoPE, qk-norm, the KV-group +layout, RMSNorm, logit softcapping, and embedding scaling are left to HF's own forward. """ from __future__ import annotations diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/identity.py b/packages/a2d-worker-hf/src/a2d_core/transform/identity.py index 7711932..fd380f4 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/identity.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/identity.py @@ -32,9 +32,9 @@ def check_identity( ) -> IdentityResult: """Compare an unpatched ``base`` to a patched model at ``alpha=0`` on ``probe``. - ``base`` must be un-patched and un-grown; ``patched`` is the patched (possibly - grown) model whose ``_a2d_anneal`` is ``state``. Both are forced to CPU/eval so - the gate is deterministic float32 (Risk 2). + ``base`` must be un-patched and un-grown (it stays on HF's own ``"eager"`` mask); + ``patched`` is the patched (possibly grown) model whose registered mask seam reads + ``state``. Both are forced to CPU/eval so the gate is deterministic float32 (Risk 2). """ import torch diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py index 6379736..c159409 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py @@ -1,43 +1,28 @@ """Annealed causal->bidirectional attention for the sliding-window Gemma family (Gemma 2/3). -Gemma 2 and Gemma 3 (``transformers==4.51.3``) are the RoPE/GQA seam PLUS a per-layer -sliding window. Their decoder ``*Model`` builds ONE full 4D additive causal mask in -``_update_causal_mask`` - exactly the Gemma 1 seam - and hands it to every layer. -Then each *sliding* decoder layer's ``forward`` further masks the strictly- -far-past (keys more than ``sliding_window`` behind the query) to ``finfo(dtype).min`` -with identical logic in both families; the remaining layers are *global* and skip -that step (Gemma 3 windows all but every ``sliding_window_pattern``-th layer, Gemma 2 +Gemma 2 and Gemma 3 (``transformers==5.14.1``) are the RoPE/GQA seam PLUS a per-layer +sliding window. Their decoder builds one mask per layer *type* - ``config.layer_types`` +labels each layer ``"full_attention"`` or ``"sliding_attention"``, and the decoder calls +``create_causal_mask`` for the global layers and ``create_sliding_window_causal_mask`` for +the local ones (Gemma 3 windows all but every ``sliding_window_pattern``-th layer, Gemma 2 every other layer). Eager attention just does ``scores + attention_mask``, so BOTH -causality (future) AND the window (far-past) flow entirely through the additive mask, -layer by layer. (Mistral-style models instead fold their window into the model-level -mask and have no sliding layers; they are the ``attn.gqa`` seam, not this one.) - -Bidirectionalizing therefore needs TWO coordinated anneals over one shared -``AnnealState``: - -* the ``_update_causal_mask`` wrap (reused verbatim from the ``attn.gqa`` seam via - ``install_gqa_anneal_patch``) reveals strictly-future cells. This opens every - layer's future AND feeds the sliding layers their future-revealed base mask. -* a per-sliding-layer ``forward`` wrap reveals the strictly-far-past cells the layer - would otherwise window out. - -Both use the same penalty ramp as the GPT-2/GQA seams: ``finfo(dtype).min`` at -``alpha=0`` (so each reveal is bit-identical to base -> the D13 identity gate reads -``max_abs_diff == 0.0``) and ``clamp(log(alpha), finfo.min)``, reaching ``0`` at -``alpha=1`` (fully non-causal AND unwindowed). Global layers are untouched by the -second wrap; they open through the first alone. RoPE (Gemma 3's per-layer local vs -global theta), query-key norm, the GQA/MQA layout, RMSNorm, logit softcapping -(Gemma 2), and Gemma's sqrt(hidden) embedding scaling all stay in HF's own forward - -only the additive mask is patched, exactly as the Gemma 1 seam does. - -The far-past wrap temporarily flips the decoder layer's ``is_sliding`` to ``False`` -so HF's own (hard ``finfo.min``) window re-mask is skipped for that one call, and -supplies its annealed replacement instead. It does NOT touch ``self_attn.is_sliding``, -which selects the local RoPE embedding, so positions stay exactly as base computes -them. Overrides live on module INSTANCES (shadowing the class methods), so a sibling -un-patched base model in the same process - the identity gate's reference copy - -keeps its original causal+windowed attention. +causality (future) AND the window (far-past) flow entirely through those additive masks. +(Mistral-style models instead fold their window into the single model-level mask and +declare no ``layer_types``; they are the ``attn.gqa`` seam, not this one.) + +Both builders resolve through the SAME ``ALL_MASK_ATTENTION_FUNCTIONS`` entry, so the +shared ``install_mask_anneal`` seam (see ``attention.py``) opens the strictly-future cells +on every layer and the far-past out-of-window cells on the local layers in ONE install: +each masked cell carries exactly ``finfo(dtype).min``, and the reveal re-scores all of them +to ``clamp(log(alpha), finfo.min)``. At ``alpha=0`` that is bit-identical to base (the D13 +identity gate reads ``max_abs_diff == 0.0``); at ``alpha=1`` the model is fully non-causal +AND unwindowed. This module owns only the structural gate. + +RoPE (Gemma 3's per-layer local vs global theta), query-key norm, the GQA/MQA layout, +RMSNorm, logit softcapping (Gemma 2), and Gemma's sqrt(hidden) embedding scaling all stay +in HF's own forward - only the additive mask is ours. ``resolve_capabilities`` checks +``attn.swa`` BEFORE ``attn.gqa`` because Gemma 2/3 carry both structural signals. """ from __future__ import annotations diff --git a/packages/a2d-worker-hf/src/a2d_core/worker.py b/packages/a2d-worker-hf/src/a2d_core/worker.py index 76c7b37..4838f8f 100644 --- a/packages/a2d-worker-hf/src/a2d_core/worker.py +++ b/packages/a2d-worker-hf/src/a2d_core/worker.py @@ -170,12 +170,13 @@ def _convert(job: ConversionJob, emit: Callable[[dict[str, Any]], None]) -> int: emit(progress("grow", 2, total)) mask_token_id = resolve_mask_token(model, tokenizer, cfg.mask_token) - # 4. patch: install the annealed attention seam at alpha=0 (Decision 2). The seam - # is resolved from the model's own eager causal structure: GPT-2's self.bias - # (attn.full) vs the RoPE family's _update_causal_mask mask (attn.gqa - Gemma/ - # Qwen2/Llama) vs that same mask plus per-layer sliding windows (attn.swa - - # Gemma 2/3). The ConversionJob carries no capability set, so the worker picks - # the handler honestly from the model itself. + # 4. patch: install the annealed attention seam at alpha=0 (Decision 2). All three + # seams register one annealed mask function in HF's mask interface; which + # capability applies is resolved from the model's own eager structure: + # GPT2Attention (attn.full) vs num_key_value_heads (attn.gqa - Gemma/Qwen2/Llama/ + # Mistral) vs config.layer_types carrying sliding layers (attn.swa - Gemma 2/3). + # The ConversionJob carries no capability set, so the worker picks the handler + # honestly from the model itself. # ponytail: attention seam only; feed the manifest's model_spec.capabilities # through the job when P6 adds sink handlers keyed off config-only fields. emit(progress("patch", 3, total)) diff --git a/packages/a2d-worker-hf/tests/conftest.py b/packages/a2d-worker-hf/tests/conftest.py index 393739e..7ef4e61 100644 --- a/packages/a2d-worker-hf/tests/conftest.py +++ b/packages/a2d-worker-hf/tests/conftest.py @@ -14,7 +14,9 @@ @pytest.fixture def tiny_gpt2() -> Callable[..., Any]: """Factory for a seeded tiny GPT-2 (eager, eval, no download). Same seed => - bit-identical weights, so two calls give an aligned base/patched pair.""" + bit-identical weights, so two calls give an aligned base/patched pair. Extra + ``**overrides`` go straight to ``GPT2Config`` (e.g. ``reorder_and_upcast_attn=True`` + for the install-rejection guard).""" def _make(seed: int = 0, **overrides: Any) -> Any: import torch @@ -131,13 +133,13 @@ def tiny_gemma2() -> Callable[..., Any]: """Factory for a seeded tiny Gemma 2 (eager, eval, no download). Same ``(kwargs, seed)`` => bit-identical weights, so two calls give an aligned - base/patched pair. Exercises the Gemma 2 per-layer sliding-window seam: the window - logic is identical to Gemma 3's, but the decoder layer's ``forward`` takes ONE - ``position_embeddings`` pair where Gemma 3 takes a global/local pair - the - signature the swa wrapper must not hardcode away - and every EVEN layer slides. - Defaults (2 layers) put both a local layer (0) and a global layer (1) in the - stack; pass ``num_hidden_layers=1`` for an all-sliding model whose single layer's - receptive field is exactly the window.""" + base/patched pair. Exercises the Gemma 2 flavor of the sliding-window seam: the mask + logic is identical to Gemma 3's, but Gemma 2's own eager attention adds logit + softcapping - which the seam must leave to HF's forward, since it resolves each + family's ``eager_attention_forward`` rather than substituting one - and every EVEN + layer slides. Defaults (2 layers) put both a local layer (0) and a global layer (1) + in the stack; pass ``num_hidden_layers=1`` for an all-sliding model whose single + layer's receptive field is exactly the window.""" def _make(seed: int = 0, num_hidden_layers: int = 2, sliding_window: int = 2) -> Any: import torch @@ -169,10 +171,10 @@ def tiny_mistral() -> Callable[..., Any]: sliding window (default 2, well under the tests' seq_len 8). Same ``(kwargs, seed)`` => bit-identical weights, so two calls give an aligned - base/patched pair. Mistral folds its window into the single model-level 4D mask - ``_update_causal_mask`` builds and has NO sliding decoder layers, so it routes to - ``attn.gqa`` - the single-mask windowed flavor whose far-past cells the shared - reveal must open alongside the strictly-future ones.""" + base/patched pair. Mistral folds its window into the single model-level 4D mask and + declares no ``config.layer_types``, so it routes to ``attn.gqa`` - the single-mask + windowed flavor whose far-past cells the shared reveal must open alongside the + strictly-future ones.""" def _make(seed: int = 0, sliding_window: int = 2) -> Any: import torch diff --git a/packages/a2d-worker-hf/tests/test_gqa_attention.py b/packages/a2d-worker-hf/tests/test_gqa_attention.py index 51cad60..8f5be8f 100644 --- a/packages/a2d-worker-hf/tests/test_gqa_attention.py +++ b/packages/a2d-worker-hf/tests/test_gqa_attention.py @@ -2,8 +2,8 @@ at ``alpha=0`` (bit-identical to base) and the ``alpha=1`` bidirectional-behavior guard, plus the worker's structural handler dispatch. -Mirrors ``test_identity``/``test_bidir`` (GPT-2) for the RoPE family, whose eager -seam is the 4D mask ``_update_causal_mask`` builds, not GPT-2's ``self.bias``. Also +Mirrors ``test_identity``/``test_bidir`` (GPT-2) for the RoPE family, whose eager seam is +the single model-level 4D mask HF builds through its mask interface. Also covers the single-mask windowed flavor (Mistral v0.1), whose sliding window is folded into that same model-level mask: the shared reveal must open its far-past window alongside the strictly-future cells, so a converted Mistral is never silently still @@ -132,7 +132,7 @@ def test_resolve_capabilities_picks_gqa_for_rope_family(tiny_gqa: Callable[..., def test_resolve_capabilities_keeps_gpt2_on_full(tiny_gpt2: Callable[..., Any]) -> None: - """GPT-2 must keep selecting its own attn.full self.bias seam, unchanged.""" + """GPT-2 declares no num_key_value_heads, so it must keep selecting attn.full.""" assert resolve_capabilities(tiny_gpt2(0)) == ["attn.full"] @@ -209,9 +209,10 @@ def test_gqa_padding_stays_masked_and_identity_holds_with_padding( def test_gqa_reinstall_with_fresh_state_takes_effect( tiny_gqa: Callable[..., Any], family: str ) -> None: - """Re-installing on an already-patched model must swap in the NEW state (parity - with the GPT-2 seam's per-install re-tag) without double-wrapping: after a second - install at alpha=1, an earlier position sees a perturbed future token.""" + """Re-installing on an already-patched model must swap in the NEW state: the + implementation key is derived from the state, so a second install registers and + selects a fresh key rather than being silently ignored. After a second install at + alpha=1, an earlier position sees a perturbed future token.""" model = tiny_gqa(family, 0) install_gqa_anneal_patch(model, AnnealState(alpha=0.0)) install_gqa_anneal_patch(model, AnnealState(alpha=1.0)) diff --git a/packages/a2d-worker-hf/tests/test_swa_attention.py b/packages/a2d-worker-hf/tests/test_swa_attention.py index 60fcbee..7a6d749 100644 --- a/packages/a2d-worker-hf/tests/test_swa_attention.py +++ b/packages/a2d-worker-hf/tests/test_swa_attention.py @@ -4,14 +4,14 @@ and the window-open (the sliding window's far-past opens) - plus the worker's structural handler dispatch. -The Gemma 2/3 eager seam is the single 4D ``_update_causal_mask`` (like Gemma 1) PLUS -a per-layer far-past window re-mask on the local (sliding) decoder layers. The window -test uses an all-sliding single-layer model so a query's receptive field is exactly -its window; the identity/future tests use a mixed 4-layer stack (local layers 0, 2 and -global layers 1, 3). Gemma 2 - whose decoder layer takes a single -``position_embeddings`` pair where Gemma 3 takes a global/local pair - guards the -wrapper's signature-agnostic re-bind. All hermetic: tiny random-weight configs on CPU -float32, no network. +The Gemma 2/3 eager seam is the model-level causal mask (like Gemma 1) PLUS a +sliding-window mask on the layers ``config.layer_types`` labels ``"sliding_attention"``; +both are built through the one mask interface a2d registers into. The window test uses an +all-sliding single-layer model so a query's receptive field is exactly its window; the +identity/future tests use a mixed 4-layer stack (local layers 0, 2 and global layers 1, 3). +Gemma 2 is covered alongside Gemma 3 because its own eager attention adds logit +softcapping, which the seam must leave to HF's forward. All hermetic: tiny random-weight +configs on CPU float32, no network. """ from __future__ import annotations @@ -201,10 +201,10 @@ def test_swa_padding_stays_masked_and_identity_holds_with_padding( def test_swa_gemma2_patched_at_alpha0_is_bit_identical_to_base( tiny_gemma2: Callable[..., Any], ) -> None: - """Regression (review: swa-gemma2-signature-crash): Gemma 2's decoder layer takes a - single ``position_embeddings`` pair, so the wrapper must survive its keyword call - path without crashing, and patched@alpha=0 logits must equal base to 0.0 with both - a local and a global layer exercised.""" + """Regression (review: swa-gemma2-signature-crash): the Gemma 2 stack must survive the + install without crashing - its eager attention differs from Gemma 3's (logit + softcapping) - and patched@alpha=0 logits must equal base to 0.0 with both a local and + a global layer exercised.""" base = tiny_gemma2() patched = tiny_gemma2() patched.load_state_dict(base.state_dict()) # guarantee identical weights @@ -225,7 +225,7 @@ def test_swa_gemma2_patched_at_alpha0_is_bit_identical_to_base( def test_swa_gemma2_opens_window_and_future_at_alpha1_not_alpha0( tiny_gemma2: Callable[..., Any], ) -> None: - """Gemma 2 end-to-end reveal through the signature-agnostic wrapper: on an + """Gemma 2 end-to-end reveal through the shared mask seam: on an all-sliding single-layer model an in-window key moves the query's logits even at alpha=0 (non-vacuity), while a far-past out-of-window key AND a strictly-future key move them at alpha=1 only.""" @@ -250,9 +250,9 @@ def test_swa_gemma2_opens_window_and_future_at_alpha1_not_alpha0( def test_swa_wrapped_layer_survives_gradient_checkpointing( tiny_gemma2: Callable[..., Any], tiny_gemma3: Callable[..., Any] ) -> None: - """The positional call path: under gradient checkpointing the model invokes the - decoder layer with every argument positional, which the wrapper must re-bind - against the original signature for both the Gemma 2 and Gemma 3 shapes.""" + """The checkpointed call path: under gradient checkpointing the model invokes the + decoder layer with every argument positional, and the annealed mask must still be built + and backprop through it, for both the Gemma 2 and Gemma 3 shapes.""" for model in (tiny_gemma2(), tiny_gemma3()): state = AnnealState(alpha=0.5) install_swa_anneal_patch(model, state) @@ -278,8 +278,9 @@ def test_swa_install_on_non_sliding_model_raises_and_leaves_model_unpatched( def test_swa_reinstall_with_fresh_state_takes_effect(tiny_gemma3: Callable[..., Any]) -> None: - """Re-installing on an already-patched model must swap in the NEW state without - double-wrapping: after a second install at alpha=1, an out-of-window token reaches the + """Re-installing on an already-patched model must swap in the NEW state: the + implementation key is derived from the state, so a second install registers and selects + a fresh key. After a second install at alpha=1, an out-of-window token reaches the query.""" model = tiny_gemma3(num_hidden_layers=1, sliding_window=2, sliding_window_pattern=6) install_swa_anneal_patch(model, AnnealState(alpha=0.0)) diff --git a/pyproject.toml b/pyproject.toml index e37c620..baf944e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,11 +38,12 @@ strict = true files = ["packages"] exclude = "packages/a2d-contracts/src/a2d_contracts/models" -# transformers 4.51.3 ships an (empty) py.typed marker, but the worker's transform -# layer patches HF's eager attention seams directly with runtime-only knowledge mypy -# cannot follow, so keep treating transformers as untyped (Any). `follow_imports=skip` -# is what makes that hold now that the marker exists - `ignore_missing_imports` alone -# no longer suffices. tokenizers is treated the same way. +# transformers 5.14.1 still ships an (empty) py.typed marker, so mypy would otherwise +# follow it and check the worker against v5's inline annotations - which disagree with +# the runtime for how we use it (config fields arriving via `**kwargs`, +# `Trainer.compute_loss`), reintroducing 68 errors. Keep treating transformers as +# untyped (Any); `follow_imports=skip` is what makes that hold now that the marker +# exists - `ignore_missing_imports` alone no longer suffices. tokenizers is the same. [[tool.mypy.overrides]] module = ["transformers", "transformers.*", "tokenizers", "tokenizers.*"] ignore_missing_imports = true From 00e236cd45ba2424eb101c4440406083bacf0f76 Mon Sep 17 00:00:00 2001 From: Jerry Xiao Date: Wed, 29 Jul 2026 11:34:29 +0000 Subject: [PATCH 4/4] no-mistakes(document): revert docs resync; defer prose to stacked docs PR --- AGENTS.md | 4 +- docs/CONCEPTS.md | 2 +- docs/SPEC-HANDOFF.md | 8 +-- .../src/a2d_core/transform/apply.py | 23 +++---- .../src/a2d_core/transform/attention.py | 66 ++++++++----------- .../src/a2d_core/transform/gqa_attention.py | 53 +++++++++------ .../transform/handlers/full_attention.py | 9 +-- .../transform/handlers/gqa_attention.py | 10 +-- .../transform/handlers/swa_attention.py | 15 ++--- .../src/a2d_core/transform/identity.py | 6 +- .../src/a2d_core/transform/swa_attention.py | 57 ++++++++++------ packages/a2d-worker-hf/src/a2d_core/worker.py | 13 ++-- packages/a2d-worker-hf/tests/conftest.py | 26 ++++---- .../a2d-worker-hf/tests/test_gqa_attention.py | 13 ++-- .../a2d-worker-hf/tests/test_swa_attention.py | 37 +++++------ pyproject.toml | 11 ++-- 16 files changed, 178 insertions(+), 175 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 789d9e5..ea83902 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,8 +4,8 @@ This file is the project's committed home for project-intrinsic agent knowledge: - **What a2d is + the extension playbook:** `docs/SPEC-HANDOFF.md` (esp. §3 open-closed model, §5 repo layout) and `docs/ARCHITECTURE.md` (the ML recipe: mask annealing, MDLM, D13 identity gate). Adding a model/capability is additive by policy — one file per quirk, never edit an existing adapter/handler. - **Detect (Rust, config-only, no GPU):** `crates/a2d-detect/`. New `model_type` quirk = one file in `src/adapters/` delegating to `generic::detect` + `inferred=false`, one `mod` line in `adapters/mod.rs`, and one `fixtures/configs//{config.json,expected.json}` (auto-picked up by `tests/corpus.rs`). Capabilities are the unit of support, not model names; the gate blocks only conversion-blocking caps. -- **Convert handlers (Python worker):** `packages/a2d-worker-hf/src/a2d_core/transform/`. One seam, three gates. Since transformers v5 every mask is built through `masking_utils.ALL_MASK_ATTENTION_FUNCTIONS`, keyed by `config._attn_implementation`, so `attention.py`'s `install_mask_anneal` registers one annealed mask function per `AnnealState` (key `a2d_annealed_eager_`, registered in the mask _and_ attention interfaces since one config value selects both) and points only that model's config at it — isolation is per model, not per process, which is what lets the D13 gate hold an un-patched sibling on `"eager"` in the same process. The reveal derives FROM HF's own `eager_mask` output instead of rebuilding the mask, so `alpha=0` is bit-identical by construction (any dtype, cache offset, padding) and genuine 2D padding stays masked at every alpha. `attn.full`/`attn.gqa`/`attn.swa` stay DISTINCT capabilities (contract shared with the Rust detect crate; the handler registry is keyed on them) and are now three structural gates on that one install, checked in this order by `resolve_capabilities` (`apply.py`): `config.layer_types` containing `sliding_attention` (`attn.swa`, Gemma 2/3 — first, because they also carry the gqa signal), then `num_key_value_heads` (`attn.gqa`, Llama/Qwen2/Gemma 1/Mistral), then `GPT2Attention` (`attn.full`). Mistral folds its window into the single model-level mask and declares no `layer_types`, so it correctly stays `attn.gqa` and still unwindows at `alpha=1`. `worker.py` picks the handler by structural introspection, not from job tags — the `ConversionJob` carries no capability set. D13 rule: patched@`alpha=0` must equal base logits bit-for-bit. Because the seam is a registry key rather than a monkeypatch, a silently-not-applied patch is now the dangerous failure mode; `tests/test_bidir.py::test_install_routes_the_model_and_only_it_through_the_annealed_seam` is the guard that the key is live and the sibling is untouched. `attn.swa` is a SUPPORTED, non-blocking capability (`crates/a2d-contracts/src/lib.rs` `blocking()`); `attn.sink`/`attn.mla`/SSM still reject. -- **HF transformers is pinned to `5.14.1`** (`packages/a2d-worker-hf/pyproject.toml`) because Decision 2 patches HF's mask seam directly, so even a minor bump can silently move it. Gotchas: v5 deleted all three 4.x seams (GPT-2's per-layer `self.bias`, the per-model `_update_causal_mask`, Gemma 2/3's per-decoder-layer window re-mask) in favour of the mask interface, so any 4.x note about them is obsolete; `from_pretrained` takes `dtype=`, not `torch_dtype=`; and 5.14.1 still ships an (empty) `py.typed`, so the root `pyproject.toml` mypy override keeps `transformers`/`tokenizers` untyped via `follow_imports = "skip"` (dropping it reintroduces 68 errors, because v5's inline annotations disagree with the runtime for `**kwargs` config fields and `Trainer.compute_loss`). +- **Convert handlers (Python worker):** `packages/a2d-worker-hf/src/a2d_core/transform/`. Three eager causal seams — GPT-2 bakes causality into per-layer `self.bias` (`attn.full`, `attention.py`); the RoPE family (Llama/Qwen2/Gemma 1) routes it through the 4D mask `_update_causal_mask` builds (`attn.gqa`, `gqa_attention.py` - the reveal opens EVERY base-masked real-key cell, so single-mask windowed families like Mistral v0.1 or Qwen2 with active `use_sliding_window` also unwindow through this seam); Gemma 2/3 add a per-layer sliding window on their *local* decoder layers on top of that same full mask (`attn.swa`, `swa_attention.py` - anneals BOTH the full-causal future-reveal AND each sliding layer's far-past window via a signature-agnostic layer-forward wrap; `resolve_capabilities` checks SWA before GQA because Gemma 2/3 own both seams). `worker.py` picks the handler by structural introspection (`resolve_capabilities` in `apply.py`), not from job tags — the `ConversionJob` carries no capability set. D13 rule: patched@`alpha=0` must equal base logits bit-for-bit. `attn.swa` is a SUPPORTED, non-blocking capability (`crates/a2d-contracts/src/lib.rs` `blocking()`); `attn.sink`/`attn.mla`/SSM still reject. +- **HF transformers is pinned to `4.51.3`** (`packages/a2d-worker-hf/pyproject.toml`), the first pin shipping Gemma 3; the GPT-2 eager seam and Gemma 1 `_update_causal_mask` are unchanged from the old `4.48.3`. Two gotchas: 4.51.3 ships an (empty) `py.typed`, so the root `pyproject.toml` mypy override keeps `transformers`/`tokenizers` untyped via `follow_imports = "skip"` (not just `ignore_missing_imports`); and in 4.51.3 Gemma 3's sliding window is applied per-decoder-layer in `Gemma3DecoderLayer.forward`, NOT via a model-level mask-mapping dict (that is a later-transformers refactor) - verify the seam before assuming. - **Hermetic tests only:** never download weights (Gemma is gated; CI is CPU/no-network). Build tiny random-weight configs in-process (see `tests/conftest.py` `tiny_gpt2`/`tiny_gqa`/`tiny_gemma2`/`tiny_gemma3`/`tiny_mistral`). - **Full CI gate before shipping:** `cargo fmt --all --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`; `uv run ruff check .`, `uv run ruff format --check .`, `uv run mypy` (strict), `uv run pytest`; contracts are generated — after touching `crates/a2d-contracts/` run `bash scripts/codegen.sh` (CI fails on `schema/`+`packages/a2d-contracts/` drift). diff --git a/docs/CONCEPTS.md b/docs/CONCEPTS.md index f182cae..771e57c 100644 --- a/docs/CONCEPTS.md +++ b/docs/CONCEPTS.md @@ -24,7 +24,7 @@ Everything else in the vocabulary exists to decide *which models a2d will touch* - **Bidirectional** - a token sees left *and* right. What diffusion needs. - **MDLM** - the masked-diffusion objective a2d uses: mask a random fraction, predict them, repeat. - **Canvas** - the span of positions a diffusion model denoises (its workspace). -- **Anneal (`attn.full`, `attn.gqa`, `attn.swa`)** - a2d's trick: slowly turn the causal mask off so AR weights adapt to bidirectional. One transform per attention shape, all annealing the same HF mask seam: GPT-2's dense causal mask (`attn.full`); the RoPE family (Gemma 1 / Qwen2 / Llama / Mistral), which routes causality through one model-level mask (`attn.gqa`); Gemma 2/3, which add a sliding-window mask on their local layers (`attn.swa`). +- **Anneal (`attn.full`, `attn.gqa`, `attn.swa`)** - a2d's trick: slowly turn the causal mask off so AR weights adapt to bidirectional. One transform per attention seam: GPT-2 bakes causality per layer (`attn.full`); the RoPE family (Gemma 1 / Qwen2 / Llama / Mistral) routes it through one shared mask (`attn.gqa`); Gemma 2/3 add a per-layer sliding window on top of that mask (`attn.swa`). - **Identity gate** - hard correctness check: at `anneal=0` the patched model must match the base model's logits, or convert aborts. - **GQA** - grouped-query attention: fewer key/value heads than query heads (a memory trick). The mechanism rides along in HF's forward; the `attn.gqa` tag also names the RoPE-family anneal transform (see Anneal). - **RoPE** - rotary position encoding (modern position scheme). Passthrough. diff --git a/docs/SPEC-HANDOFF.md b/docs/SPEC-HANDOFF.md index 88ade07..2a17cec 100644 --- a/docs/SPEC-HANDOFF.md +++ b/docs/SPEC-HANDOFF.md @@ -216,10 +216,10 @@ a2d/ src/a2d_core/ ingest/ # ◄ EXTENSION POINT: format normalizers (copy-on-normalize) transform/ - attention.py # shared HF mask-interface seam (attn.full) + AnnealState/schedule - gqa_attention.py # RoPE-family gate: num_key_value_heads (attn.gqa) - swa_attention.py # Gemma 2/3 gate: config.layer_types sliding layers (attn.swa) - apply.py # load model, resolve capabilities from its structure, apply handlers + attention.py # GPT-2 self.bias seam (attn.full) + AnnealState/schedule + gqa_attention.py # RoPE-family _update_causal_mask seam (attn.gqa) + swa_attention.py # Gemma 2/3 per-layer sliding-window seam (attn.swa) + apply.py # load model, resolve capabilities from its seam, apply handlers handlers/ # ◄ EXTENSION POINT: capability handlers (attn.full, attn.gqa, attn.swa, ffn.moe, …) objectives/ # ◄ EXTENSION POINT: mdlm.py, bd3lm.py (corrupt/loss iface) data/ # local corpus readers + streaming, packing, noising collators diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/apply.py b/packages/a2d-worker-hf/src/a2d_core/transform/apply.py index 4d929cf..3c60f8f 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/apply.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/apply.py @@ -69,19 +69,16 @@ def resolve_mask_token(model: Any, tokenizer: Any, strategy: str = "grow") -> in def resolve_capabilities(model: Any) -> list[str]: """The attention-handler capabilities a loaded model needs, chosen by its eager - causal seam (Decision 2). ``transformers`` v5 builds all of them through one mask - interface, but they stay three distinct capabilities - the set is a contract shared - with the Rust detect crate - so they are three structural gates on one install: - - - Gemma 2/3 label their layers in ``config.layer_types``, so some layers take a - sliding-window mask on top of causality -> ``attn.swa``. Checked FIRST, because - these models also carry the ``attn.gqa`` signal; the swa install opens both the - future and the window. - - The RoPE family (Llama/Qwen2/Gemma 1/Mistral) declares ``num_key_value_heads`` and - routes causality through one model-level 4D mask -> ``attn.gqa`` (covers GQA, MQA, - and full-attn RoPE alike; the mask is family-independent). - - GPT-2 declares neither and is recognised by its ``GPT2Attention`` modules -> - ``attn.full``. + causal seam (Decision 2). Three disjoint seams exist in this scope: + + - Gemma 2/3 route causality through ``_update_causal_mask`` AND add a per-layer + sliding window on their local decoder layers -> ``attn.swa``. Checked FIRST, + because these models also own ``_update_causal_mask`` (the ``attn.gqa`` signal); + the swa handler subsumes the gqa future-reveal and additionally opens the window. + - The RoPE family (Llama/Qwen2/Gemma 1) routes causality through the 4D mask that + ``_update_causal_mask`` builds -> ``attn.gqa`` (covers GQA, MQA, and full-attn + RoPE alike; the mask is family-independent). + - GPT-2 bakes causality into a per-layer ``self.bias`` buffer -> ``attn.full``. The seam is read from the model itself, not from detect's tags: the ``ConversionJob`` does not carry the capability set, so the worker independently picks the correct, diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/attention.py index 5a3e226..c886561 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/attention.py @@ -1,40 +1,31 @@ -"""Annealed causal->bidirectional attention: the shared mask seam (Decision 2). - -``transformers==5.14.1`` builds every attention mask through ONE documented extension -point - ``masking_utils.ALL_MASK_ATTENTION_FUNCTIONS``, keyed by -``config._attn_implementation``. Both ``create_causal_mask`` and -``create_sliding_window_causal_mask`` resolve their builder there, so a single registered -mask function covers all three seams a2d converts: GPT-2 (which no longer bakes causality -into a per-layer ``self.bias`` buffer), the RoPE family's one model-level causal mask, and -the per-layer sliding-window mask Gemma 2/3 request for their local layers. - -``install_mask_anneal`` registers ``annealed_eager_mask`` under a key derived from the -``AnnealState`` (``a2d_annealed_eager_``) and points only that model's config at -it. Isolation is therefore per model, not per process: the D13 identity gate holds an -un-patched sibling copy of the same model in the same process, and it stays on ``"eager"`` -with HF's own causal mask. The key is registered in BOTH registries (mask and attention) -because one config value selects both. Rationale for a registry key over the alternatives: -``PretrainedConfig`` defines ``__eq__`` without ``__hash__``, so a ``WeakKeyDictionary`` -keyed by config is impossible, and a ``config`` attribute holding the state breaks -``save_pretrained`` (not JSON-serializable). - -The reveal is derived FROM HF's own ``eager_mask`` output rather than rebuilt: every cell -the base mask masked (exactly ``finfo(dtype).min``) is re-scored to -``clamp(log(alpha), finfo.min)``. - -* At ``alpha=0`` the penalty IS ``finfo.min`` - the value the base mask already carries - - so the returned tensor is bit-identical to base and patched@0 logits equal base to the - bit (the D13 identity gate), by construction and independent of dtype, cache offset or - padding. -* At ``alpha=1`` the penalty is ``log(1)=0``, so attention is fully bidirectional AND - unwindowed; intermediate ``alpha`` scales each masked position's pre-softmax mass by - ``alpha`` (a smooth monotone reveal). - -Genuine 2D padding is preserved: a masked cell is only revealed when its key is a real -token, so a padded key stays ``finfo.min`` at every alpha. +"""Annealed causal->bidirectional attention patch for GPT-2's eager seam (Decision 2). + +``transformers==4.48.3`` GPT-2 bakes causality INSIDE the attention op: eager +``GPT2Attention`` masks scores with a lower-triangular ``self.bias`` buffer via +``torch.where(causal, scores, finfo(float32).min)``; the passed 4D +``attention_mask`` is padding-only. A model-level additive mask therefore does +NOT control causality, so this module patches the seam that does. + +The patch neutralizes ``self.bias`` (registers it all-True so ``torch.where`` +never masks) and re-supplies a single annealed additive mask driven by one shared +``AnnealState``: on/below the diagonal it is ``0``; strictly-future (``j>i``) it is +``clamp(log(alpha), finfo(float32).min)``. + +* At ``alpha=0`` a future entry is ``finfo.min``, and a finite pre-softmax score is + negligible against it in float32 (``score + finfo.min == finfo.min`` to the bit), + so patched-at-0 scores/logits are bit-identical to base. +* At ``alpha=1`` the penalty is ``log(1)=0`` and, with ``self.bias`` neutralized, + attention is fully bidirectional; intermediate ``alpha`` scales each future + position's pre-softmax mass by ``alpha`` (a smooth monotone reveal). This deliberately replaces the ML-recon's ``(1-alpha)*(-inf) + alpha*0`` blend, which is ``-inf`` for every ``alpha<1`` and thus not an anneal at all (Risk 4). + +The eager path is patched at the module-global ``eager_attention_forward`` that +``GPT2Attention.forward`` resolves at call time. The replacement is inert for any +attention module without a ``_a2d_anneal`` tag, so an unpatched base model (the +identity gate's reference copy) keeps its original causal attention even though +the global is process-wide. """ from __future__ import annotations @@ -149,12 +140,7 @@ def install_mask_anneal(model: Any, state: AnnealState) -> None: def install_anneal_patch(model: Any, state: AnnealState) -> None: - """``attn.full`` install: the shared mask anneal, gated on GPT-2's dense attention. - - ``reorder_and_upcast_attn=True`` is rejected by name rather than left to fail as a bare - ``max_abs_diff`` in the D13 gate: that GPT-2 path is only taken under the literal - ``"eager"`` key, so base would keep it and the patched model would not. - """ + """``attn.full`` install: the shared mask anneal, gated on GPT-2's dense attention.""" if not any(type(m).__name__ == "GPT2Attention" for m in model.modules()): raise ValueError("anneal patch found no GPT2Attention modules (not a GPT-2 eager model?)") if getattr(model.config, "reorder_and_upcast_attn", False): diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py index 4c883a3..e19840b 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/gqa_attention.py @@ -1,25 +1,40 @@ """Annealed causal->bidirectional attention for the RoPE/GQA family (Decision 2). -Llama / Qwen2 / Gemma 1 build ONE model-level 4D additive causal mask per forward and hand +Llama / Qwen2 / Gemma (``transformers==4.51.3``) do NOT bake causality into a +per-layer ``self.bias`` buffer the way GPT-2 does. Instead the decoder ``*Model`` +builds one 4D additive causal mask per forward in ``_update_causal_mask`` and hands it down to every layer; eager attention just does ``scores + causal_mask``. Causality -flows entirely through that mask, and in ``transformers==5.14.1`` the mask is built by -whatever ``ALL_MASK_ATTENTION_FUNCTIONS`` holds for ``config._attn_implementation``. The -anneal itself is therefore the shared ``install_mask_anneal`` seam (see ``attention.py``), -and this module owns only the structural gate that recognises the family. - -One gate covers Gemma (MQA), Qwen2/Llama (GQA) and full-attention RoPE models (Llama-2-7B) -alike: the mask is family-independent, and the GQA group expansion (``repeat_kv``), RoPE, -RMSNorm and Gemma's sqrt(hidden) embedding scaling all stay untouched in HF's own forward. -It also covers the single-mask windowed flavors - Mistral v0.1, Qwen2 with an active -``use_sliding_window`` - which fold their window into that SAME model-level mask and -declare no ``config.layer_types``: the shared reveal opens EVERY base-masked real-key cell, -so their far-past out-of-window cells reopen through the identical anneal and ``alpha=1`` -is fully non-causal AND unwindowed. - -``attn.gqa`` stays a capability distinct from ``attn.full``/``attn.swa`` even though v5 -collapsed all three onto one seam: the capability set is a contract shared with the Rust -detect crate, and the handler registry is keyed on it. They are now three structural gates -on one install. +therefore flows entirely through that mask, so annealing it (not a ``self.bias`` seam) +is what opens attention here. This is the ONE seam shared verbatim by all three +families, so a single handler covers Gemma (MQA), Qwen2/Llama (GQA), and even +full-attention RoPE models (Llama-2-7B): the mask is family-independent, and the GQA +group expansion (``repeat_kv``), RoPE, RMSNorm, and Gemma's sqrt(hidden) embedding +scaling all stay untouched in HF's own forward. + +The patch wraps the decoder's bound ``_update_causal_mask``: it calls the original +to get the exact base mask, then re-reveals every cell that mask masked for a real +(non-padded) key under one shared ``AnnealState``. For the full-attention RoPE +family those cells are exactly the strictly-future set; Mistral v0.1 and Qwen2 with +an active ``use_sliding_window`` fold their sliding window into this SAME model-level +mask (they have no per-layer window), so their far-past out-of-window cells reopen +through the identical anneal and ``alpha=1`` is fully non-causal AND unwindowed: + +* At ``alpha=0`` the penalty is exactly ``finfo(dtype).min`` - the same value the + base mask already carries at masked cells - so ``torch.where(reveal, penalty, + base)`` returns a tensor bit-identical to base, and patched@0 logits equal base to + the bit (the D13 identity gate). +* At ``alpha=1`` the penalty is ``log(1)=0`` so masked cells become attendable and + attention is fully bidirectional; intermediate ``alpha`` applies ``log(alpha)`` (a + smooth monotone reveal), mirroring the GPT-2 seam's semantics. + +Deriving the result FROM the base mask (rather than rebuilding it) is what makes the +``alpha=0`` no-op bit-identical by construction, independent of dtype, cache offset, +or padding. A genuine 2D padding mask is preserved: a masked cell is only revealed +when its key is a real (non-padded) token, so padding stays masked at every alpha. + +The override is installed on the model INSTANCE (it shadows the class method via the +instance ``__dict__``), so a sibling un-patched base model in the same process - the +identity gate's reference copy - keeps its original causal attention. """ from __future__ import annotations diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/full_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/full_attention.py index 08f33ce..ac3e13e 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/full_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/full_attention.py @@ -1,11 +1,8 @@ """``attn.full`` transform: install the annealed bidirectional attention patch. -GPT-2 dense causal attention -> annealed causal->bidirectional via the shared -mask-interface seam (Decision 2), behind a ``GPT2Attention`` structural gate that also -rejects ``reorder_and_upcast_attn=True`` by name. The ``alpha=0`` identity gate and -``test_bidir`` together prove the patch is both bit-identical to base and genuinely -routed - the model's ``_attn_implementation`` really is a2d's registered key, and a -sibling model in the same process is not. +GPT-2 dense causal attention -> annealed causal->bidirectional via the eager-seam +patch (Decision 2). The ``alpha=0`` identity gate and ``test_bidir`` together prove +the patch is both bit-identical to base and genuinely reaches GPT-2's causality. """ from __future__ import annotations diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/gqa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/gqa_attention.py index ffffe3c..125c3fb 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/gqa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/gqa_attention.py @@ -1,10 +1,10 @@ """``attn.gqa`` transform: annealed bidirectional attention for the RoPE/GQA family. -Llama / Qwen2 / Gemma route causality through one model-level 4D mask, so this handler -installs the shared mask-interface anneal seam behind the RoPE-family structural gate -(``num_key_value_heads``). It covers GQA, MQA (Gemma's ``num_key_value_heads=1``), -full-attention RoPE models, and the single-mask windowed families (Mistral v0.1, Qwen2 with -an active sliding window) whose far-past window lives in that same mask, leaving RoPE, the +Llama / Qwen2 / Gemma route causality through the 4D mask that ``_update_causal_mask`` +builds (not GPT-2's per-layer ``self.bias``), so this handler installs the mask-seam +anneal patch. It covers GQA, MQA (Gemma's ``num_key_value_heads=1``), full-attention +RoPE models, and the single-mask windowed families (Mistral v0.1, Qwen2 with an active +sliding window) whose far-past window lives in that same mask, leaving RoPE, the KV-group expansion, RMSNorm, and Gemma's embedding scaling to HF's own forward. The ``alpha=0`` identity gate and the ``alpha=1`` bidir test together prove the patch is bit-identical to base yet genuinely reaches future tokens. diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/swa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/swa_attention.py index 54af2b5..14391b8 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/handlers/swa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/handlers/swa_attention.py @@ -1,14 +1,13 @@ """``attn.swa`` transform: annealed bidirectional attention for the sliding-window Gemma family (Gemma 2/3). -Gemma 2/3 route causality through a model-level 4D mask like the RoPE/GQA family but ADD a -sliding-window mask on the layers ``config.layer_types`` labels ``"sliding_attention"``. -Both masks are built through the same mask interface, so this handler installs the shared -anneal seam behind the sliding-window structural gate: one install reveals the -strictly-future cells on every layer and the far-past out-of-window cells on the local -ones, so at ``alpha=0`` the model is bit-identical to base (identity gate) and at -``alpha=1`` attention is fully non-causal AND unwindowed. RoPE, qk-norm, the KV-group -layout, RMSNorm, logit softcapping, and embedding scaling are left to HF's own forward. +Gemma 2/3 route causality through the same model-level 4D mask as the RoPE/GQA family +(``_update_causal_mask``) but ADD a per-layer sliding window on their local layers. +This handler installs the two-part SWA anneal: the shared future-reveal on the full +causal mask plus a far-past reveal on every sliding decoder layer, so at ``alpha=0`` +the model is bit-identical to base (identity gate) and at ``alpha=1`` attention is +fully non-causal AND unwindowed. RoPE, qk-norm, the KV-group layout, RMSNorm, and +embedding scaling are left to HF's own forward. """ from __future__ import annotations diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/identity.py b/packages/a2d-worker-hf/src/a2d_core/transform/identity.py index fd380f4..7711932 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/identity.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/identity.py @@ -32,9 +32,9 @@ def check_identity( ) -> IdentityResult: """Compare an unpatched ``base`` to a patched model at ``alpha=0`` on ``probe``. - ``base`` must be un-patched and un-grown (it stays on HF's own ``"eager"`` mask); - ``patched`` is the patched (possibly grown) model whose registered mask seam reads - ``state``. Both are forced to CPU/eval so the gate is deterministic float32 (Risk 2). + ``base`` must be un-patched and un-grown; ``patched`` is the patched (possibly + grown) model whose ``_a2d_anneal`` is ``state``. Both are forced to CPU/eval so + the gate is deterministic float32 (Risk 2). """ import torch diff --git a/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py b/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py index c159409..6379736 100644 --- a/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py +++ b/packages/a2d-worker-hf/src/a2d_core/transform/swa_attention.py @@ -1,28 +1,43 @@ """Annealed causal->bidirectional attention for the sliding-window Gemma family (Gemma 2/3). -Gemma 2 and Gemma 3 (``transformers==5.14.1``) are the RoPE/GQA seam PLUS a per-layer -sliding window. Their decoder builds one mask per layer *type* - ``config.layer_types`` -labels each layer ``"full_attention"`` or ``"sliding_attention"``, and the decoder calls -``create_causal_mask`` for the global layers and ``create_sliding_window_causal_mask`` for -the local ones (Gemma 3 windows all but every ``sliding_window_pattern``-th layer, Gemma 2 +Gemma 2 and Gemma 3 (``transformers==4.51.3``) are the RoPE/GQA seam PLUS a per-layer +sliding window. Their decoder ``*Model`` builds ONE full 4D additive causal mask in +``_update_causal_mask`` - exactly the Gemma 1 seam - and hands it to every layer. +Then each *sliding* decoder layer's ``forward`` further masks the strictly- +far-past (keys more than ``sliding_window`` behind the query) to ``finfo(dtype).min`` +with identical logic in both families; the remaining layers are *global* and skip +that step (Gemma 3 windows all but every ``sliding_window_pattern``-th layer, Gemma 2 every other layer). Eager attention just does ``scores + attention_mask``, so BOTH -causality (future) AND the window (far-past) flow entirely through those additive masks. -(Mistral-style models instead fold their window into the single model-level mask and -declare no ``layer_types``; they are the ``attn.gqa`` seam, not this one.) - -Both builders resolve through the SAME ``ALL_MASK_ATTENTION_FUNCTIONS`` entry, so the -shared ``install_mask_anneal`` seam (see ``attention.py``) opens the strictly-future cells -on every layer and the far-past out-of-window cells on the local layers in ONE install: -each masked cell carries exactly ``finfo(dtype).min``, and the reveal re-scores all of them -to ``clamp(log(alpha), finfo.min)``. At ``alpha=0`` that is bit-identical to base (the D13 -identity gate reads ``max_abs_diff == 0.0``); at ``alpha=1`` the model is fully non-causal -AND unwindowed. This module owns only the structural gate. - -RoPE (Gemma 3's per-layer local vs global theta), query-key norm, the GQA/MQA layout, -RMSNorm, logit softcapping (Gemma 2), and Gemma's sqrt(hidden) embedding scaling all stay -in HF's own forward - only the additive mask is ours. ``resolve_capabilities`` checks -``attn.swa`` BEFORE ``attn.gqa`` because Gemma 2/3 carry both structural signals. +causality (future) AND the window (far-past) flow entirely through the additive mask, +layer by layer. (Mistral-style models instead fold their window into the model-level +mask and have no sliding layers; they are the ``attn.gqa`` seam, not this one.) + +Bidirectionalizing therefore needs TWO coordinated anneals over one shared +``AnnealState``: + +* the ``_update_causal_mask`` wrap (reused verbatim from the ``attn.gqa`` seam via + ``install_gqa_anneal_patch``) reveals strictly-future cells. This opens every + layer's future AND feeds the sliding layers their future-revealed base mask. +* a per-sliding-layer ``forward`` wrap reveals the strictly-far-past cells the layer + would otherwise window out. + +Both use the same penalty ramp as the GPT-2/GQA seams: ``finfo(dtype).min`` at +``alpha=0`` (so each reveal is bit-identical to base -> the D13 identity gate reads +``max_abs_diff == 0.0``) and ``clamp(log(alpha), finfo.min)``, reaching ``0`` at +``alpha=1`` (fully non-causal AND unwindowed). Global layers are untouched by the +second wrap; they open through the first alone. RoPE (Gemma 3's per-layer local vs +global theta), query-key norm, the GQA/MQA layout, RMSNorm, logit softcapping +(Gemma 2), and Gemma's sqrt(hidden) embedding scaling all stay in HF's own forward - +only the additive mask is patched, exactly as the Gemma 1 seam does. + +The far-past wrap temporarily flips the decoder layer's ``is_sliding`` to ``False`` +so HF's own (hard ``finfo.min``) window re-mask is skipped for that one call, and +supplies its annealed replacement instead. It does NOT touch ``self_attn.is_sliding``, +which selects the local RoPE embedding, so positions stay exactly as base computes +them. Overrides live on module INSTANCES (shadowing the class methods), so a sibling +un-patched base model in the same process - the identity gate's reference copy - +keeps its original causal+windowed attention. """ from __future__ import annotations diff --git a/packages/a2d-worker-hf/src/a2d_core/worker.py b/packages/a2d-worker-hf/src/a2d_core/worker.py index 4838f8f..76c7b37 100644 --- a/packages/a2d-worker-hf/src/a2d_core/worker.py +++ b/packages/a2d-worker-hf/src/a2d_core/worker.py @@ -170,13 +170,12 @@ def _convert(job: ConversionJob, emit: Callable[[dict[str, Any]], None]) -> int: emit(progress("grow", 2, total)) mask_token_id = resolve_mask_token(model, tokenizer, cfg.mask_token) - # 4. patch: install the annealed attention seam at alpha=0 (Decision 2). All three - # seams register one annealed mask function in HF's mask interface; which - # capability applies is resolved from the model's own eager structure: - # GPT2Attention (attn.full) vs num_key_value_heads (attn.gqa - Gemma/Qwen2/Llama/ - # Mistral) vs config.layer_types carrying sliding layers (attn.swa - Gemma 2/3). - # The ConversionJob carries no capability set, so the worker picks the handler - # honestly from the model itself. + # 4. patch: install the annealed attention seam at alpha=0 (Decision 2). The seam + # is resolved from the model's own eager causal structure: GPT-2's self.bias + # (attn.full) vs the RoPE family's _update_causal_mask mask (attn.gqa - Gemma/ + # Qwen2/Llama) vs that same mask plus per-layer sliding windows (attn.swa - + # Gemma 2/3). The ConversionJob carries no capability set, so the worker picks + # the handler honestly from the model itself. # ponytail: attention seam only; feed the manifest's model_spec.capabilities # through the job when P6 adds sink handlers keyed off config-only fields. emit(progress("patch", 3, total)) diff --git a/packages/a2d-worker-hf/tests/conftest.py b/packages/a2d-worker-hf/tests/conftest.py index 7ef4e61..393739e 100644 --- a/packages/a2d-worker-hf/tests/conftest.py +++ b/packages/a2d-worker-hf/tests/conftest.py @@ -14,9 +14,7 @@ @pytest.fixture def tiny_gpt2() -> Callable[..., Any]: """Factory for a seeded tiny GPT-2 (eager, eval, no download). Same seed => - bit-identical weights, so two calls give an aligned base/patched pair. Extra - ``**overrides`` go straight to ``GPT2Config`` (e.g. ``reorder_and_upcast_attn=True`` - for the install-rejection guard).""" + bit-identical weights, so two calls give an aligned base/patched pair.""" def _make(seed: int = 0, **overrides: Any) -> Any: import torch @@ -133,13 +131,13 @@ def tiny_gemma2() -> Callable[..., Any]: """Factory for a seeded tiny Gemma 2 (eager, eval, no download). Same ``(kwargs, seed)`` => bit-identical weights, so two calls give an aligned - base/patched pair. Exercises the Gemma 2 flavor of the sliding-window seam: the mask - logic is identical to Gemma 3's, but Gemma 2's own eager attention adds logit - softcapping - which the seam must leave to HF's forward, since it resolves each - family's ``eager_attention_forward`` rather than substituting one - and every EVEN - layer slides. Defaults (2 layers) put both a local layer (0) and a global layer (1) - in the stack; pass ``num_hidden_layers=1`` for an all-sliding model whose single - layer's receptive field is exactly the window.""" + base/patched pair. Exercises the Gemma 2 per-layer sliding-window seam: the window + logic is identical to Gemma 3's, but the decoder layer's ``forward`` takes ONE + ``position_embeddings`` pair where Gemma 3 takes a global/local pair - the + signature the swa wrapper must not hardcode away - and every EVEN layer slides. + Defaults (2 layers) put both a local layer (0) and a global layer (1) in the + stack; pass ``num_hidden_layers=1`` for an all-sliding model whose single layer's + receptive field is exactly the window.""" def _make(seed: int = 0, num_hidden_layers: int = 2, sliding_window: int = 2) -> Any: import torch @@ -171,10 +169,10 @@ def tiny_mistral() -> Callable[..., Any]: sliding window (default 2, well under the tests' seq_len 8). Same ``(kwargs, seed)`` => bit-identical weights, so two calls give an aligned - base/patched pair. Mistral folds its window into the single model-level 4D mask and - declares no ``config.layer_types``, so it routes to ``attn.gqa`` - the single-mask - windowed flavor whose far-past cells the shared reveal must open alongside the - strictly-future ones.""" + base/patched pair. Mistral folds its window into the single model-level 4D mask + ``_update_causal_mask`` builds and has NO sliding decoder layers, so it routes to + ``attn.gqa`` - the single-mask windowed flavor whose far-past cells the shared + reveal must open alongside the strictly-future ones.""" def _make(seed: int = 0, sliding_window: int = 2) -> Any: import torch diff --git a/packages/a2d-worker-hf/tests/test_gqa_attention.py b/packages/a2d-worker-hf/tests/test_gqa_attention.py index 8f5be8f..51cad60 100644 --- a/packages/a2d-worker-hf/tests/test_gqa_attention.py +++ b/packages/a2d-worker-hf/tests/test_gqa_attention.py @@ -2,8 +2,8 @@ at ``alpha=0`` (bit-identical to base) and the ``alpha=1`` bidirectional-behavior guard, plus the worker's structural handler dispatch. -Mirrors ``test_identity``/``test_bidir`` (GPT-2) for the RoPE family, whose eager seam is -the single model-level 4D mask HF builds through its mask interface. Also +Mirrors ``test_identity``/``test_bidir`` (GPT-2) for the RoPE family, whose eager +seam is the 4D mask ``_update_causal_mask`` builds, not GPT-2's ``self.bias``. Also covers the single-mask windowed flavor (Mistral v0.1), whose sliding window is folded into that same model-level mask: the shared reveal must open its far-past window alongside the strictly-future cells, so a converted Mistral is never silently still @@ -132,7 +132,7 @@ def test_resolve_capabilities_picks_gqa_for_rope_family(tiny_gqa: Callable[..., def test_resolve_capabilities_keeps_gpt2_on_full(tiny_gpt2: Callable[..., Any]) -> None: - """GPT-2 declares no num_key_value_heads, so it must keep selecting attn.full.""" + """GPT-2 must keep selecting its own attn.full self.bias seam, unchanged.""" assert resolve_capabilities(tiny_gpt2(0)) == ["attn.full"] @@ -209,10 +209,9 @@ def test_gqa_padding_stays_masked_and_identity_holds_with_padding( def test_gqa_reinstall_with_fresh_state_takes_effect( tiny_gqa: Callable[..., Any], family: str ) -> None: - """Re-installing on an already-patched model must swap in the NEW state: the - implementation key is derived from the state, so a second install registers and - selects a fresh key rather than being silently ignored. After a second install at - alpha=1, an earlier position sees a perturbed future token.""" + """Re-installing on an already-patched model must swap in the NEW state (parity + with the GPT-2 seam's per-install re-tag) without double-wrapping: after a second + install at alpha=1, an earlier position sees a perturbed future token.""" model = tiny_gqa(family, 0) install_gqa_anneal_patch(model, AnnealState(alpha=0.0)) install_gqa_anneal_patch(model, AnnealState(alpha=1.0)) diff --git a/packages/a2d-worker-hf/tests/test_swa_attention.py b/packages/a2d-worker-hf/tests/test_swa_attention.py index 7a6d749..60fcbee 100644 --- a/packages/a2d-worker-hf/tests/test_swa_attention.py +++ b/packages/a2d-worker-hf/tests/test_swa_attention.py @@ -4,14 +4,14 @@ and the window-open (the sliding window's far-past opens) - plus the worker's structural handler dispatch. -The Gemma 2/3 eager seam is the model-level causal mask (like Gemma 1) PLUS a -sliding-window mask on the layers ``config.layer_types`` labels ``"sliding_attention"``; -both are built through the one mask interface a2d registers into. The window test uses an -all-sliding single-layer model so a query's receptive field is exactly its window; the -identity/future tests use a mixed 4-layer stack (local layers 0, 2 and global layers 1, 3). -Gemma 2 is covered alongside Gemma 3 because its own eager attention adds logit -softcapping, which the seam must leave to HF's forward. All hermetic: tiny random-weight -configs on CPU float32, no network. +The Gemma 2/3 eager seam is the single 4D ``_update_causal_mask`` (like Gemma 1) PLUS +a per-layer far-past window re-mask on the local (sliding) decoder layers. The window +test uses an all-sliding single-layer model so a query's receptive field is exactly +its window; the identity/future tests use a mixed 4-layer stack (local layers 0, 2 and +global layers 1, 3). Gemma 2 - whose decoder layer takes a single +``position_embeddings`` pair where Gemma 3 takes a global/local pair - guards the +wrapper's signature-agnostic re-bind. All hermetic: tiny random-weight configs on CPU +float32, no network. """ from __future__ import annotations @@ -201,10 +201,10 @@ def test_swa_padding_stays_masked_and_identity_holds_with_padding( def test_swa_gemma2_patched_at_alpha0_is_bit_identical_to_base( tiny_gemma2: Callable[..., Any], ) -> None: - """Regression (review: swa-gemma2-signature-crash): the Gemma 2 stack must survive the - install without crashing - its eager attention differs from Gemma 3's (logit - softcapping) - and patched@alpha=0 logits must equal base to 0.0 with both a local and - a global layer exercised.""" + """Regression (review: swa-gemma2-signature-crash): Gemma 2's decoder layer takes a + single ``position_embeddings`` pair, so the wrapper must survive its keyword call + path without crashing, and patched@alpha=0 logits must equal base to 0.0 with both + a local and a global layer exercised.""" base = tiny_gemma2() patched = tiny_gemma2() patched.load_state_dict(base.state_dict()) # guarantee identical weights @@ -225,7 +225,7 @@ def test_swa_gemma2_patched_at_alpha0_is_bit_identical_to_base( def test_swa_gemma2_opens_window_and_future_at_alpha1_not_alpha0( tiny_gemma2: Callable[..., Any], ) -> None: - """Gemma 2 end-to-end reveal through the shared mask seam: on an + """Gemma 2 end-to-end reveal through the signature-agnostic wrapper: on an all-sliding single-layer model an in-window key moves the query's logits even at alpha=0 (non-vacuity), while a far-past out-of-window key AND a strictly-future key move them at alpha=1 only.""" @@ -250,9 +250,9 @@ def test_swa_gemma2_opens_window_and_future_at_alpha1_not_alpha0( def test_swa_wrapped_layer_survives_gradient_checkpointing( tiny_gemma2: Callable[..., Any], tiny_gemma3: Callable[..., Any] ) -> None: - """The checkpointed call path: under gradient checkpointing the model invokes the - decoder layer with every argument positional, and the annealed mask must still be built - and backprop through it, for both the Gemma 2 and Gemma 3 shapes.""" + """The positional call path: under gradient checkpointing the model invokes the + decoder layer with every argument positional, which the wrapper must re-bind + against the original signature for both the Gemma 2 and Gemma 3 shapes.""" for model in (tiny_gemma2(), tiny_gemma3()): state = AnnealState(alpha=0.5) install_swa_anneal_patch(model, state) @@ -278,9 +278,8 @@ def test_swa_install_on_non_sliding_model_raises_and_leaves_model_unpatched( def test_swa_reinstall_with_fresh_state_takes_effect(tiny_gemma3: Callable[..., Any]) -> None: - """Re-installing on an already-patched model must swap in the NEW state: the - implementation key is derived from the state, so a second install registers and selects - a fresh key. After a second install at alpha=1, an out-of-window token reaches the + """Re-installing on an already-patched model must swap in the NEW state without + double-wrapping: after a second install at alpha=1, an out-of-window token reaches the query.""" model = tiny_gemma3(num_hidden_layers=1, sliding_window=2, sliding_window_pattern=6) install_swa_anneal_patch(model, AnnealState(alpha=0.0)) diff --git a/pyproject.toml b/pyproject.toml index baf944e..e37c620 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,12 +38,11 @@ strict = true files = ["packages"] exclude = "packages/a2d-contracts/src/a2d_contracts/models" -# transformers 5.14.1 still ships an (empty) py.typed marker, so mypy would otherwise -# follow it and check the worker against v5's inline annotations - which disagree with -# the runtime for how we use it (config fields arriving via `**kwargs`, -# `Trainer.compute_loss`), reintroducing 68 errors. Keep treating transformers as -# untyped (Any); `follow_imports=skip` is what makes that hold now that the marker -# exists - `ignore_missing_imports` alone no longer suffices. tokenizers is the same. +# transformers 4.51.3 ships an (empty) py.typed marker, but the worker's transform +# layer patches HF's eager attention seams directly with runtime-only knowledge mypy +# cannot follow, so keep treating transformers as untyped (Any). `follow_imports=skip` +# is what makes that hold now that the marker exists - `ignore_missing_imports` alone +# no longer suffices. tokenizers is treated the same way. [[tool.mypy.overrides]] module = ["transformers", "transformers.*", "tokenizers", "tokenizers.*"] ignore_missing_imports = true