Skip to content

Commit d351814

Browse files
committed
feat: make Qwen3.5 quickstart installable
1 parent b46c656 commit d351814

11 files changed

Lines changed: 314 additions & 137 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ software, so compatibility can still change between development versions.
1212
- `PackedRecurrentStateCache` for keeping recurrent states packed between layer
1313
calls, with opt-in evidence recording and exact resident-byte accounting.
1414
- A guarded Qwen3.5 cache factory, pinned Qwen3.5 quickstart, and compatibility
15-
checks for the tested Transformers minor release and eager, single-device
15+
checks for the tested Transformers release and eager, single-device
1616
inference path.
17+
- A reusable frozen v0.2 mixed-policy cache helper and an installed
18+
`recurquant qwen35` workflow shared with the source-tree quickstart. Uniform
19+
INT4 remains available only as an explicitly named stress baseline.
1720
- Frozen MBPP calibration and development evaluation workflows with pinned
1821
dataset/model revisions, prepared token manifests, canonical evidence hashes,
1922
equal-byte baselines, and resumable per-task checkpoints.
@@ -25,9 +28,10 @@ software, so compatibility can still change between development versions.
2528
- Scale-storage emulation now uses the declared physical FP16 or FP32 format;
2629
superseding diagnostic results retain the earlier record instead of rewriting
2730
it.
28-
- The Transformers dependency is constrained to `>=5.14.1,<5.15` while the
29-
package depends on that minor release's internal linear-attention cache
30-
contract.
31+
- The Transformers dependency is pinned to exactly `5.14.1` while the alpha
32+
package depends on that release's internal linear-attention cache contract.
33+
- FP16 scale storage is identified as the evaluated default. FP32 scale storage
34+
remains supported for experiments but is not covered by full-model evidence.
3135

3236
### Known limitations
3337

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ In particular:
5353
them.
5454

5555
The current supported package boundary is recorded in
56-
[docs/compatibility.md](docs/compatibility.md). A new model, Transformers minor
56+
[docs/compatibility.md](docs/compatibility.md). A new model, Transformers
5757
release, execution backend, generation mode, or hardware target is unsupported
5858
until it has a regression test and clearly scoped full-model evidence.
5959

README.md

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,48 @@ The commands are short; the first run still needs to download the pinned model
2020
and tokenizer. Python 3.11 and a CUDA GPU are recommended for the evaluated
2121
path.
2222

23+
Windows PowerShell:
24+
2325
```powershell
2426
git clone https://github.com/Labeeb2339/recurquant.git
2527
cd recurquant
2628
py -3.11 -m venv .venv
2729
.\.venv\Scripts\python.exe -m pip install .
28-
.\.venv\Scripts\python.exe examples\qwen35_quickstart.py --max-new-tokens 16
30+
.\.venv\Scripts\recurquant.exe qwen35 --max-new-tokens 16
31+
```
32+
33+
macOS or Linux:
34+
35+
```bash
36+
git clone https://github.com/Labeeb2339/recurquant.git
37+
cd recurquant
38+
python3.11 -m venv .venv
39+
.venv/bin/python -m pip install .
40+
.venv/bin/recurquant qwen35 --max-new-tokens 16
2941
```
3042

31-
On macOS or Linux, replace `.\.venv\Scripts\python.exe` with
32-
`.venv/bin/python`. The runnable source is
33-
[`examples/qwen35_quickstart.py`](examples/qwen35_quickstart.py); read the
43+
The installed command and
44+
[`examples/qwen35_quickstart.py`](examples/qwen35_quickstart.py) call the same
45+
implementation. The default is the frozen v0.2 mixed policy: layer 0 at INT8
46+
and the remaining recurrent layers at INT4. Uniform INT4 is retained only as an
47+
explicit stress baseline via `--policy uniform-int4-stress`. Read the
3448
[compatibility contract](docs/compatibility.md) before using a different model,
3549
Transformers version, device layout, or generation mode.
3650

