Meanblock commited on
Commit
0e01057
·
verified ·
1 Parent(s): f7100b2

Add JEV-CPU: CPU port of SemIf + web UI

Browse files
Files changed (2) hide show
  1. README.md +1 -15
  2. docs/index.html +186 -1
README.md CHANGED
@@ -1,17 +1,3 @@
1
- ---
2
- license: mit
3
- base_model:
4
- - Qwen/Qwen3-0.6B
5
- tags:
6
- - semantic-if
7
- - decision
8
- - semif
9
- - cpu
10
- - zero-shot-classification
11
- pipeline_tag: zero-shot-classification
12
- library_name: transformers
13
- ---
14
-
15
  # JEV-CPU
16
 
17
  <div align="center">
@@ -270,7 +256,7 @@ MODEL = "openbmb/MiniCPM5-2B" # or "Qwen/Qwen3.5-4B"
270
 
271
  That is exactly what fixes the slip in the loan demo: `Qwen3-0.6B` flags *high risk* correctly but still leans *approve*; a 2B/4B brain keeps the secondary decision consistent. The trade-off is resources — a 4B model needs `transformers`' native Qwen3.5 support and more RAM/compute than this 8 GB CPU box; a GPU (SemIf's target) makes it comfortable.
272
 
273
- **Takeaway:** JEV-CPU shows the method runs anywhere; **accuracy scales with the model you point it at.**
274
 
275
  ---
276
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # JEV-CPU
2
 
3
  <div align="center">
 
256
 
257
  That is exactly what fixes the slip in the loan demo: `Qwen3-0.6B` flags *high risk* correctly but still leans *approve*; a 2B/4B brain keeps the secondary decision consistent. The trade-off is resources — a 4B model needs `transformers`' native Qwen3.5 support and more RAM/compute than this 8 GB CPU box; a GPU (SemIf's target) makes it comfortable.
258
 
