Skip to content

Commit 97bccb6

Browse files
committed
Merge branch 'lab/mt-bench' into 'export/v1-2-0'
add mt-bench and throughput evaluator See merge request onecomp/onecomp-lab!80
2 parents 1fc5536 + 595cc71 commit 97bccb6

61 files changed

Lines changed: 4510 additions & 83 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ tests/onecomp/quantizer/jointq/data/model_layers_0_self_attn_k_proj.pth
2929
.uv-sync-vllm.lock
3030
.uv-sync-vllm.done
3131
.venv-vllm/
32+
# MT-Bench eval data (download via onecomp/eval/scripts/download_mt_bench_data*.sh)
33+
onecomp/eval/data/
34+
onecomp/eval/data_backup/
3235

3336
# ── dashboard/ (HPC LLM quantization dashboard) ──────────────
3437
# Re-include the app/models package shadowed by the global `models` rule above

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
## [v1.2.0] 2026-06-04 (WIP)
55

6+
### Evaluation:
7+
- Added `onecomp.eval` and the `onecomp-eval` CLI: one vLLM server, subprocess evaluators, aggregated `summary.json` / `summary.csv`
8+
- Added `mt_bench` (Japanese MT-Bench) and opt-in `throughput` (TTFT / decode tok/s) evaluators
9+
610
## New Feature : Dashboard
711

812
- Added `dashboard/`, a browser-based web app for OneCompression on **SLURM-managed HPC GPU nodes without Docker**: pick a Hugging Face model and quantization settings in the UI, run jobs on the GPU, deploy the quantized checkpoint, and validate inference via chat

dashboard/backend/app/api/jobs.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
from fastapi import APIRouter, Depends, HTTPException
2-
from sqlalchemy.orm import Session
3-
41
from app.constants import ChatTaskStatus, InferenceStatus, JobStatus
52
from app.core.config import settings
63
from app.core.database import get_db
@@ -20,6 +17,8 @@
2017
from app.services.inference import chat_vllm, stop_inference
2118
from app.worker.celery_app import celery_app
2219
from app.worker.tasks import chat_with_model, deploy_model, run_quantization
20+
from fastapi import APIRouter, Depends, HTTPException
21+
from sqlalchemy.orm import Session
2322

2423
router = APIRouter(prefix="/jobs", tags=["jobs"])
2524

@@ -200,9 +199,7 @@ def get_chat_result(task_id: str) -> ChatTaskResult:
200199

201200