3751
## Use it in Python
3852

39-
This example keeps Gated DeltaNet layer 0 at INT8 and the other 17 recurrent
40-
layers at INT4, matching the frozen development policy. Remove `layer_specs`
41-
for uniform INT4.
53+
This example uses the reusable frozen v0.2 helper, which keeps Gated DeltaNet
54+
layer 0 at INT8 and the other 17 recurrent layers at INT4. The generic
55+
`create_qwen35_packed_cache()` factory remains available for controlled policy
56+
experiments.
4257

4358
```python
4459
import warnings
4560

4661
import torch
4762
from transformers import AutoModelForCausalLM, AutoTokenizer
4863

49-
from recurquant import QuantizationSpec, create_qwen35_packed_cache
64+
from recurquant import create_qwen35_v02_mixed_cache
5065

5166
MODEL_ID = "Qwen/Qwen3.5-0.8B-Base"
5267
REVISION = "dc7cdfe2ee4154fa7e30f5b51ca41bfa40174e68"
@@ -74,12 +89,7 @@ model = AutoModelForCausalLM.from_pretrained(
7489
).to(device)
7590
model.eval()
7691

77-
cache = create_qwen35_packed_cache(
78-
model,
79-
bits=4,
80-
group_size=128,
81-
layer_specs={0: QuantizationSpec(bits=8, group_size=128)},
82-
)
92+
cache = create_qwen35_v02_mixed_cache(model)
8393
inputs = tokenizer("Explain recurrent-state quantization simply.", return_tensors="pt")
8494
inputs = inputs.to(device)
8595
continuation = []
@@ -102,10 +112,11 @@ print(tokenizer.decode(generated_ids[0], skip_special_tokens=True))
102112
print(cache.storage_summary())
103113
```
104114

105-
`create_qwen35_packed_cache()` rejects unsupported Transformers versions,
106-
non-eager attention, training mode, multi-device placement, and incompatible
107-
Qwen configurations early. The returned cache exposes exact live tensor byte
108-
accounting through `storage_summary()`.
115+
`create_qwen35_v02_mixed_cache()` and `create_qwen35_packed_cache()` reject
116+
unsupported Transformers versions, non-eager attention, training mode,
117+
multi-device placement, and incompatible Qwen configurations early. The
118+
returned cache exposes exact live tensor byte accounting through
119+
`storage_summary()`.
109120

110121
## What is physically smaller
111122

@@ -150,10 +161,12 @@ Important boundaries:
150161

151162
The supported public surface is deliberately narrow:
152163

153-
- Python `>=3.11` and `transformers>=5.14.1,<5.15`;
164+
- Python `>=3.11` and exactly `transformers==5.14.1` for this alpha;
154165
- text-only Qwen3.5 hybrid models with `linear_attention` and `full_attention`
155166
layer types;
156-
- physical INT4 or INT8 recurrent-state payloads with FP16 scales;
167+
- physical INT4 or INT8 recurrent-state payloads; FP16 scales are the evaluated
168+
default, while FP32 scales are supported as an experimental, unevaluated
169+
option;
157170
- eager, evaluation-only, single-device inference; and
158171
- explicit `past_key_values=cache` model calls.
159172

