Beemer Claude Opus 4.7 commited on
Commit
1628497
·
1 Parent(s): b2950fb

Safe runtime fixes: ONNX session tuning + webapp per-token panel rebuild

Browse files

rerank.make_ort_session (used by both the reranker and, via import, the
embedder): disables ONNX Runtime's spin-waiting between requests -- by
default worker threads busy-spin after an inference for a microsecond
latency win, which burns CPU quota between queries on the Space's shared
2 vCPUs -- and adds an env-configurable intra-op thread cap
(CANLEX_ORT_THREADS; unset keeps ORT's default, so dev behaviour and
numerics are unchanged). 159-Q eval re-run as a gate: identical
(Hit@1 .81 / Hit@3 .94 / Hit@5 .97 / MRR .88).

webapp/app.py: the sources panel was re-rendered from the FULL tool log
on every streamed token -- O(tokens x log-size) string churn plus a
re-sent multi-KB Gradio payload per token late in a session. The
rendered markdown is now cached and refreshed only when a tool call
completes. Display-identical.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Files changed (3) hide show
  1. canlex/embed.py +2 -2
  2. canlex/rerank.py +21 -1
  3. webapp/app.py +13 -7
canlex/embed.py CHANGED
@@ -71,8 +71,8 @@ class Embedder:
71
  if model_path is None:
72
  raise RuntimeError(f"Could not download an ONNX model from {EMB_REPO}.")
73
  tok_path = hf_hub_download(EMB_REPO, "tokenizer.json")
74
- self.session = ort.InferenceSession(model_path,
75
- providers=["CPUExecutionProvider"])
76
  self.input_names = {i.name for i in self.session.get_inputs()}
77
  self.tokenizer = Tokenizer.from_file(tok_path)
78
  self.tokenizer.enable_truncation(max_length=_MAX_TOKENS)
 
71
  if model_path is None:
72
  raise RuntimeError(f"Could not download an ONNX model from {EMB_REPO}.")
73
  tok_path = hf_hub_download(EMB_REPO, "tokenizer.json")
74
+ from .rerank import make_ort_session
75
+ self.session = make_ort_session(model_path)
76
  self.input_names = {i.name for i in self.session.get_inputs()}
77
  self.tokenizer = Tokenizer.from_file(tok_path)
78
  self.tokenizer.enable_truncation(max_length=_MAX_TOKENS)
canlex/rerank.py CHANGED
@@ -20,6 +20,26 @@ _MAX_DOC_CHARS = int(os.environ.get("CANLEX_RERANK_DOC_CHARS", "1000"))
20
  # 3000 -> 1000 (with pool 50 -> 16) held the eval exactly.
21
 
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  class Reranker:
24
  """Cross-encoder that scores (query, section) pairs for relevance.
25
 
@@ -36,7 +56,7 @@ class Reranker:
36
  with open(cfg_path, encoding="utf-8") as fh:
37
  self.pad_id = json.load(fh).get("pad_token_id", 0)
38
 
39
- self.session = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
40
  self.input_names = {i.name for i in self.session.get_inputs()}
41
 
42
  self.tokenizer = Tokenizer.from_file(tok_path)
 
20
  # 3000 -> 1000 (with pool 50 -> 16) held the eval exactly.
21
 
22
 
23
+ def make_ort_session(model_path):
24
+ """A CPU InferenceSession tuned for shared, small-vCPU hosts.
25
+
26
+ Two deviations from onnxruntime defaults: (1) spin-waiting between requests
27
+ is disabled -- by default ORT worker threads busy-spin after an inference
28
+ for a microsecond-scale latency win, which on the Space's shared 2 vCPUs
29
+ burns quota between queries; (2) the intra-op thread count is
30
+ env-configurable (CANLEX_ORT_THREADS) so concurrent sessions can be capped
31
+ below core count if contention is ever observed. Unset, ORT's default
32
+ (all cores) is kept, so single-query numerics and speed are unchanged."""
33
+ opts = ort.SessionOptions()
34
+ threads = int(os.environ.get("CANLEX_ORT_THREADS", "0"))
35
+ if threads:
36
+ opts.intra_op_num_threads = threads
37
+ opts.add_session_config_entry("session.intra_op.allow_spinning", "0")
38
+ opts.add_session_config_entry("session.inter_op.allow_spinning", "0")
39
+ return ort.InferenceSession(model_path, sess_options=opts,
40
+ providers=["CPUExecutionProvider"])
41
+
42
+
43
  class Reranker:
44
  """Cross-encoder that scores (query, section) pairs for relevance.
45
 
 
56
  with open(cfg_path, encoding="utf-8") as fh:
57
  self.pad_id = json.load(fh).get("pad_token_id", 0)
58
 
59
+ self.session = make_ort_session(model_path)
60
  self.input_names = {i.name for i in self.session.get_inputs()}
61
 
62
  self.tokenizer = Tokenizer.from_file(tok_path)
webapp/app.py CHANGED
@@ -427,6 +427,11 @@ async def _agentic_answer(question: str):
427
  trace: list[str] = []
428
 
