lumierenoir commited on
Commit
d0bef41
·
verified ·
1 Parent(s): ff0284e

Usage: validated in-process and HTTP paths

Browse files

klev is a library first, so the card now leads with an in-process call rather than a server. model/infer.py is the shared core: it reuses data/encoding.py, data/format.py and model/head.py with the scorer rather than reimplementing them, so the probabilities a user sees are the ones eval/eval_decisions.py measures. Plus examples/system_one.py for the shell and serve/server.py for when HTTP is actually wanted.

Every snippet was executed. Real output is pasted, not illustrative: noul p(true)=0.9169, choice atm 0.6136, score 1.485, rejection 0.168 and 0.027. The temperature table is called out because head.pt stores 1.0 (written during training; the head only tempers in eval mode) and serving raw costs 0.08 ECE.

The server routes match kev's own shapes, so eval/eval_systemone_http.py scores it unmodified -- verified over 120 decision-v7 development rows, zero failures. Also records that the ~195-token ceiling on the RX 6600M is a training limit only: a 703-token row was served 200 with the correct answer on that card.

Files changed (1) hide show
  1. README.md +97 -23
README.md CHANGED
@@ -239,33 +239,107 @@ Probe hyperparameters (in `stitch/probe-*.pt`): TweetEval `T = 105.19`, `beta =
239
 
240
  ## Usage
241
 
242
- Built and served with Unsloth — `import unsloth` must come **before** `transformers` / `peft`
243
- (it patches them at import time), which is why it is first below.
 
 
244
 
245
  ```python
246
- import unsloth # noqa: F401 — must precede transformers / peft / trl
247
- import torch
248
- from huggingface_hub import snapshot_download
249
- from peft import PeftModel
250
- from unsloth import FastModel
251
-
252
- DELIMITERS = ["<|fim_prefix|>", "<|fim_suffix|>", "<|fim_middle|>",
253
- "<|fim_pad|>", "<|repo_name|>"]
254
-
255
- ckpt = snapshot_download("lumierenoir/klev-0.8b", allow_patterns=["adapter/*", "head.pt"])
256
- model, processor = FastModel.from_pretrained(
257
- "Qwen/Qwen3.5-0.8B", max_seq_length=2048, load_in_4bit=True)
258
- tokenizer = getattr(processor, "tokenizer", processor)
259
- tokenizer.add_special_tokens({"additional_special_tokens": DELIMITERS})
260
- model = PeftModel.from_pretrained(model, f"{ckpt}/adapter")
261
- saved = torch.load(f"{ckpt}/head.pt", map_location="cpu", weights_only=False)
262
- # apply saved["deltas"]["embed_tokens"] to the 5 delimiter rows and saved["head"] to
263
- # PointerHead(1024, dp=256, garbage=True), then read decisions exactly as in
264
- # eval/eval_decisions.py (code repo).
265
  ```
266
 
267
- The 5 delimiter ids are 248060, 248062, 248061, 248063, 248064 on this base. If you change the
268
- base, the ids must be re-derived with `tokenizer.convert_tokens_to_ids` — do not assume them.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
  ## License and data
271
 
 
239
 
240
  ## Usage
241
 
242
+ klev is a **library first**. A decision model is small and stateless, so the normal way to use
243
+ one is in-process: load it once, call it as many times as you like. No server, no port, no
244
+ daemon. `import unsloth` must come **before** `transformers` / `peft` (it patches them at import
245
+ time), which is why it is first.
246
 
247
  ```python
248
+ import unsloth # noqa: F401 — must precede transformers / peft
249
+ from model.infer import load_klev, answer # from github.com/felipepenhorate/klev
250
+
251
+ model, tokenizer, head = load_klev("lumierenoir/klev-0.8b") # or a local dir with adapter/ + head.pt
252
+
253
+ result = answer(model, tokenizer, head, {
254
+ "state": "My card was declined twice at a supermarket and the ATM refused it too.",
255
+ "questions": {
256
+ "is_card": {"type": "noul", "instructions": "Is the card being declined?",
257
+ "criteria": {"false": "no", "true": "yes"}},
258
+ "action": {"type": "choice", "instructions": "What should the agent do first?",
259
+ "criteria": {"usage": "ask how the card was used",
260
+ "atm": "raise an ATM fault",
261
+ "fraud": "run a fraud check"}},
262
+ "urgency": {"type": "score", "instructions": "How urgent is this?",
263
+ "criteria": ["no action needed", "next working day", "same day", "immediate"]},
264
+ }},
265
+ temperature=2.2974)
 
