Siddh12334 commited on
Commit
46c2f98
·
1 Parent(s): 842caac

feat: add optional trained model inference to env space

Browse files

Add lazy LoRA adapter inference endpoints and GPU model dependencies while preserving the OpenEnv reset/step server routes.

Made-with: Cursor

Dockerfile CHANGED
@@ -4,7 +4,9 @@ WORKDIR /app
4
 
5
  # Install deps first (cached layer — only invalidated when requirements change)
6
  COPY requirements-server.txt .
7
- RUN pip install --no-cache-dir -r requirements-server.txt
 
 
8
 
9
  # Copy source (excludes venv/, training/, assets/, .git/ via .dockerignore)
10
  COPY . .
@@ -14,5 +16,5 @@ RUN python -m data.loader || echo "facts.json generation skipped — will use fa
14
 
15
  EXPOSE 7860
16
 
17
- # 2 workers: handles concurrent sessions without threading issues
18
- CMD ["uvicorn", "environment.server:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "2"]
 
4
 
5
  # Install deps first (cached layer — only invalidated when requirements change)
6
  COPY requirements-server.txt .
7
+ RUN pip install --no-cache-dir \
8
+ torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 && \
9
+ pip install --no-cache-dir -r requirements-server.txt
10
 
11
  # Copy source (excludes venv/, training/, assets/, .git/ via .dockerignore)
12
  COPY . .
 
16
 
17
  EXPOSE 7860
18
 
19
+ # One worker so the optional trained-model adapter is loaded at most once.
20
+ CMD ["uvicorn", "environment.server:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
environment/model_inference.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import threading
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import torch
7
+ from pydantic import BaseModel
8
+
9
+ from environment.actions import EpisodeObservation
10
+
11
+
12
+ MODEL_ID = os.getenv("MODEL_ID", "Siddh12334/qwen-1.5b-context-corruption")
13
+ MAX_NEW_TOKENS = int(os.getenv("MODEL_MAX_NEW_TOKENS", "128"))
14
+ _LOCK = threading.Lock()
15
+ _MODEL = None
16
+ _TOKENIZER = None
17
+
18
+
19
+ class InferenceRequest(BaseModel):
20
+ observation: EpisodeObservation
21
+
22
+
23
+ class InferenceResponse(BaseModel):
24
+ text: str
25
+ loaded_model: str
26
+
27
+
28
+ def configure_runtime_dirs():
29
+ root = Path(os.getenv("MODEL_RUNTIME_DIR", "/tmp/context-corruption-model"))
30
+ cache = root / "cache"
31
+ env_dirs = {
32
+ "HOME": root,
33
+ "XDG_CACHE_HOME": cache,
34
+ "HF_HOME": cache / "huggingface",
35
+ "HF_HUB_CACHE": cache / "huggingface" / "hub",
36
+ "TRANSFORMERS_CACHE": cache / "huggingface" / "transformers",
37
+ }
38
+ for path in env_dirs.values():
39
+ path.mkdir(parents=True, exist_ok=True)
40
+ for key, path in env_dirs.items():
41
+ os.environ.setdefault(key, str(path))
42
+
43
+
44
+ def model_status() -> dict[str, Any]:
45
+ return {
46
+ "model_id": MODEL_ID,
47
+ "loaded": _MODEL is not None,
48
+ "cuda_available": torch.cuda.is_available(),
49
+ "cuda_device": torch.cuda.get_device_name(0) if torch.cuda.is_available() else None,
50
+ }
51
+
52
+
53
+ def _load_model():
54
+ global _MODEL, _TOKENIZER
55
+ if _MODEL is not None and _TOKENIZER is not None:
56
+ return _MODEL, _TOKENIZER
57
+
58
+ with _LOCK:
59
+ if _MODEL is not None and _TOKENIZER is not None:
60
+ return _MODEL, _TOKENIZER
61
+
62
+ configure_runtime_dirs()
63
+ from peft import AutoPeftModelForCausalLM
64
+ from transformers import AutoTokenizer, BitsAndBytesConfig
65
+
66
+ quantization_config = None
67
+ if torch.cuda.is_available():
68
+ quantization_config = BitsAndBytesConfig(
69
+ load_in_4bit=True,
70
+ bnb_4bit_compute_dtype=torch.bfloat16,
71
+ bnb_4bit_quant_type="nf4",
72
+ bnb_4bit_use_double_quant=True,
73
+ )
74
+
75
+ _TOKENIZER = AutoTokenizer.from_pretrained(MODEL_ID)
76
+ _MODEL = AutoPeftModelForCausalLM.from_pretrained(
77
+ MODEL_ID,
78
+ device_map="auto" if torch.cuda.is_available() else "cpu",
79
+ torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
80
+ quantization_config=quantization_config,
81
+ low_cpu_mem_usage=True,
82
+ )
83
+ _MODEL.eval()
84
+ return _MODEL, _TOKENIZER
85
+
86
+
87
+ def _format_prompt(observation: EpisodeObservation) -> list[dict[str, str]]:
88
+ docs_text = "\n\n".join(
89
+ f"[Doc {doc.id}] {doc.title}\n{doc.content}" for doc in observation.documents
90
+ )
91
+ system = (
92
+ "You are an epistemic agent. Answer the question and identify corrupted documents. "
93
+ 'Respond ONLY as JSON: {"answer": "...", "suspicious_docs": [0], "confidence": 0.8}'
94
+ )
95
+ user = f"Question: {observation.question}\n\nDocuments:\n{docs_text}"
96
+ return [
97
+ {"role": "system", "content": system},
98
+ {"role": "user", "content": user},
99
+ ]
100
+
101
+
102
+ def run_inference(observation: EpisodeObservation) -> InferenceResponse:
103
+ model, tokenizer = _load_model()
104
+ messages = _format_prompt(observation)
105
+ prompt = tokenizer.apply_chat_template(
106
+ messages,
107
+ tokenize=False,
108
+ add_generation_prompt=True,
109
+ )
110
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
111
+ with torch.inference_mode():
112
+ output_ids = model.generate(
113
+ **inputs,
114
+ max_new_tokens=MAX_NEW_TOKENS,
115
+ do_sample=False,
116
+ pad_token_id=tokenizer.eos_token_id,
117
+ )
118
+ generated_ids = output_ids[0][inputs["input_ids"].shape[-1]:]
119
+ text = tokenizer.decode(generated_ids, skip_special_tokens=True).strip()
120
+ return InferenceResponse(text=text, loaded_model=MODEL_ID)
environment/server.py CHANGED
@@ -7,6 +7,7 @@ load_dotenv()
7
 
8
  from environment.actions import ContextCorruptionAction, EpisodeObservation
9
  from environment.env import ContextCorruptionEnv
 
10
 
11
  _difficulty_env = os.getenv("DIFFICULTY")
12
  _difficulty = int(_difficulty_env) if _difficulty_env else None
@@ -20,5 +21,15 @@ app = create_app(
20
  max_concurrent_envs=_max_sessions,
21
  )
22
 
 
 
 
 
 
 
 
 
 
 
23
  if __name__ == "__main__":
24
  uvicorn.run("environment.server:app", host="0.0.0.0", port=7860, reload=False)
 
7
 
8
  from environment.actions import ContextCorruptionAction, EpisodeObservation
9
  from environment.env import ContextCorruptionEnv
10
+ from environment.model_inference import InferenceRequest, model_status, run_inference
11
 
12
  _difficulty_env = os.getenv("DIFFICULTY")
13
  _difficulty = int(_difficulty_env) if _difficulty_env else None
 
21
  max_concurrent_envs=_max_sessions,
22
  )
23
 
24
+
25
+ @app.get("/model/status")
26
+ def get_model_status():
27
+ return model_status()
28
+
29
+
30
+ @app.post("/model/infer")
31
+ def infer_with_trained_model(request: InferenceRequest):
32
+ return run_inference(request.observation)
33
+
34
  if __name__ == "__main__":
35
  uvicorn.run("environment.server:app", host="0.0.0.0", port=7860, reload=False)
requirements-server.txt CHANGED
@@ -6,3 +6,8 @@ datasets>=2.0.0
6
  faker>=18.0.0
7
  python-dotenv>=1.0.0
8
  websockets>=15.0.0
 
 
 
 
 
 
6
  faker>=18.0.0
7
  python-dotenv>=1.0.0
8
  websockets>=15.0.0
9
+ transformers>=5.5.0
10
+ peft>=0.19.0
11
+ accelerate>=1.0.0
12
+ bitsandbytes>=0.45.0
13
+ safetensors>=0.4.0