docs/compatibility.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,27 @@ model quality, speed, or production readiness.
1010
| Area | Current evidence | Support boundary |
1111
|---|---|---|
1212
| Python | The full MBPP development run used Python `3.11.15`. Packaging CI is configured to smoke-test Python `3.11` and `3.13` on Linux, plus Python `3.11` on Windows. | Python `>=3.11` is declared. Full-model numerical evidence is limited to `3.11.15`. |
13-
| Transformers | `5.14.1` | The dependency is deliberately constrained to `transformers>=5.14.1,<5.15`. Other minor versions are unsupported until their internal cache contract is tested. |
13+
| Transformers | `5.14.1` | The alpha dependency is deliberately pinned to `transformers==5.14.1`. Every other release is unsupported until its internal cache contract is tested. |
1414
| Model | `Qwen/Qwen3.5-0.8B-Base` at revision `dc7cdfe2ee4154fa7e30f5b51ca41bfa40174e68` | No other checkpoint, revision, model family, or recurrent architecture has full-model evidence. |
1515
| Model execution | BF16 weights, batch size one, evaluation mode, `trust_remote_code=False`, eager attention | Use `attn_implementation="eager"`. Flash, SDPA, and other attention implementations are not validated. |
1616
| Full-run environment | PyTorch `2.11.0+cu128`, CUDA runtime `12.8`, NVIDIA driver `592.15`, NVIDIA GeForce RTX 5070 Laptop GPU, recorded platform `Windows-10-10.0.26200-SP0` | CPU and other accelerator support is limited to unit or API smoke tests; the public numerical result was not replicated there. |
17-
| Packed formats | Physical INT4 and INT8 recurrent-state payloads with FP16 scales | The packed cache does not accept other payload widths. |
17+
| Packed formats | Physical INT4 and INT8 recurrent-state payloads. FP16 scales are the evaluated default. | FP32 scales are supported for experiments but have no full-model fidelity evidence. The packed cache does not accept other payload widths. |
1818

1919
The exact model and software provenance is recorded in
2020
[`evidence/mbpp-v02-development.json`](../evidence/mbpp-v02-development.json).
2121

22+
## Installed quickstart
23+
24+
`recurquant qwen35` and `examples/qwen35_quickstart.py` use one shared
25+
implementation. Both default to the frozen v0.2 mixed policy: model layer 0 at
26+
INT8 and every other recurrent layer at INT4, with group size 128 and FP16
27+
scales. `--policy uniform-int4-stress` is retained only for reproducing the
28+
uniform INT4 stress baseline.
29+
30+
The command downloads the pinned model and tokenizer unless
31+
`--local-files-only` is supplied. It performs manual greedy decoding; the
32+
full-checkpoint quality of free-running generations has not been evaluated.
33+
2234
## Generation and cache modes
2335

2436
The following paths have direct evidence:

examples/qwen35_quickstart.py

