File size: 5,933 Bytes
9644d0b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""
scripts/quant_int8_regression_test.py — E2E smoke test for 4b + int8 quantization
==================================================================================

Verifiziert, dass der Quantization-Pfad vom Server bis zum forward() läuft:
  1. HTTP 200 status, no error frame
  2. Assistant returns non-empty text
  3. Server log shows "quantized int8: ..." (not "quantization=none")
  4. Server log shows no fresh device-mismatch / RuntimeError

TDD-red: scheitert wenn:
  - quantization nicht durchgereicht wird (int8 nie aktiv)
  - QuantizedLinear-Buffers auf CPU landen (device mismatch)
  - Model-Loading crash

TDD-green: nach Fixes in
  - schemas.py: quantization field
  - server.py: passthrough to get_model
  - model_manager.py: registry-default fallback
  - quantized_linear.py: device-inheritance

Known limit: T > ~4200 tokens OOM'd auch mit int8 (4b + SigLIP vision encoder
sind immer noch zu groß für 12 GB bei langen Prefills). Bei T ≈ 4200
läuft die Generierung sauber.

Run:
    python scripts/quant_int8_regression_test.py
"""
import argparse
import json
import os
import sys
import time
from pathlib import Path

import httpx


REPO = Path(__file__).resolve().parent.parent
DEFAULT_LOG = REPO / "local_debug.log"
SERVER_URL = "https://localhost:7860/v1/chat/completions"
SSL_VERIFY = False


def _http_post_chat(model_id, prompt, preset="BASELINE", quantization="int8",
                    max_tokens=64, timeout=300.0):
    payload = {
        "model": model_id,
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": max_tokens,
        "temperature": 0.7,
        "stream": False,
        "px_config_preset": preset,
        "quantization": quantization,
    }
    try:
        with httpx.Client(timeout=timeout, verify=SSL_VERIFY) as client:
            r = client.post(SERVER_URL, json=payload)
        if r.status_code != 200:
            return r.status_code, "", f"HTTP {r.status_code}: {r.text[:300]!r}"
        body = r.json()
        text = body["choices"][0]["message"]["content"]
        return r.status_code, text, None
    except httpx.ConnectError as e:
        return 0, "", f"connect error: {e}"
    except Exception as e:
        return 0, "", f"exception: {type(e).__name__}: {e}"


def _scan_log_for_errors(log_path, size_before, needles):
    if not log_path.exists():
        return []
    bad = []
    with log_path.open("rb") as f:
        f.seek(size_before)
        for raw in f:
            try:
                line = raw.decode("utf-8", errors="replace")
            except Exception:
                continue
            if any(n in line for n in needles):
                bad.append(line.rstrip())
    return bad


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--model", default="gemma3-4b-it")
    # Realistic for 4b + int8 on RTX 2060 12 GB. Longer prompts OOM even with
    # quantization (SigLIP vision encoder + embeddings eat ~1.5 GB). At T≈4222
    # int8 path stays under 11.5 GB and forward completes.
    _LONG_BODY = (
        "Dies ist ein langer Text über das Phänomen der Selbstwahrnehmung "
        "in einem Sprachmodell. " * 200
    )
    ap.add_argument("--prompt", default=(
        "Bitte fasse den folgenden Text in zwei Sätzen zusammen:\n\n" + _LONG_BODY
    ))
    ap.add_argument("--preset", default="BASELINE")
    ap.add_argument("--quantization", default="int8")
    ap.add_argument("--max-tokens", type=int, default=64)
    ap.add_argument("--timeout", type=float, default=300.0)
    ap.add_argument("--log", default=str(DEFAULT_LOG))
    args = ap.parse_args()

    log_path = Path(args.log)
    log_size_before = log_path.stat().st_size if log_path.exists() else 0
    t0 = time.time()

    print(f"[E2E] POST {args.model} preset={args.preset} quant={args.quantization}")
    print(f"[E2E] prompt={args.prompt[:60]!r}...")
    status, text, err = _http_post_chat(
        args.model, args.prompt, preset=args.preset,
        quantization=args.quantization, max_tokens=args.max_tokens,
    )
    elapsed = time.time() - t0
    print(f"[E2E] status={status} text_len={len(text)} elapsed={elapsed:.1f}s")
    if err:
        print(f"[E2E] ERROR: {err}")

    print(f"[E2E] --- assistant ---")
    print(text[:800])
    print(f"[E2E] -------------------")

    # Check that quantization was actually applied (look at fresh log entries
    # since this run started; the model may already be loaded and the log line
    # is from earlier — that's still proof that int8 is active).
    q_applied = _scan_log_for_errors(log_path, 0, (
        f"{args.model} quantized int8", f"{args.model} quantization={args.quantization}",
    ))
    q_applied_fresh = _scan_log_for_errors(log_path, log_size_before, (
        f"{args.model} quantized int8", f"{args.model} quantization={args.quantization}",
    ))
    fresh_errs = _scan_log_for_errors(log_path, log_size_before, (
        "RuntimeError", "mat2 is on cpu", "different from other tensors on cuda:0",
        "Traceback", "OutOfMemoryError",
    ))

    failures = []
    if status != 200:
        failures.append(f"HTTP status {status} (expected 200)")
    if err:
        failures.append(f"stream error: {err}")
    if not text.strip():
        failures.append("empty assistant text")
    if not q_applied and not q_applied_fresh:
        failures.append(
            f"server log shows no 'quantized int8: ...' for {args.model} "
            f"(anywhere in log); quantization field did not reach _load_model")
    if fresh_errs:
        failures.append(
            f"server-side errors in log ({len(fresh_errs)} lines):")
        for l in fresh_errs[-6:]:
            failures.append(f"    {l[:200]}")

    if failures:
        print("\n[E2E] FAIL")
        for f in failures:
            print(f"  - {f}")
        sys.exit(1)
    print("\n[E2E] PASS — 4b int8 quantization regression clean")


if __name__ == "__main__":
    main()