259
+ **Takeaway:** JEV-CPU shows the method runs anywhere; **accuracy scales with the model you point it at.** And a larger open-weight model **served on a GPU** relaxes the CPU latency wall too — lower latency, far larger inputs (toward the model's 40,960-token context), and many decisions per second via batching + shared-state reuse. That's the shape of a **production JEV** — see the report's [Outlook: GPU serving & a production JEV](https://leesk212.github.io/JEV-CPU/#outlook).
260
 
261
  ---
262
 
docs/index.html CHANGED
@@ -99,9 +99,11 @@
99
  <a href="#domains">5. Qualitative results across domains</a>
100
  <a href="#latency">6. Latency and input-length limits</a>
101
  <a href="#scaling">7. Accuracy scaling with model size</a>
 
102
  <a href="#limitations">8. Limitations</a>
103
  <a href="#conclusion">9. Conclusion</a>
104
  <a href="#repro">10. Reproducibility</a>
 
105
  <a href="#refs">References</a>
106
  </nav>
107
 
@@ -239,6 +241,36 @@ accuracy floor, not the ceiling. From SemIf's own evaluation:</p>
239
  compute that exceed this CPU box (a 4B model wants a GPU, SemIf's target). The takeaway: <strong>JEV-CPU shows
240
  the method runs anywhere; accuracy scales with the model you point it at.</strong></p>
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  <h2 id="limitations">8. Limitations</h2>
243
  <ul>
244
  <li><strong>No new method.</strong> The logit-readout mechanism is prior art; this is a port and measurement.</li>
@@ -256,7 +288,9 @@ the method runs anywhere; accuracy scales with the model you point it at.</stron
256
  SemIf's engine runs on a commodity CPU with a 0.6B model, decides across many domains at ~1&nbsp;s each, and
257
  stays memory-stable well past its default token cap. On this hardware the honest limit is latency: inputs above
258
  ~7,700 tokens cross the ~120&nbsp;s mark and are impractical, and small-model accuracy — not context or memory —
259
- is what improves by scaling the model up. JEV-CPU is offered as a reproducible, minimal demonstration of that.</p>
 
 
260
 
261
  <h2 id="repro">10. Reproducibility</h2>
262
  <p>Code, the web UI, the exact scripts behind §5–§6, and all demo GIFs are public:</p>
@@ -270,6 +304,157 @@ pip install transformers accelerate
270
  python semif_cpu.py # CLI: typed option probabilities
271
  python server.py # web UI on http://localhost:8080</code></pre>
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  <h2 id="refs">References</h2>
274
  <ol class="refs">
275
  <li>T. Lee (TheoLeeCJ). <em>SemIf — Semantic ifs from open models.</em> <a href="https://github.com/TheoLeeCJ/SemIf">github.com/TheoLeeCJ/SemIf</a>. Browser demo: <a href="https://openjev.com/">openjev.com</a>.</li>
 
99
  <a href="#domains">5. Qualitative results across domains</a>
100
  <a href="#latency">6. Latency and input-length limits</a>
101
  <a href="#scaling">7. Accuracy scaling with model size</a>
102
+ <a href="#outlook">7.1 Outlook: GPU serving &amp; a production JEV</a>
103
  <a href="#limitations">8. Limitations</a>
104
  <a href="#conclusion">9. Conclusion</a>
105
  <a href="#repro">10. Reproducibility</a>
106
+ <a href="#appendix">Appendix A. Code-level walkthrough</a>
107
  <a href="#refs">References</a>
108
  </nav>
109
 
 
241
  compute that exceed this CPU box (a 4B model wants a GPU, SemIf's target). The takeaway: <strong>JEV-CPU shows
242
  the method runs anywhere; accuracy scales with the model you point it at.</strong></p>
243
 
244
+ <h3 id="outlook">7.1 Outlook — GPU serving and a production JEV</h3>
245
+ <p>The two axes of this report — accuracy (§7) and CPU latency (§6) — are usually assumed to trade off, but a
246
+ GPU relaxes <em>both at once</em>. On CPU the wall is prefill: a single 7,697-token decision took ~117&nbsp;s
247
+ (§6). A GPU changes the regime on three fronts:</p>
248
+ <ul>
249
+ <li><strong>Latency collapses.</strong> Prefill is exactly the workload GPUs are built for. SemIf reports a
250
+ 4B model on a single RTX&nbsp;3090 returning <strong>21 binary decisions in a median 1.02&nbsp;s</strong>,
251
+ versus 5.3&nbsp;s to generate the equivalent JSON array — the same logit-readout path, just not latency-bound.</li>
252
+ <li><strong>Input size grows.</strong> With the latency wall gone, the practical input rises from our
253
+ ~7,700-token / ~120&nbsp;s CPU ceiling toward the model's full <strong>40,960-token</strong> context — long
254
+ documents, transcripts, or code diffs become decidable in one pass.</li>
255
+ <li><strong>Throughput multiplies.</strong> Because a decision is one forward pass reading fixed positions,
256
+ decisions <em>batch</em> trivially, and shared-state prefix reuse amortizes one long state across many
257
+ criteria. SemIf measures <strong>~20 decisions/s</strong> (777 decisions in 38.8&nbsp;s) with parallel
258
+ suffixes on one 3090.</li>
259
+ </ul>
260
+ <p>Put together, a larger open-weight model (4B+) served on a GPU is <strong>simultaneously more accurate,
261
+ accepts far larger inputs, and answers many decisions per second</strong> — all with typed, auditable outputs
262
+ and no generation to parse. That combination is the shape of a <strong>production, potentially commercial,
263
+ JEV</strong>: semantic-if offered as a low-latency, high-throughput hosted service, with per-tenant models and
264
+ batched shared-state decisions. In that framing, <strong>JEV-CPU is the floor</strong> — proof the method is
265
+ portable to any machine — and a <strong>GPU-served larger model is the ceiling</strong> that turns the same
266
+ engine into a product.</p>
267
+ <div class="note">
268
+ <strong>Scope.</strong> We did not run GPU experiments in this report. The latency and throughput figures
269
+ above are SemIf's published single-GPU (RTX 3090, 4B) measurements together with projections from our own
270
+ CPU curve (§6) — offered as an implication, not as measured results of this work. Quantifying a GPU-served
271
+ JEV (tokens/s, decisions/s, cost per million decisions, batching and concurrency) is the natural next study.
272
+ </div>
273
+
274
  <h2 id="limitations">8. Limitations</h2>
275
  <ul>
276
  <li><strong>No new method.</strong> The logit-readout mechanism is prior art; this is a port and measurement.</li>
 
288
  SemIf's engine runs on a commodity CPU with a 0.6B model, decides across many domains at ~1&nbsp;s each, and
289
  stays memory-stable well past its default token cap. On this hardware the honest limit is latency: inputs above
290
  ~7,700 tokens cross the ~120&nbsp;s mark and are impractical, and small-model accuracy — not context or memory —
291
+ is what improves by scaling the model up. JEV-CPU is offered as a reproducible, minimal demonstration of that
292
+ floor; the same engine, given a larger open-weight model on a GPU, points toward the ceiling (§7.1) — an
293
+ accurate, high-throughput, low-latency semantic-if service, i.e. a production JEV.</p>
294
 
295
  <h2 id="repro">10. Reproducibility</h2>
296
  <p>Code, the web UI, the exact scripts behind §5–§6, and all demo GIFs are public:</p>
 
304
  python semif_cpu.py # CLI: typed option probabilities
305
  python server.py # web UI on http://localhost:8080</code></pre>
306
 
307
+ <hr>
308
+
309
+ <h2 id="appendix">Appendix A. Code-level walkthrough: turning an open-weight model into a decision engine</h2>
310
+ <p>A "JEV" / decision engine is <strong>not a fine-tune and not new weights</strong> — it is a way of
311
+ <em>calling</em> an ordinary open-weight causal language model so that a typed decision falls out of a single
312
+ forward pass. This appendix walks the full pipeline function by function, as it runs in JEV-CPU. The scoring
313
+ code is <a href="https://github.com/TheoLeeCJ/SemIf">SemIf</a>'s, reproduced here verbatim for the report;
314
+ JEV-CPU changes only the model <em>loader</em> (<a href="#a7">A.7</a>). All snippets are from
315
+ <code>src/semif_phase1/{core,direct}.py</code>.</p>
316
+
317
+ <h3 id="a1">A.1 — The record and its validation</h3>
318
+ <p>A decision is one plain record: a <code>state</code> (evidence, string / JSON object / array), a
319
+ <code>question</code> (criterion), and 2–16 <code>options</code>, each with a stable <code>id</code> and a
320
+ human <code>description</code>. Validation is strict so nothing is silently coerced:</p>
321
+ <pre><code>def validate_row(row):
322
+ required = {"id", "state", "question", "options"}
323
+ if not required &lt;= row.keys(): raise ValueError(...)
324
+ # state must be a nonempty, finite, JSON-serializable string/object/array
325
+ json.dumps(state, ensure_ascii=False, allow_nan=False)
326
+ # 2..16 options, each {id: str, description: str}, ids unique
327
+ if not isinstance(options, list) or not 2 &lt;= len(options) &lt;= len(LETTERS): raise ValueError(...)</code></pre>
328
+
329
+ <h3 id="a2">A.2 — Building a letter-choice prompt</h3>
330
+ <p>Each option is assigned an uppercase letter and the record is serialized into a two-turn chat. The system
331
+ turn constrains the model to answer with a single letter and nothing else — this is what makes the <em>next</em>
332
+ token the entire decision:</p>
333
+ <pre><code>LETTERS = "ABCDEFGHIJKLMNOP"
334
+ DIRECT_SYSTEM = ("Apply the supplied criterion to the supplied evidence. "
335
+ "Choose exactly one listed option. Respond with only its "
336
+ "uppercase letter, with no explanation or reasoning.")
337
+
338
+ def direct_messages(row):
339
+ payload = {
340
+ "evidence": row["state"],
341
+ "criterion": row["question"],
342
+ "options": [{"letter": LETTERS[i], "description": o["description"]}
343
+ for i, o in enumerate(row["options"])],
344
+ }
345
+ return [{"role": "system", "content": DIRECT_SYSTEM},
346
+ {"role": "user", "content": json.dumps(payload, ensure_ascii=False)}]</code></pre>
347
+
348
+ <h3 id="a3">A.3 — Pinning each option to exactly one token</h3>
349
+ <p>The readout compares the model's probability of each answer letter, so every letter must map to a
350
+ <strong>single, clean vocabulary slot</strong>. Each letter is checked to encode to exactly one token that
351
+ round-trips, with no collisions between options:</p>
352
+ <pre><code>def _slot_ids(tokenizer, count):
353
+ result = []
354
+ for letter in LETTERS[:count]:
355
+ encoded = tokenizer.encode(letter, add_special_tokens=False)
356
+ if len(encoded) != 1 or tokenizer.decode(encoded) != letter:
357
+ raise ValueError(f"Answer slot {letter!r} is not one exact round-trip token")
358
+ result.append(encoded[0])
359
+ if len(result) != len(set(result)):
360
+ raise ValueError("Answer-slot tokens collide")
361
+ return result # e.g. token ids for "A", "B", "C"</code></pre>
362
+
363
+ <h3 id="a4">A.4 — Encoding and verifying the answer boundary</h3>
364
+ <p>The chat template is applied with the generation prompt on and thinking disabled, then two invariants are
365
+ asserted: the prompt fits the token budget (no silent truncation), and appending any answer letter extends the
366
+ tokenization by exactly that one slot token — i.e. the boundary between prompt and answer is stable:</p>
367
+ <pre><code>def encode_prompt(tokenizer, row, max_tokens):
368
+ prompt = tokenizer.apply_chat_template(
369
+ direct_messages(row), tokenize=False,
370
+ add_generation_prompt=True, enable_thinking=False)
371
+ ids = tokenizer.encode(prompt, add_special_tokens=False)
372
+ if not ids or len(ids) &gt; max_tokens:
373
+ raise ValueError("input tokens exceed limit; no truncation allowed")
374
+ slots = _slot_ids(tokenizer, len(row["options"]))
375
+ for letter, token in zip(LETTERS, slots):
376
+ if tokenizer.encode(prompt + letter, add_special_tokens=False) != ids + [token]:
377
+ raise ValueError(f"Answer boundary changes tokenization for slot {letter}")
378
+ return ids, slots, digest(prompt) # digest = sha256 for auditability</code></pre>
379
+
380
+ <h3 id="a5">A.5 — One forward pass, last-position logits</h3>
381
+ <p>The whole model is run <strong>once</strong>. We keep only the final position's logits — the distribution
382
+ over the next token. <code>logits_to_keep=1</code> (when the model supports it) tells the model to compute
383
+ just that row, avoiding a full-sequence logit tensor. There is no sampling and no decode loop:</p>
384
+ <pre><code>def _forward(model, inputs):
385
+ params = inspect.signature(model.forward).parameters
386
+ kwargs = dict(inputs, use_cache=False, return_dict=True)
387
+ if "logits_to_keep" in params:
388
+ kwargs["logits_to_keep"] = 1
389
+ return model(**kwargs).logits[:, -1, :] # shape (batch, vocab)</code></pre>
390
+
391
+ <h3 id="a6">A.6 — Gather the slots, softmax, decide</h3>
392
+ <p>From the full-vocabulary logit vector we index the option-letter tokens and softmax over <em>only</em>
393
+ those, giving a probability per option conditional on the declared set. Note the two device lines: the code
394
+ reads whatever device the model is on and only synchronizes under CUDA — the property that makes it run
395
+ unchanged on CPU:</p>
396
+ <pre><code>def score(model, tokenizer, row, metadata, max_tokens=4096):
397
+ import torch
398
+ ids, slots, prompt_hash = encode_prompt(tokenizer, row, max_tokens)
399
+ device = next(model.parameters()).device # &lt;- follow the model
400
+ inputs = {"input_ids": torch.tensor([ids], dtype=torch.long, device=device),
401
+ "attention_mask": torch.ones((1, len(ids)), dtype=torch.long, device=device)}
402
+ if device.type == "cuda": torch.cuda.synchronize(device) # &lt;- no-op on CPU
403
+ with torch.inference_mode():
404
+ vocabulary = _forward(model, inputs)[0].float() # (vocab,)
405
+ selected = vocabulary[slots].cpu().tolist() # logits at A, B, C, ...
406
+ return {
407
+ "option_ids": [o["id"] for o in row["options"]],
408
+ "option_logits": selected,
409
+ "probabilities": softmax(selected), # winner = argmax
410
+ "input_tokens": len(ids),
411
+ "readout": "native full-vocabulary last-position logits restricted to declared answer slots",
412
+ "probability_status": "conditional option score; uncalibrated as decision confidence",
413
+ ...
414
+ }</code></pre>
415
+ <p>That is the entire "JEV-ification": a stock <code>AutoModelForCausalLM</code> is never asked to generate;
416
+ it is asked once for its next-token logits, and the decision is the arg-max over the option slots.</p>
417
+
418
+ <h3 id="a7">A.7 — The only change to run on CPU</h3>
419
+ <p>SemIf's loader hard-codes a single CUDA device and <code>bfloat16</code>. JEV-CPU replaces just this
420
+ function; every function above is untouched:</p>
421
+ <div class="tablewrap">
422
+ <table>
423
+ <thead><tr><th>SemIf — <code>core.load_causal_model</code> (GPU)</th><th>JEV-CPU — <code>load_causal_model_cpu</code></th></tr></thead>
424
+ <tbody><tr>
425
+ <td><pre style="margin:0"><code>if not torch.cuda.is_available() \
426
+ or torch.cuda.device_count() != 1:
427
+ raise ValueError("Expose exactly "
428
+ "one CUDA GPU ...")
429
+ ...
430
+ model = cls.from_pretrained(
431
+ source, config=config,
432
+ dtype=torch.bfloat16,
433
+ device_map={"": "cuda:0"},
434
+ low_cpu_mem_usage=True, **common)</code></pre></td>
435
+ <td><pre style="margin:0"><code># no CUDA check, no device_map
436
+ model = AutoModelForCausalLM.from_pretrained(
437
+ source, config=config,
438
+ dtype=torch.float32, # CPU-stable
439
+ low_cpu_mem_usage=True, **common)
440
+ model.eval()</code></pre></td>
441
+ </tr></tbody>
442
+ </table>
443
+ </div>
444
+ <p>Because <code>score()</code> and <code>score_shared()</code> derive their device from the model object,
445
+ loading on CPU is sufficient — the same scoring code then runs with <code>torch.cuda.synchronize</code> skipped.
446
+ Swapping the brain model is the one other knob: change <code>MODEL = "Qwen/Qwen3-0.6B"</code> to any causal
447
+ LM whose answer letters are single tokens.</p>
448
+
449
+ <h3 id="a8">A.8 — Reusing one state across many criteria (shared mode)</h3>
450
+ <p>When many criteria judge the same <code>state</code>, <code>shared.py</code> prefills that state once into a
451
+ native key–value cache, replicates the cache across branches with <code>cache.reorder_cache(...)</code>, and
452
+ evaluates every criterion's answer position in one batched forward using a vector
453
+ <code>logits_to_keep</code>. One expensive prefill, many cheap decisions — and, like the direct path, it
454
+ synchronizes only under CUDA, so it too runs unchanged on CPU.</p>
455
+
456
+ <hr>
457
+
458
  <h2 id="refs">References</h2>
459
  <ol class="refs">
460
  <li>T. Lee (TheoLeeCJ). <em>SemIf — Semantic ifs from open models.</em> <a href="https://github.com/TheoLeeCJ/SemIf">github.com/TheoLeeCJ/SemIf</a>. Browser demo: <a href="https://openjev.com/">openjev.com</a>.</li>