Upload infer_constrained.py with huggingface_hub
Browse files- 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])
|