Praneshrajan15's picture
Deploy DataForge playground API
13fe504 verified
Raw
History Blame Contribute Delete
4.51 kB
"""Local model backend for the DataForge verified agent (offline by default).
Loads a fine-tuned causal LM (the DataForge GRPO/SFT Qwen checkpoint by
default) with transformers and exposes the same synchronous completion
signature the hosted provider client uses, so a single agent loop drives both
local and hosted policies.
This module is intentionally import-light: heavy dependencies (``torch``,
``transformers``) are imported lazily inside :func:`build_local_completion`,
and the model is loaded once and reused. If the dependencies or the model are
unavailable the loader raises, and the policy factory degrades to the
deterministic policy rather than failing the run.
Environment variables:
DATAFORGE_AGENT_MODEL Model id or local path (default: the DataForge
0.5B GRPO checkpoint).
DATAFORGE_AGENT_DEVICE ``cpu`` / ``cuda`` / ``auto`` (default: ``auto``).
DATAFORGE_AGENT_MAX_NEW_TOKENS Generation cap (default: 256).
HF_HUB_OFFLINE Honoured by transformers for fully offline use.
"""
from __future__ import annotations
import os
from collections.abc import Callable, Sequence
from typing import Any
from dataforge.agent.providers import Message
__all__ = ["DEFAULT_LOCAL_MODEL", "build_local_completion"]
DEFAULT_LOCAL_MODEL = "praneshrajan15/DataForge-0.5B-GRPO"
_MODEL_CACHE: dict[str, tuple[Any, Any, str]] = {}
def _resolve_model_id(model: str | None) -> str:
"""Resolve the model id from the argument, env var, or default."""
if model:
return model
return (
os.environ.get("DATAFORGE_AGENT_MODEL", DEFAULT_LOCAL_MODEL).strip() or DEFAULT_LOCAL_MODEL
)
def _resolve_device(requested: str | None) -> str:
"""Pick a torch device, honouring DATAFORGE_AGENT_DEVICE then availability."""
import torch
choice = (requested or os.environ.get("DATAFORGE_AGENT_DEVICE", "auto")).strip().lower()
if choice in {"cpu", "cuda"}:
return choice
return "cuda" if torch.cuda.is_available() else "cpu"
def _load_model(model_id: str) -> tuple[Any, Any, str]:
"""Load and cache the tokenizer/model, returning (tokenizer, model, device)."""
cached = _MODEL_CACHE.get(model_id)
if cached is not None:
return cached
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
device = _resolve_device(None)
tokenizer: Any = AutoTokenizer.from_pretrained(model_id)
dtype = torch.float16 if device == "cuda" else torch.float32
model: Any = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=dtype)
model.to(device)
model.eval()
_MODEL_CACHE[model_id] = (tokenizer, model, device)
return tokenizer, model, device
def build_local_completion(
model: str | None = None,
) -> Callable[[Sequence[Message], str | None, float], str]:
"""Build a synchronous completion callable backed by a local model.
Args:
model: Optional model id/path override.
Returns:
A callable ``(messages, model_name, temperature) -> str`` compatible
with :data:`dataforge.agent.policy.CompletionFn`.
Raises:
ImportError: If transformers/torch are not installed.
Exception: If the model cannot be loaded (missing weights, offline).
"""
model_id = _resolve_model_id(model)
tokenizer, loaded_model, device = _load_model(model_id)
import torch
max_new_tokens = int(os.environ.get("DATAFORGE_AGENT_MAX_NEW_TOKENS", "256"))
def _complete(messages: Sequence[Message], _model_name: str | None, temperature: float) -> str:
chat = [{"role": m["role"], "content": m["content"]} for m in messages]
prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(prompt, return_tensors="pt").to(device)
do_sample = temperature > 0.0
with torch.no_grad():
output_ids = loaded_model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=do_sample,
temperature=temperature if do_sample else None,
pad_token_id=tokenizer.pad_token_id or tokenizer.eos_token_id,
)
generated = output_ids[0][inputs["input_ids"].shape[1] :]
decoded: str = tokenizer.decode(generated, skip_special_tokens=True)
return decoded
return _complete