266
  ```
267
 
268
+ Verified output from that request:
269
+
270
+ ```json
271
+ {"answers": {
272
+ "is_card": {"type": "noul", "noul": 0.9169, "none": 0.01314},
273
+ "action": {"type": "choice", "choice": "atm", "confidence": 0.4848,
274
+ "probabilities": {"usage": 0.1311, "atm": 0.6136, "fraud": 0.0870},
275
+ "none": 0.16828},
276
+ "urgency": {"type": "score", "score": 1.485, "legend": {"0": "no action needed",
277
+ "1": "next working day", "2": "same day", "3": "immediate"},
278
+ "probabilities": {"0": 0.2936, "1": 0.2397, "2": 0.1817, "3": 0.2580, "4": 0.0270},
279
+ "confidence": 0.6288, "none": 0.026994}}}
280
+ ```
281
+
282
+ `none` is the **rejection channel**: the mass on "none of these apply", renormalised off the K
283
+ options so the returned distribution still sums to 1.
284
+
285
+ The three question types are `noul` (`criteria` = `{false, true}`), `choice` (`criteria` = an
286
+ option map) and `score` (`criteria` = an ordered level list). A request is validated by
287
+ klev's own `SystemOneRequest` before any GPU work, and labels / `target` / `_meta` are ignored —
288
+ so a labelled eval record can be passed straight through.
289
+
290
+ **Temperature matters.** `answer(..., temperature=1.0)` is the **raw** readout, which is what the
291
+ benchmark tables above score (ECE 0.113 on decision-v7 dev). The *served* numbers use the
292
+ per-model temperature from `scripts/calibrate.py`:
293
+
294
+ | arm | serve at |
295
+ |---|---|
296
+ | `lumierenoir/klev-0.8b` (IT) | **2.2974** |
297
+ | `lumierenoir/klev-0.8b` + `/base` | **2.2449** |
298
+
299
+ `head.pt` stores `1.0`, because it is written during training and the head only tempers in eval
300
+ mode — so a deployment has to supply the fitted value itself. That is the one thing easy to get
301
+ wrong, and it is worth 0.08 ECE.
302
+
303
+ ### From a shell, still no server
304
+
305
+ ```bash
306
+ python examples/system_one.py --ckpt lumierenoir/klev-0.8b # built-in demo
307
+ python examples/system_one.py --ckpt lumierenoir/klev-0.8b --request r.json # your request
308
+ echo '{"state":"...","questions":{...}}' | python examples/system_one.py --ckpt ... --request -
309
+ ```
310
+
311
+ which ends with a plain-language readback — the quickest way to sanity-check a checkpoint. The
312
+ built-in demo request (a card decline, with 2 options and a 4-level severity scale):
313
+
314
+ ```
315
+ channel_issue noul p(true)=0.885 -> yes reject=0.015
316
+ next_step choice -> atm_fault conf=0.371 reject=0.193
317
+ severity score -> 1.52 reject=0.025
318
+ ```
319
+
320
+ ### If you would rather have HTTP
321
+
322
+ Worth it when several processes share one warm copy of the weights, or when the caller is not
323
+ Python. Otherwise it adds a port and a failure mode to save ~1 GB of load time.
324
+
325
+ ```bash
326
+ python serve/server.py --ckpt lumierenoir/klev-0.8b --port 8090 --temperature 2.2974
327
+ curl -s localhost:8090/v1/systemone -H 'content-type: application/json' -d @request.json
328
+ ```
329
+
330
+ | route | |
331
+ |---|---|
332
+ | `POST /v1/systemone` | `{"state", "questions"}` → `{"answers": {qid: typed answer}}` |
333
+ | `GET /v1/models` | loaded base, temperature, whether the rejection channel exists |
334
+ | `GET /health` | `{"ok": true, "model": ...}` |
335
+
336
+ Same routes and response shapes as `kev`'s own server, so `eval/eval_systemone_http.py` scores
337
+ it on the bench records **unmodified** — verified here over 120 decision-v7 development rows with
338
+ zero failures. It is stdlib `http.server` on purpose: no fastapi/uvicorn to install. One
339
+ decision per request under a lock, since a forward is not re-entrant.
340
+
341
+ Validated locally: 703-token decision-v7 row served `200` with the correct answer, malformed
342
+ bodies `400`, unknown routes `404`, and rows over `--max-seq` `400` naming the token count.
343
 
344
  ## License and data
345