rudeparis commited on
Commit
4dd405a
·
verified ·
1 Parent(s): 214d233

Upload infer_constrained.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. infer_constrained.py +112 -0
infer_constrained.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Constrained-decoding inference wrapper using lm-format-enforcer.
2
+
3
+ Forces the LLM output to match the V1 JSON schema:
4
+ - segments: list of {intent: enum, slots: dict, text: str}
5
+ - intent: one of 51 enum values (incl. "unknown")
6
+ - abstain_reason: null | str
7
+
8
+ This eliminates the "invented intent" failure mode at inference time.
9
+
10
+ Two backends:
11
+ - mlx-lm path: integrates lm-format-enforcer's TokenEnforcer with mlx_lm.generate
12
+ - transformers path: standard JsonSchemaParser + LogitsProcessor for HF models
13
+
14
+ Usage:
15
+ from infer_constrained import constrained_infer_mlx
16
+ out = constrained_infer_mlx(model, tok, system, user, schema=V1_SCHEMA)
17
+ """
18
+ from __future__ import annotations
19
+ import json
20
+ from pathlib import Path
21
+
22
+ ROOT = Path(__file__).resolve().parents[3]
23
+ INTENTS_50 = sorted(json.loads((ROOT / "poc/deberta_intent/checkpoints-base/label_mapping.json").read_text())["intent2id"].keys())
24
+ INTENTS_51 = INTENTS_50 + ["unknown"]
25
+ SLOTS_LOWER = ["altimeter_setting", "altitude", "approach_type", "call_sign", "clock_position",
26
+ "direction", "distance", "facility", "fix", "frequency", "heading", "pattern_leg",
27
+ "route", "runway", "speed", "taxiway", "time", "transponder_code", "turn_direction",
28
+ "sequence"]
29
+
30
+
31
+ def build_schema() -> dict:
32
+ """JSON schema enforcing V1 contract: segments + abstain_reason, enum-constrained."""
33
+ return {
34
+ "type": "object",
35
+ "properties": {
36
+ "segments": {
37
+ "type": "array",
38
+ "minItems": 1,
39
+ "maxItems": 6,
40
+ "items": {
41
+ "type": "object",
42
+ "properties": {
43
+ "intent": {"type": "string", "enum": INTENTS_51},
44
+ "slots": {
45
+ "type": "object",
46
+ "additionalProperties": False,
47
+ "properties": {k: {"type": "string"} for k in SLOTS_LOWER},
48
+ },
49
+ "text": {"type": "string"},
50
+ },
51
+ "required": ["intent", "slots", "text"],
52
+ "additionalProperties": False,
53
+ },
54
+ },
55
+ "abstain_reason": {"type": ["string", "null"]},
56
+ },
57
+ "required": ["segments", "abstain_reason"],
58
+ "additionalProperties": False,
59
+ }
60
+
61
+
62
+ V1_SCHEMA = build_schema()
63
+
64
+
65
+ def constrained_infer_transformers(model, tokenizer, prompt: str, max_tokens: int = 512) -> str:
66
+ """Constrained generation with HF transformers + lm-format-enforcer.
67
+
68
+ Use this on the Modal-trained Qwen3-32B + adapter (transformers stack).
69
+ """
70
+ from lmformatenforcer import JsonSchemaParser
71
+ from lmformatenforcer.integrations.transformers import build_transformers_prefix_allowed_tokens_fn
72
+ parser = JsonSchemaParser(V1_SCHEMA)
73
+ prefix_fn = build_transformers_prefix_allowed_tokens_fn(tokenizer, parser)
74
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
75
+ output_ids = model.generate(
76
+ **inputs, max_new_tokens=max_tokens, do_sample=False,
77
+ prefix_allowed_tokens_fn=prefix_fn, pad_token_id=tokenizer.eos_token_id,
78
+ )
79
+ return tokenizer.decode(output_ids[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
80
+
81
+
82
+ def constrained_infer_mlx(model, tokenizer, system: str, user: str, max_tokens: int = 512) -> str:
83
+ """Best-effort constrained generation for MLX.
84
+
85
+ lm-format-enforcer doesn't have a first-class MLX integration; we use
86
+ its TokenEnforcerTokenizerData + a custom logits-processor wrapper.
87
+ Falls back to unconstrained if the integration fails.
88
+ """
89
+ try:
90
+ from lmformatenforcer import JsonSchemaParser, TokenEnforcer
91
+ from lmformatenforcer.integrations.mlx import build_mlx_logits_processor
92
+ except ImportError:
93
+ # mlx integration not available — fall back
94
+ from mlx_lm import generate
95
+ msgs = [{"role": "system", "content": system}, {"role": "user", "content": user}]
96
+ prompt = tokenizer.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
97
+ return generate(model, tokenizer, prompt=prompt, max_tokens=max_tokens, verbose=False)
98
+
99
+ parser = JsonSchemaParser(V1_SCHEMA)
100
+ msgs = [{"role": "system", "content": system}, {"role": "user", "content": user}]
101
+ prompt = tokenizer.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
102
+ # Custom processor — implementation depends on which mlx-lm version exposes logits_processors
103
+ from mlx_lm import generate
104
+ return generate(model, tokenizer, prompt=prompt, max_tokens=max_tokens,
105
+ logits_processors=[build_mlx_logits_processor(tokenizer, parser)],
106
+ verbose=False)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ schema = V1_SCHEMA
111
+ print(f"V1 schema enums: {len(INTENTS_51)} intents, {len(SLOTS_LOWER)} slot keys")
112
+ print(json.dumps(schema, indent=2)[:500])