429
  answer_buf = ""
 
 
 
 
 
430
 
431
  def status_md(thinking: bool = True) -> str:
432
  lines = [f"- {line}" for line in trace]
@@ -435,7 +440,7 @@ async def _agentic_answer(question: str):
435
  return "\n".join(lines) if lines else ""
436
 
437
  for step in range(MAX_TOOL_ITERATIONS):
438
- yield status_md(), answer_buf, _format_sources(tool_log)
439
 
440
  # Stream Gemini's next turn. Stream text deltas to the answer
441
  # panel optimistically; revert to the pre-turn answer if it
@@ -451,7 +456,7 @@ async def _agentic_answer(question: str):
451
  turn_text += chunk["text"]
452
  yield (status_md(),
453
  answer_buf + turn_text,
454
- _format_sources(tool_log))
455
  elif chunk["type"] == "function_call":
456
  turn_calls.append(chunk["call"])
457
  if optimistic and turn_text:
@@ -460,7 +465,7 @@ async def _agentic_answer(question: str):
460
  optimistic = False
461
  yield (status_md(),
462
  answer_buf,
463
- _format_sources(tool_log))
464
  elif chunk["type"] == "finish":
465
  turn_parts = chunk["parts"] or []
466
  # Capture any text-only finish reason so the caller can
@@ -479,7 +484,7 @@ async def _agentic_answer(question: str):
479
  answer_buf += turn_text
480
  yield (status_md(thinking=False),
481
  _match_badge(tool_log) + answer_buf,
482
- _format_sources(tool_log))
483
  return
484
 
485
  # Tool turn. If the model emitted a commentary fragment before
@@ -499,10 +504,11 @@ async def _agentic_answer(question: str):
499
  args = call.get("args") or {}
500
  label = _summarize_call(name, args)
501
  trace.append(label)
502
- yield status_md(), answer_buf, _format_sources(tool_log)
503
 
504
  output = await _run_tool(session, name, args)
505
  tool_log.append((name, args, output))
 
506
  function_responses.append({
507
  "functionResponse": {
508
  "name": name,
@@ -525,12 +531,12 @@ async def _agentic_answer(question: str):
525
  turn_text += chunk["text"]
526
  yield (status_md(thinking=False),
527
  answer_buf + turn_text,
528
- _format_sources(tool_log))
529
  answer_buf += turn_text or \
530
  "_(no answer produced after the tool-call budget was exhausted)_"
531
  yield (status_md(thinking=False),
532
  _match_badge(tool_log) + answer_buf,
533
- _format_sources(tool_log))
534
 
535
 
536
  # --- Gradio handler -----------------------------------------------------------
 
427
  trace: list[str] = []
428
 
429
  answer_buf = ""
430
+ # The rendered sources panel is cached and refreshed only when a
431
+ # tool call completes: rebuilding it from the full tool_log on
432
+ # every streamed token was O(tokens x log-size) string churn (tens
433
+ # of KB re-rendered and re-sent per token late in a session).
434
+ sources_md = ""
435
 
436
  def status_md(thinking: bool = True) -> str:
437
  lines = [f"- {line}" for line in trace]
 
440
  return "\n".join(lines) if lines else ""
441
 
442
  for step in range(MAX_TOOL_ITERATIONS):
443
+ yield status_md(), answer_buf, sources_md
444
 
445
  # Stream Gemini's next turn. Stream text deltas to the answer
446
  # panel optimistically; revert to the pre-turn answer if it
 
456
  turn_text += chunk["text"]
457
  yield (status_md(),
458
  answer_buf + turn_text,
459
+ sources_md)
460
  elif chunk["type"] == "function_call":
461
  turn_calls.append(chunk["call"])
462
  if optimistic and turn_text:
 
465
  optimistic = False
466
  yield (status_md(),
467
  answer_buf,
468
+ sources_md)
469
  elif chunk["type"] == "finish":
470
  turn_parts = chunk["parts"] or []
471
  # Capture any text-only finish reason so the caller can
 
484
  answer_buf += turn_text
485
  yield (status_md(thinking=False),
486
  _match_badge(tool_log) + answer_buf,
487
+ sources_md)
488
  return
489
 
490
  # Tool turn. If the model emitted a commentary fragment before
 
504
  args = call.get("args") or {}
505
  label = _summarize_call(name, args)
506
  trace.append(label)
507
+ yield status_md(), answer_buf, sources_md
508
 
509
  output = await _run_tool(session, name, args)
510
  tool_log.append((name, args, output))
511
+ sources_md = _format_sources(tool_log)
512
  function_responses.append({
513
  "functionResponse": {
514
  "name": name,
 
531
  turn_text += chunk["text"]
532
  yield (status_md(thinking=False),
533
  answer_buf + turn_text,
534
+ sources_md)
535
  answer_buf += turn_text or \
536
  "_(no answer produced after the tool-call budget was exhausted)_"
537
  yield (status_md(thinking=False),
538
  _match_badge(tool_log) + answer_buf,
539
+ sources_md)
540
 
541
 
542
  # --- Gradio handler -----------------------------------------------------------