Lines changed: 3 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,6 @@
1-
"""Run a pinned Qwen3.5 model with RecurQuant's packed recurrent-state cache."""
2-
3-
from __future__ import annotations
4-
5-
import argparse
6-
import warnings
7-
8-
import torch
9-
from transformers import AutoModelForCausalLM, AutoTokenizer
10-
11-
from recurquant import create_qwen35_packed_cache
12-
13-
MODEL_ID = "Qwen/Qwen3.5-0.8B-Base"
14-
MODEL_REVISION = "dc7cdfe2ee4154fa7e30f5b51ca41bfa40174e68"
15-
16-
17-
def _arguments() -> argparse.Namespace:
18-
parser = argparse.ArgumentParser(description=__doc__)
19-
parser.add_argument(
20-
"--prompt",
21-
default="Explain recurrent-state quantization in two sentences.",
22-
)
23-
parser.add_argument("--max-new-tokens", type=int, default=32)
24-
parser.add_argument("--device", choices=("auto", "cpu", "cuda"), default="auto")
25-
return parser.parse_args()
26-
27-
28-
def _device(requested: str) -> torch.device:
29-
if requested == "auto":
30-
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
31-
if requested == "cuda" and not torch.cuda.is_available():
32-
raise RuntimeError("--device cuda was requested, but CUDA is not available")
33-
return torch.device(requested)
34-
35-
36-
def _model_dtype(device: torch.device) -> torch.dtype:
37-
if device.type != "cuda":
38-
return torch.float32
39-
if torch.cuda.is_bf16_supported():
40-
return torch.bfloat16
41-
warnings.warn(
42-
"CUDA BF16 is unavailable; falling back to FP16. RecurQuant's public "
43-
"full-model fidelity evidence has not been validated for FP16 weights.",
44-
RuntimeWarning,
45-
stacklevel=2,
46-
)
47-
return torch.float16
48-
49-
50-
def main() -> None:
51-
args = _arguments()
52-
if args.max_new_tokens <= 0:
53-
raise ValueError("--max-new-tokens must be positive")
54-
55-
device = _device(args.device)
56-
dtype = _model_dtype(device)
57-
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, revision=MODEL_REVISION)
58-
model = AutoModelForCausalLM.from_pretrained(
59-
MODEL_ID,
60-
revision=MODEL_REVISION,
61-
dtype=dtype,
62-
attn_implementation="eager",
63-
).to(device)
64-
model.eval()
65-
66-
cache = create_qwen35_packed_cache(model, bits=4, group_size=128)
67-
encoded = tokenizer(args.prompt, return_tensors="pt").to(device)
68-
generated: list[torch.Tensor] = []
69-
70-
with torch.inference_mode():
71-
output = model(**encoded, past_key_values=cache, use_cache=True)
72-
for step in range(args.max_new_tokens):
73-
next_token = output.logits[:, -1, :].argmax(dim=-1, keepdim=True)
74-
generated.append(next_token)
75-
reached_eos = (
76-
tokenizer.eos_token_id is not None
77-
and bool((next_token == tokenizer.eos_token_id).all().item())
78-
)
79-
if reached_eos or step + 1 == args.max_new_tokens:
80-
break
81-
output = model(input_ids=next_token, past_key_values=cache, use_cache=True)
82-
83-
generated_ids = torch.cat(generated, dim=1)
84-
print(tokenizer.decode(generated_ids[0], skip_special_tokens=True))
85-
86-
summary = cache.storage_summary()
87-
print(f"resident_recurrent_state_bytes={summary['resident_bytes']}")
88-
print(
89-
"full_precision_equivalent_recurrent_state_bytes="
90-
f"{summary['full_precision_equivalent_bytes']}"
91-
)
92-
print(
93-
"largest_materialized_recurrent_state_bytes="
94-
f"{summary['largest_materialized_state_bytes']}"
95-
)
96-
print(f"resident_compression_ratio={summary['resident_compression_ratio']:.3f}x")
1+
"""Run the installed RecurQuant Qwen3.5 quickstart workflow."""
972

3+
from recurquant.qwen35_quickstart import main
984

995
if __name__ == "__main__":
100-
main()
6+
raise SystemExit(main())

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ dependencies = [
3636
"safetensors>=0.5",
3737
"torch>=2.7",
3838
# PackedRecurrentStateCache subclasses an internal cache layer. Keep the
39-
# tested minor pinned until its compatibility contract is covered in CI.
40-
"transformers>=5.14.1,<5.15",
39+
# alpha on the exact tested release until another release is covered in CI.
40+
"transformers==5.14.1",
4141
]
4242

4343
[project.optional-dependencies]

src/recurquant/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
quantize_dequantize,
99
quantize_pack,
1010
)
11-
from .qwen35 import create_qwen35_packed_cache
11+
from .qwen35 import create_qwen35_packed_cache, create_qwen35_v02_mixed_cache
1212

1313
__all__ = [
1414
"PackedQuantizedTensor",
1515
"PackedRecurrentStateCache",
1616
"QuantizationResult",
1717
"QuantizationSpec",
1818
"create_qwen35_packed_cache",
19+
"create_qwen35_v02_mixed_cache",
1920
"quantize_dequantize",
2021
"quantize_pack",
2122
]