202201
@router.get("")
203-
def list_jobs(
204-
limit: int = 20, offset: int = 0, db: Session = Depends(get_db)
205-
) -> JobListResponse:
202+
def list_jobs(limit: int = 20, offset: int = 0, db: Session = Depends(get_db)) -> JobListResponse:
206203
total = db.query(Job).count()
207204
jobs = db.query(Job).order_by(Job.created_at.desc()).offset(offset).limit(limit).all()
208205
return JobListResponse(

dashboard/backend/app/core/database.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1+
from app.core.config import settings
12
from sqlalchemy import create_engine
23
from sqlalchemy.orm import DeclarativeBase, sessionmaker
34

4-
from app.core.config import settings
5-
65
_connect_args = {"check_same_thread": False} if settings.database_url.startswith("sqlite") else {}
76
engine = create_engine(settings.database_url, connect_args=_connect_args)
87
SessionLocal = sessionmaker(bind=engine)

dashboard/backend/app/main.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
from contextlib import asynccontextmanager
22

3+
from app.api.jobs import router as jobs_router
4+
from app.core.database import Base, engine
35
from fastapi import FastAPI
46
from fastapi.middleware.cors import CORSMiddleware
57
from sqlalchemy import inspect, text
68

7-
from app.api.jobs import router as jobs_router
8-
from app.core.database import Base, engine
9-
109

1110
def _sync_schema():
1211
"""Create missing tables and add missing columns to existing tables."""
@@ -24,7 +23,9 @@ def _sync_schema():
2423
col_type = col.type.compile(dialect=engine.dialect)
2524
nullable = "NULL" if col.nullable else "NOT NULL"
2625
conn.execute(
27-
text(f'ALTER TABLE "{table_name}" ADD COLUMN "{col.name}" {col_type} {nullable}')
26+
text(
27+
f'ALTER TABLE "{table_name}" ADD COLUMN "{col.name}" {col_type} {nullable}'
28+
)
2829
)
2930

3031

dashboard/backend/app/models/job.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
import uuid
22
from datetime import datetime, timezone
33

4+
from app.constants import InferenceStatus, JobStatus
5+
from app.core.database import Base
46
from sqlalchemy import JSON, DateTime, Integer, String, Text
57
from sqlalchemy.orm import Mapped, mapped_column
68

7-
from app.core.database import Base
8-
from app.constants import InferenceStatus, JobStatus
9-
109

1110
class Job(Base):
1211
__tablename__ = "jobs"
1312

1413
id: Mapped[str] = mapped_column(
1514
String(36), primary_key=True, default=lambda: str(uuid.uuid4())
1615
)
17-
status: Mapped[str] = mapped_column(
18-
String(20), default=JobStatus.PENDING, index=True
19-
)
16+
status: Mapped[str] = mapped_column(String(20), default=JobStatus.PENDING, index=True)
2017
progress: Mapped[int] = mapped_column(Integer, default=0)
2118

2219
model_name: Mapped[str] = mapped_column(String(256))

dashboard/backend/app/schemas/job.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
from datetime import datetime
22

3-
from pydantic import BaseModel, Field
4-
53
from app.constants import ChatTaskStatus, InferenceStatus, JobStatus
4+
from pydantic import BaseModel, Field
65

76

87
class QuantParams(BaseModel):

dashboard/backend/app/services/inference.py

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77

88
import httpx
99
import torch
10-
11-
from app.core.config import settings
1210
from app.constants import InferenceStatus
11+
from app.core.config import settings
1312
from app.core.database import SessionLocal
1413
from app.models.job import Job
1514
from app.schemas.job import ChatMessage
@@ -28,6 +27,7 @@
2827

2928
# ── Mock implementations ──────────────────────────────────────
3029

30+
3131
def deploy_mock(job_id: str) -> None:
3232
update_job(job_id, inference_status=InferenceStatus.DEPLOYING)
3333
time.sleep(1)
@@ -36,12 +36,14 @@ def deploy_mock(job_id: str) -> None:
3636

3737
# ── OneComp implementations (CPU / MPS) ──────────────────────
3838

39+
3940
def deploy_onecomp(job_id: str, model_name: str, model_dir: str) -> None:
4041
"""Load quantized model directly from local path."""
4142
update_job(job_id, inference_status=InferenceStatus.DEPLOYING)
4243

4344
try:
4445
from onecomp import load_quantized_model
46+
4547
model, tokenizer = load_quantized_model(model_dir)
4648

4749
if settings.device == "mps" and torch.backends.mps.is_available():
@@ -87,7 +89,7 @@ def chat_onecomp(
8789
temperature=temperature if temperature > 0 else None,
8890
)
8991
generated = tokenizer.decode(
90-
output_ids[0][inputs["input_ids"].shape[1]:],
92+
output_ids[0][inputs["input_ids"].shape[1] :],
9193
skip_special_tokens=True,
9294
)
9395

@@ -139,8 +141,9 @@ def _pick_gpu_memory_utilization() -> float:
139141
free, total = torch.cuda.mem_get_info()
140142
free_gb, total_gb = free / (1 << 30), total / (1 << 30)
141143
util = round(min(max((free_gb * 0.9) / total_gb, 0.1), 0.9), 2)
142-
logger.info("GPU memory: %.1f/%.1f GiB free → gpu-memory-utilization=%.2f",
143-
free_gb, total_gb, util)
144+
logger.info(
145+
"GPU memory: %.1f/%.1f GiB free → gpu-memory-utilization=%.2f", free_gb, total_gb, util
146+
)
144147
return util
145148

146149

@@ -170,13 +173,20 @@ def deploy_vllm(job_id: str, model_name: str, model_dir: str) -> None:
170173
env.setdefault("NCCL_SOCKET_FAMILY", "AF_INET")
171174

172175
cmd = [
173-
settings.vllm_python, "-m", "vllm.entrypoints.openai.api_server",
174-
"--model", model_dir,
175-
"--served-model-name", model_name,
176-
"--port", str(port),
177-
"--host", "0.0.0.0",
176+
settings.vllm_python,
177+
"-m",
178+
"vllm.entrypoints.openai.api_server",
179+
"--model",
180+
model_dir,
181+
"--served-model-name",
182+
model_name,
183+
"--port",
184+
str(port),
185+
"--host",
186+
"0.0.0.0",
178187
"--trust-remote-code",
179-
"--gpu-memory-utilization", str(gpu_util),
188+
"--gpu-memory-utilization",
189+
str(gpu_util),
180190
"--no-enable-log-requests",
181191
"--enforce-eager",
182192
]
@@ -200,13 +210,16 @@ def deploy_vllm(job_id: str, model_name: str, model_dir: str) -> None:
200210
with open(log_path) as f:
201211
lines = f.readlines()
202212
error_lines = [
203-
l.rstrip() for l in lines
213+
l.rstrip()
214+
for l in lines
204215
if "ERROR" in l or "ValueError" in l or "RuntimeError" in l
205216
]
206217
tail = "".join(lines[-40:])
207218
raise RuntimeError(
208219
f"vLLM exited with code {proc.returncode}:\n"
209-
+ "\n".join(error_lines[-20:]) + "\n---\n" + tail
220+
+ "\n".join(error_lines[-20:])
221+
+ "\n---\n"
222+
+ tail
210223
)
211224
try:
212225
resp = httpx.get(health_url, timeout=5)
@@ -217,9 +230,7 @@ def deploy_vllm(job_id: str, model_name: str, model_dir: str) -> None:
217230

218231
else:
219232
proc.terminate()
220-
raise RuntimeError(
221-
f"vLLM failed to become healthy within {_VLLM_HEALTH_TIMEOUT}s"
222-
)
233+
raise RuntimeError(f"vLLM failed to become healthy within {_VLLM_HEALTH_TIMEOUT}s")
223234

224235
inference_url = f"http://{host}:{port}"
225236
update_job(
@@ -266,6 +277,7 @@ def chat_vllm(
266277

267278
# ── Lifecycle ─────────────────────────────────────────────────
268279

280+
269281
def stop_inference(job_id: str) -> None:
270282
_loaded_models.pop(job_id, None)
271283

dashboard/backend/app/worker/celery_app.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from celery import Celery
2-
31
from app.core.config import settings
2+
from celery import Celery
43

54
celery_app = Celery(
65
"onecomp_worker",

dashboard/backend/app/worker/tasks.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import threading
77
import time
88

9-
from app.core.config import settings
109
from app.constants import InferenceStatus, JobStatus
10+
from app.core.config import settings
1111
from app.core.database import SessionLocal
1212
from app.models.job import Job
1313
from app.services.job_store import update_job
@@ -171,9 +171,7 @@ def _run_real_quantization(job_id: str, model_name: str, quant_method: str, para
171171
# VRAM budget. Matches onecomp.Runner.auto_run.
172172
wbits = math.floor(est.target_bitwidth * 100) / 100
173173
if wbits <= 0:
174-
raise RuntimeError(
175-
f"VRAM-based wbits estimation returned non-positive value: {wbits}"
176-
)
174+
raise RuntimeError(f"VRAM-based wbits estimation returned non-positive value: {wbits}")
177175
resolved_params = {
178176
**params,
179177
"bits": wbits,
@@ -247,6 +245,7 @@ def run_quantization(self, job_id: str):
247245
gc.collect()
248246
try:
249247
import torch
248+
250249
if torch.cuda.is_available():
251250
torch.cuda.empty_cache()
252251
logger.info("Released CUDA cache after quantization")

0 commit comments

Comments
 (0)