Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<name>/{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/`. `transformers>=5` unified what used to be three distinct eager seams (GPT-2's `self.bias`, the RoPE family's `_update_causal_mask`, Gemma 2/3's per-layer window) into ONE: every family's 4D additive mask is built by `transformers.masking_utils` through `ALL_MASK_ATTENTION_FUNCTIONS[config._attn_implementation]`. `attention.py` `install_mask_anneal` registers a per-`AnnealState` key in that registry (plus `ALL_ATTENTION_FUNCTIONS`, same key) and points only that model's config at it, so isolation is per model — the identity gate's un-patched reference copy is unaffected. The reveal derives FROM `eager_mask`'s output (every base-masked cell with a real, non-padded key), which is why `alpha=0` is bit-identical by construction; it opens strictly-future cells, Mistral-style single-mask windows, and Gemma 2/3's separate `sliding_attention` mask alike. `attn.full`/`attn.gqa`/`attn.swa` stay distinct capabilities (detect's contract, registry keys) but are now three gates on one install: `resolve_capabilities` (`apply.py`) checks `config.layer_types` for `sliding_attention` (swa) before `num_key_value_heads` (gqa) before `GPT2Attention` (full), reading the model's own structure, not 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 `5.14.1`** (`packages/a2d-worker-hf/pyproject.toml`). Two gotchas: it still ships an (empty) `py.typed`, and its inline annotations disagree with the runtime (`**kwargs` config fields, `Trainer.compute_loss`), so the root `pyproject.toml` mypy override keeps `transformers`/`tokenizers` untyped via `follow_imports = "skip"` (not just `ignore_missing_imports`) — dropping it reintroduces 68 errors. And a silently-not-applied mask patch is the dangerous failure mode now that the seam is a registry key rather than a monkeypatch: `test_bidir.py::test_install_routes_the_model_and_only_it_through_the_annealed_seam` is the guard.
- **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).

Expand Down
11 changes: 5 additions & 6 deletions crates/a2d-contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,11 @@ impl Capability {

/// True ONLY for the five caps Phase 1 cannot handle. Every implemented
/// conversion cap - now including `AttnSwa`, whose sliding-window anneal the
/// worker performs for both flavors (per-layer Gemma 2/3 via the `attn.swa`
/// handler; single-mask Mistral/Qwen2 via the shared `attn.gqa` mask reveal) -
/// and every fidelity cap
/// returns false, so fidelity tags cannot block by construction. Flipping
/// `AttnSink` here later is the remaining "enable GPT-OSS" change
/// (ARCHITECTURE 5's "flip the gate").
/// worker performs for both flavors (a per-layer sliding mask, Gemma 2/3, via
/// the `attn.swa` handler; single-mask Mistral via the shared `attn.gqa` mask
/// reveal) - and every fidelity cap returns false, so fidelity tags cannot
/// block by construction. Flipping `AttnSink` here later is the remaining
/// "enable GPT-OSS" change (ARCHITECTURE 5's "flip the gate").
pub fn blocking(self) -> bool {
matches!(
self,
Expand Down
4 changes: 2 additions & 2 deletions docs/CONCEPTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ It predicts the next token left-to-right, and a **causal mask** enforces the rul
The block of positions it works on is the **canvas**; turning masks back into tokens over N steps is **denoising**.

3. **a2d's whole job** is the conversion between them: take AR weights, **anneal** the causal mask off (the `attn.full` / `attn.gqa` / `attn.swa` transforms), and briefly retrain with a masking objective (**MDLM**).
One transform per attention seam plus one objective. That is the core.
One transform per attention capability - all three sharing one mask seam - plus one objective. That is the core.

Everything else in the vocabulary exists to decide *which models a2d will touch* - the complexity lives in honest **detection**, not in the small **conversion**.

Expand All @@ -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. `transformers` v5 builds every family's mask through one shared interface, so there is ONE anneal seam with three capability gates: GPT-2's dense attention (`attn.full`); the RoPE family (Gemma 1 / Qwen2 / Llama / Mistral), which routes causality - and Mistral's window - through that single mask (`attn.gqa`); models whose `config.layer_types` names per-layer sliding layers - Gemma 2/3 - which additionally take a sliding-window mask from the same interface (`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.
Expand Down
3 changes: 2 additions & 1 deletion docs/PLAN-PHASE2.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# a2d Phase 2 Implementation Plan - Conversion core: dense happy path

**Status:** approved plan, pre-implementation.
**Superseded in part:** Decision 2's GPT-2 `self.bias` monkeypatch and the `transformers==4.48.3` pin below are historical - the shipped anneal installs through the transformers v5 mask interface in [`transform/attention.py`](../packages/a2d-worker-hf/src/a2d_core/transform/attention.py) - and this plan stays frozen as approved rather than being rewritten.
**Source spec:** [`SPEC-HANDOFF.md`](SPEC-HANDOFF.md) §4.2 (conversion pipeline: mask annealing, shift removal, identity gate, MDLM), §4.3 (run-dir target), §6 (Phase 2 scope and exit criteria), §9.4 (dllm wrap-vs-implement open decision); [`ARCHITECTURE.md`](ARCHITECTURE.md) M0 recipe (the three things conversion touches) and D13 (the identity test).

## Context
Expand Down Expand Up @@ -385,4 +386,4 @@ cat runs/gpt2-diffusion/manifest.json # status completed; model_spec + convers
Accepted with `uv` caching; an optional-extra dependency split is the documented upgrade path.

15. **Scope discipline: MoE-router-under-anneal + finetune (P4), BD3LM + schedulers (P5), the eval harness and eval-parity (P3), golden fixtures (candle-track), `head.py`, and the `datasets` dep are explicitly NOT built in P2.**
Flagged so they are not silently smuggled in; the MoE router monitor and eval parity in particular wait for their own phases because Phase 2 is the dense GPT-2 happy path only.
Flagged so they are not silently smuggled in; the MoE router monitor and eval parity in particular wait for their own phases because Phase 2 is the dense GPT-2 happy path only.
6 changes: 3 additions & 3 deletions docs/SPEC-HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,9 @@ 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)
attention.py # shared v5 mask-interface seam (all attn.*) + AnnealState/schedule
gqa_attention.py # RoPE-family gate on that shared seam (attn.gqa)
swa_attention.py # sliding-window-layer gate on that shared 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)
Expand Down
5 changes: 3 additions & 2 deletions packages/a2d-worker-hf/src/a2d_core/sample/denoiser.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ def denoise(

``prompt_ids`` is the untouched prefix; the ``canvas_len - len(prompt_ids)``
suffix positions start masked and are filled by iterative confidence reveal.
Installs the model's resolved attention transform (GPT-2 -> ``attn.full``, RoPE
family -> ``attn.gqa``) at ``alpha=1`` so attention is bidirectional.
Installs the model's resolved attention transform (Gemma 2/3 -> ``attn.swa``, RoPE
family -> ``attn.gqa``, GPT-2 -> ``attn.full``) at ``alpha=1`` so attention is
bidirectional.
"""
prompt_len = len(prompt_ids)
if canvas_len < prompt_len:
Expand Down
26 changes: 15 additions & 11 deletions packages/a2d-worker-hf/src/a2d_core/transform/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,21 @@ 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``.
"""The attention-handler capabilities a loaded model needs, chosen by the structure
of its attention stack (Decision 2). Three disjoint shapes exist in this scope:

- Gemma 2/3 give their local decoder layers a sliding-window mask, named by a
``sliding_attention`` entry in ``config.layer_types`` -> ``attn.swa``. Checked
FIRST, because these models are RoPE-family too (the ``attn.gqa`` signal); the
swa handler subsumes the gqa future-reveal and additionally opens the window.
- The RoPE family (Llama/Qwen2/Gemma 1, and Mistral, which folds its window into
the one model-level mask) declares ``num_key_value_heads`` -> ``attn.gqa``
(covers GQA, MQA, and full-attn RoPE alike; the mask is family-independent).
- GPT-2 has none of those and its own dense ``GPT2Attention`` -> ``attn.full``.

All three now share ONE eager seam - the ``transformers>=5`` mask interface (see
``transform/attention.py``) - but stay distinct capabilities because that is what
detect reports and what the handler registry is keyed on.

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,
Expand Down
Loading