src/recurquant/cli.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Command-line entry points that do not require a model download."""
1+
"""Command-line entry points for RecurQuant."""
22

33
from __future__ import annotations
44

@@ -8,6 +8,7 @@
88
import torch
99

1010
from .quantization import QuantizationSpec, quantize_dequantize
11+
from .qwen35_quickstart import add_qwen35_arguments, run_qwen35_quickstart
1112

1213

1314
def _demo(args: argparse.Namespace) -> int:
@@ -47,6 +48,13 @@ def build_parser() -> argparse.ArgumentParser:
4748
demo.add_argument("--key-dim", type=int, default=16)
4849
demo.add_argument("--value-dim", type=int, default=16)
4950
demo.set_defaults(handler=_demo)
51+
52+
qwen35 = subparsers.add_parser(
53+
"qwen35",
54+
help="Run the pinned Qwen3.5 model with the packed recurrent-state cache.",
55+
)
56+
add_qwen35_arguments(qwen35)
57+
qwen35.set_defaults(handler=run_qwen35_quickstart)
5058
return parser
5159

5260

src/recurquant/qwen35.py

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@
2020
else:
2121
Qwen35Source = object
2222

23-
_SUPPORTED_TRANSFORMERS_REQUIREMENT = "transformers>=5.14.1,<5.15"
24-
_SUPPORTED_TRANSFORMERS_SPEC = SpecifierSet(">=5.14.1,<5.15")
23+
_SUPPORTED_TRANSFORMERS_REQUIREMENT = "transformers==5.14.1"
24+
_SUPPORTED_TRANSFORMERS_SPEC = SpecifierSet("==5.14.1")
2525
_SUPPORTED_LAYER_TYPES = frozenset({"linear_attention", "full_attention"})
2626

2727

@@ -41,9 +41,9 @@ def _validate_transformers_compatibility() -> Version:
4141
or parsed not in _SUPPORTED_TRANSFORMERS_SPEC
4242
):
4343
raise RuntimeError(
44-
"RecurQuant's Qwen3.5 cache is tested only with stable releases in "
44+
"RecurQuant's Qwen3.5 cache is tested only with "
4545
f"{_SUPPORTED_TRANSFORMERS_REQUIREMENT}; "
46-
f"found transformers=={installed}. Install the tested range before creating "
46+
f"found transformers=={installed}. Install the tested release before creating "
4747
"the cache."
4848
)
4949
return parsed
@@ -177,7 +177,7 @@ def create_qwen35_packed_cache(
177177
"""Create a validated packed recurrent-state cache for Qwen3.5 inference.
178178
179179
This factory covers text-only ``Qwen3_5ForCausalLM`` models on the tested stable
180-
Transformers minor release. Passing a model additionally validates evaluation
180+
Transformers release. Passing a model additionally validates evaluation
181181
mode, eager attention, and single-device materialization. Passing only a config
182182
validates its layer structure; it cannot validate the eventual model runtime.
183183
Every model forward must run under ``torch.inference_mode()`` or
@@ -213,3 +213,36 @@ def create_qwen35_packed_cache(
213213
layer_specs=layer_specs,
214214
record_evidence=record_evidence,
215215
)
216+
217+
218+
def create_qwen35_v02_mixed_cache(
219+
model_or_config: Qwen35Source,
220+
*,
221+
record_evidence: bool = False,
222+
) -> PackedRecurrentStateCache:
223+
"""Create the frozen v0.2 mixed-precision Qwen3.5 cache.
224+
225+
The policy stores model layer 0 at INT8 and every other supported recurrent
226+
layer at INT4, with group size 128, FP16 scales, nearest rounding, and seed
227+
2339. It is the fixed development policy reported by the repository, not an
228+
automatic selector for other checkpoints.
229+
"""
230+
231+
return create_qwen35_packed_cache(
232+
model_or_config,
233+
bits=4,
234+
group_size=128,
235+
scale_bits=16,
236+
rounding="nearest",
237+
seed=2339,
238+
layer_specs={
239+
0: QuantizationSpec(
240+
bits=8,
241+
group_size=128,
242+
scale_bits=16,
243+
rounding="nearest",
244+
seed=2339,
245+
)
246+
},
247+
record_evidence=record_evidence,
248+
)

0 commit comments

Comments
 (0)