Instructions to use SurdAI/SPX-CD-Pro with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use SurdAI/SPX-CD-Pro with PEFT:
from peft import PeftModel from transformers import AutoModelForCausalLM base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen3.8-27B") model = PeftModel.from_pretrained(base_model, "SurdAI/SPX-CD-Pro") - Notebooks
- Google Colab
- Kaggle
File size: 7,624 Bytes
04f53c5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | """Inference CLI. Reference answers are never passed to the model."""
import argparse
import json
import math
import os
from pathlib import Path
from decision import decide, validate_labels
RELEASE = json.loads(Path(__file__).with_name("release.json").read_text())
MAX_PIXELS = 512 * 32 * 32
class TransformersBackend:
def __init__(self, base, adapter, max_context):
import torch
from transformers import AutoProcessor, AutoModelForMultimodalLM
from peft import PeftModel
self.torch = torch
self.processor = AutoProcessor.from_pretrained(base, max_pixels=MAX_PIXELS)
self.model = AutoModelForMultimodalLM.from_pretrained(
base, dtype=torch.bfloat16, device_map="auto")
self.model = PeftModel.from_pretrained(self.model, adapter).eval()
self.max_context = max_context
def logits(self, prompt, images, allowed):
if images:
inputs = self.processor(text=[prompt], images=list(images), return_tensors="pt")
else:
inputs = self.processor.tokenizer(prompt, add_special_tokens=False, return_tensors="pt")
if inputs["input_ids"].shape[1] + 1 > self.max_context:
raise ValueError("Prompt exceeds context limit; no automatic truncation")
inputs = inputs.to(self.model.get_input_embeddings().weight.device)
with self.torch.inference_mode():
outputs = self.model(**inputs, logits_to_keep=1, use_cache=False)
# Preserve the model's native final logits.
return outputs.logits[0, -1, allowed].float().cpu().tolist()
class VLLMBackend:
def __init__(self, base, adapter, max_context, gpu_memory):
from transformers import AutoProcessor
from vllm import LLM, SamplingParams
import vllm.sampling_params as sampling_module
from vllm.lora.request import LoRARequest
# This runner requests only the candidate vocabulary, up to 1,024 IDs.
sampling_module.MAX_LOGPROB_TOKEN_IDS = 1025
cfg = json.loads(Path(adapter, "adapter_config.json").read_text())
self.processor = AutoProcessor.from_pretrained(base, max_pixels=MAX_PIXELS)
self.params = SamplingParams
self.max_context = max_context
self.model = LLM(
model=base, dtype="bfloat16", enable_lora=True, max_lora_rank=int(cfg["r"]),
max_model_len=max_context, max_num_seqs=2,
max_num_batched_tokens=2048, gpu_memory_utilization=gpu_memory,
enforce_eager=True, logprobs_mode="raw_logits", max_logprobs=1025,
attention_config={"backend": "TRITON_ATTN"},
limit_mm_per_prompt={"image": 5, "video": 0, "audio": 0},
mm_processor_kwargs={"max_pixels": MAX_PIXELS}, seed=20261004)
self.request = LoRARequest(RELEASE["model_id"], 1, str(Path(adapter).resolve()))
def logits(self, prompt, images, allowed):
length = (self.processor(text=[prompt], images=list(images), return_tensors="pt")
["input_ids"].shape[1] if images else
len(self.processor.tokenizer.encode(prompt, add_special_tokens=False)))
if length + 1 > self.max_context:
raise ValueError("Prompt exceeds context limit; no automatic truncation")
inp = {"prompt": prompt}
if images:
inp["multi_modal_data"] = {"image": list(images)}
out = self.model.generate(
[inp], self.params(temperature=0, max_tokens=1,
allowed_token_ids=allowed, logprob_token_ids=allowed),
lora_request=self.request, use_tqdm=False)[0]
scores = out.outputs[0].logprobs[0]
return [scores[token_id].logprob for token_id in allowed]
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--backend", choices=["transformers", "vllm"], default="transformers")
p.add_argument("--base", default=RELEASE["base_model"])
p.add_argument("--adapter", required=True)
p.add_argument("--input", required=True, help="One JSON object or a JSONL file")
p.add_argument("--output", required=True, help="New JSONL file; existing files are not overwritten")
p.add_argument("--image-root", default=None)
p.add_argument("--labels", default=str(Path(__file__).with_name("label_tokens.json")))
p.add_argument("--effort", type=int, choices=range(1, 6), default=1,
help="Budget of distinct candidate orderings (1–5)")
p.add_argument("--prompt-format", choices=["cmdb", "open-format"], default="cmdb")
p.add_argument("--temperature", type=float, default=1.0)
p.add_argument("--max-context", type=int, default=65536, help="Total context token budget (default: 65536)")
p.add_argument("--gpu-memory", type=float, default=0.8)
p.add_argument("--cache-env", help="Optional JSON environment map, applied before ML imports")
p.add_argument("--dry-run", action="store_true", help="Validate inputs without loading a model")
a = p.parse_args()
if not math.isfinite(a.temperature) or a.temperature <= 0:
p.error("--temperature must be positive and finite")
if a.max_context < 2 or not 0 < a.gpu_memory < 1:
p.error("Invalid context limit or GPU memory fraction")
if a.cache_env:
os.environ.update({k: str(v) for k, v in json.loads(Path(a.cache_env).read_text()).items()})
inp = Path(a.input)
if inp.suffix == ".json":
cases = [json.loads(inp.read_text())]
else:
cases = [json.loads(line) for line in inp.open() if line.strip()]
from decision import normalize_case
labels = json.loads(Path(a.labels).read_text())
image_root = Path(a.image_root) if a.image_root else inp.parent
ids = set()
for case in cases:
case["_prompt_format"] = a.prompt_format
normalized = normalize_case(case)
if a.prompt_format == "open-format" and (case.get("image_refs") or normalized["kind"] == "multi_choice"):
raise ValueError("Use --prompt-format cmdb for images or multi-select")
if normalized["id"] in ids:
raise ValueError("Input IDs must be unique")
ids.add(normalized["id"])
if len(normalized["options"]) > len(labels["ids"]):
raise ValueError("Too many candidates")
for ref in case.get("image_refs", []):
path = (image_root / ref).resolve()
if not path.is_relative_to(image_root.resolve()) or not path.is_file():
raise ValueError(f"Image must exist within image-root: {ref}")
if a.dry_run:
print(json.dumps({"validated_inputs": len(cases), "label_count": len(labels["ids"])}))
return
if Path(a.output).exists():
raise FileExistsError("Choose a new output file")
backend = (TransformersBackend(a.base, a.adapter, a.max_context) if a.backend == "transformers"
else VLLMBackend(a.base, a.adapter, a.max_context, a.gpu_memory))
validate_labels(backend.processor, labels)
from PIL import Image
with Path(a.output).open("x") as out:
for case in cases:
images = []
for ref in case.get("image_refs", []):
with Image.open(image_root / ref) as image:
images.append(image.convert("RGB"))
if len(images) > 5:
raise ValueError("At most five images are supported by this runner")
result = decide(case, backend, labels, images, a.effort, a.temperature)
out.write(json.dumps(result, ensure_ascii=False) + "\n")
out.flush()
if __name__ == "__main__":
main()
|