From 22707d021cee4fb2a3c36dfcdbbda6014cf9c519 Mon Sep 17 00:00:00 2001
From: invi-bhagyesh <104337027+invi-bhagyesh@users.noreply.github.com>
Date: Thu, 24 Sep 2026 18:31:43 +0530
Subject: [PATCH 1/4] Add CPU, multi-GPU, and workspace storage evaluation
controls
---
.github/workflows/backend-tests.yml | 2 +-
backend/Dockerfile.worker | 2 +-
backend/Dockerfile.worker-base | 2 +-
backend/README.md | 18 +++++++++++
backend/app/api.py | 16 +++++----
backend/app/config.py | 2 +-
backend/app/governance.py | 6 +++-
backend/app/models.py | 9 ++++++
backend/app/progress.py | 3 ++
backend/app/runpod.py | 26 ++++++++++++---
backend/app/spec.py | 2 ++
backend/app/upstream.py | 2 +-
backend/tests/test_backend.py | 14 ++++++++
backend/tests/test_run_activity.py | 2 +-
backend/tests/test_runpod.py | 28 ++++++++++++++++
next-src/src/components/EvaluationRunner.tsx | 34 ++++++++++++++------
next-src/src/components/RunMonitor.tsx | 5 +--
next-src/src/lib/evaluation.ts | 1 +
18 files changed, 145 insertions(+), 29 deletions(-)
diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml
index fab53145..c7c3be82 100644
--- a/.github/workflows/backend-tests.yml
+++ b/.github/workflows/backend-tests.yml
@@ -31,7 +31,7 @@ jobs:
- uses: actions/checkout@v4
with:
repository: jchang153/EigenBench
- ref: 5e7520f71fd74fb3c884ae1a02534c8489348dbd
+ ref: c4ef51394d30ad9824eb76012db24e813f1a76c1
path: upstream-eigenbench
- uses: actions/setup-python@v5
with:
diff --git a/backend/Dockerfile.worker b/backend/Dockerfile.worker
index e431b222..1b7588e9 100644
--- a/backend/Dockerfile.worker
+++ b/backend/Dockerfile.worker
@@ -1,6 +1,6 @@
# Reuse the verified CUDA toolkit and GPU dependency image.
FROM ghcr.io/valuearena/evaluation-worker@sha256:66adcf3c9f6672826eba35cab072e8d5e5db3cbe67fcf2c71dac5a76ae4d533d
-ARG EIGENBENCH_REVISION=5e7520f71fd74fb3c884ae1a02534c8489348dbd
+ARG EIGENBENCH_REVISION=c4ef51394d30ad9824eb76012db24e813f1a76c1
RUN cd /opt/eigenbench \
&& git remote set-url origin https://github.com/jchang153/EigenBench.git \
&& git fetch origin "$EIGENBENCH_REVISION" \
diff --git a/backend/Dockerfile.worker-base b/backend/Dockerfile.worker-base
index b5670d4e..4b385d3f 100644
--- a/backend/Dockerfile.worker-base
+++ b/backend/Dockerfile.worker-base
@@ -4,7 +4,7 @@ FROM nvidia/cuda:13.0.2-devel-ubuntu22.04@sha256:6b6617592b94e7dcc6ffbe6d00720ee
FROM python:3.11-slim
COPY --from=cuda-toolkit /usr/local/cuda-13.0 /usr/local/cuda
ENV CUDA_HOME=/usr/local/cuda PATH=/usr/local/cuda/bin:$PATH LD_LIBRARY_PATH=/usr/local/cuda/lib64
-ARG EIGENBENCH_REVISION=5e7520f71fd74fb3c884ae1a02534c8489348dbd
+ARG EIGENBENCH_REVISION=c4ef51394d30ad9824eb76012db24e813f1a76c1
RUN apt-get update && apt-get install -y --no-install-recommends git libgomp1 build-essential && rm -rf /var/lib/apt/lists/*
RUN git clone https://github.com/jchang153/EigenBench.git /opt/eigenbench \
&& cd /opt/eigenbench && git checkout "$EIGENBENCH_REVISION"
diff --git a/backend/README.md b/backend/README.md
index c8f2cf17..f8af4cef 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -433,3 +433,21 @@ service HF_TOKEN when no personal token is supplied. Configure that service toke
on both API and scheduler; own-provider runs never inherit the service token.
Native OpenRouter attempts emit `[API]` lines in worker logs with timing, token
usage and retry status, without prompts or response bodies.
+
+### Compute selection
+
+Evaluations can request `compute_type: "cpu"` for API-only panels, with
+`cpu_count` (2, 4, 8, 16, or 32) and `cpu_flavor` (`cpu3c` for 2 GB RAM per
+vCPU or `cpu3g` for 4 GB). Hugging Face panels require GPU compute.
+Native GPU runs accept `gpu_count` of 1, 2, 4, or 8; all GPUs share one
+instance and each local model uses tensor parallelism across that count.
+Inspect currently accepts one GPU. Models must support the chosen parallel size.
+
+`disk_gb` controls container storage. Optional `volume_gb` mounts workspace
+storage at `/workspace`; it survives restarts but is deleted with the pod.
+Saved results remain in the configured results storage. The admin disk limit
+applies to the sum of both disks; CPU and GPU counts have separate admin limits.
+Custom compute is available to admins and users supplying their own provider keys.
+
+The worker image must include the pinned EigenBench revision for multi-GPU
+execution; `EIGENBENCH_TENSOR_PARALLEL_SIZE` is set by the scheduler.
diff --git a/backend/app/api.py b/backend/app/api.py
index 44007d4e..6678189c 100644
--- a/backend/app/api.py
+++ b/backend/app/api.py
@@ -28,7 +28,7 @@
def public_job(job):
return {key: job[key] for key in ('id', 'state', 'stage', 'created_at', 'started_at', 'finished_at',
'reserved_credits', 'charged_credits', 'error_code')} | {
- 'progress': progress(job), 'name': job['config']['name'], 'engine': job['config']['engine'],
+ 'compute_type':job['config'].get('compute_type','gpu'), 'progress': progress(job), 'name': job['config']['name'], 'engine': job['config']['engine'],
'has_artifacts': bool(job['artifact']),
'funding': job['config'].get('funding', 'service'),
'visibility': job['config'].get('visibility', 'private'),
@@ -154,7 +154,7 @@ def health(): return {'status': 'ok'}
@app.get('/models')
def models():
- return {'models': [{'id': key, 'label': entry['label']} for key, entry in catalog.items()],
+ return {'models': [{'id': key, 'label': entry['label'], 'provider': 'huggingface' if isinstance(entry['ref'], dict) else 'openrouter'} for key, entry in catalog.items()],
'engines': ['native', 'inspect'], 'default_engine': 'native', 'evaluation_mode': 'direct_rating'}
@app.get('/models/openrouter')
@@ -162,12 +162,12 @@ def available_openrouter(user_id=Depends(user)):
return {'models': openrouter_models()}
@app.get('/compute/availability')
- def compute_availability(disk_gb: int = Query(default=100, ge=50, le=1000), user_id=Depends(user)):
+ def compute_availability(disk_gb: int = Query(default=100, ge=50, le=2000), gpu_count: int = Query(default=1, ge=1, le=8), user_id=Depends(user)):
if db.member(user_id)['status'] != 'approved':
raise HTTPException(403, 'Account approval required')
from .runpod import gpu_availability
try:
- return gpu_availability(disk_gb)
+ return gpu_availability(disk_gb, gpu_count)
except Exception:
raise HTTPException(503, 'RunPod availability could not be checked. Try again shortly.') from None
@@ -186,6 +186,8 @@ def spec_preview(incoming: EvaluationRequest, user_id=Depends(user)):
import pprint
config = effective_config(incoming,user_id)
config['model_refs'] = resolve_models(incoming, catalog, incoming.hf_token.get_secret_value() or (cfg.hf_token if incoming.funding == 'service' else ''))
+ if incoming.compute_type == 'cpu' and any(isinstance(ref, dict) for ref in config['model_refs'].values()):
+ raise HTTPException(422, 'CPU evaluations support API models only; Hugging Face models require GPU')
spec = build_spec(config, '.')
return {'spec': spec, 'python': 'RUN_SPEC = ' + pprint.pformat(spec, sort_dicts=False) + '\n'}
@@ -219,10 +221,12 @@ async def submit(request: Request, user_id=Depends(user), idempotency_key: str =
raise HTTPException(422, 'Supply both your OpenRouter and RunPod API keys')
if incoming.funding == 'own_keys':
await run_in_threadpool(verify_provider_keys, keys)
- if incoming.funding == 'service' and db.member(user_id)['role']!='admin' and (incoming.gpu_type != cfg.runpod_gpu_type or incoming.disk_gb != cfg.runpod_disk_gb):
+ if incoming.funding == 'service' and db.member(user_id)['role']!='admin' and (incoming.gpu_type != cfg.runpod_gpu_type or incoming.disk_gb != cfg.runpod_disk_gb or incoming.gpu_count != 1 or incoming.compute_type != 'gpu' or incoming.cpu_count != 4 or incoming.cpu_flavor != 'cpu3g' or incoming.volume_gb != 0):
raise HTTPException(422, 'Custom compute requires your own provider keys')
digest = hashlib.sha256(json.dumps(config, sort_keys=True).encode()).hexdigest()
# Snapshot the catalog: queued jobs do not silently change if administrators update it.
+ if incoming.compute_type == 'cpu' and any(isinstance(ref, dict) for ref in refs.values()):
+ raise HTTPException(422, 'CPU evaluations support API models only. Select GPU for Hugging Face models.')
config['model_refs'] = refs
if incoming.funding != 'own_keys': keys = {}
if incoming.hf_token.get_secret_value(): keys['hf_token'] = incoming.hf_token.get_secret_value()
@@ -274,7 +278,7 @@ def activity(job):
return {'text':db.get_presentation(job['id'],'log') or '',
'provider':db.get_presentation(job['id'],'provider_activity'),
'pod_name':'valuearena-'+job['id'] if job.get('pod_id') else None,
- 'gpu':job['config'].get('gpu_type',cfg.runpod_gpu_type),
+ 'gpu':f"{job['config'].get('cpu_count', 4)} vCPUs" if job['config'].get('compute_type') == 'cpu' else f"{job['config'].get('gpu_count', 1)} × {job['config'].get('gpu_type',cfg.runpod_gpu_type)}",
'state':job['state'],'stage':job['stage'],'progress':progress({**job, 'allocation':db.get_presentation(job['id'],'allocation')})}
@app.get('/evaluations/{job_id}/logs')
diff --git a/backend/app/config.py b/backend/app/config.py
index 4a821abe..08bac5d9 100644
--- a/backend/app/config.py
+++ b/backend/app/config.py
@@ -20,7 +20,7 @@ class Settings(BaseSettings):
runpod_api_key: str = ''
worker_image: str = ''
runpod_gpu_type: str = 'NVIDIA A40'
- runpod_gpu_count: int = Field(default=1, ge=1, le=1)
+ runpod_gpu_count: int = Field(default=1, ge=1, le=8)
runpod_disk_gb: int = Field(default=100, ge=30, le=1000)
max_running_jobs: int = Field(default=1, ge=1, le=10)
scheduler_interval: int = Field(default=10, ge=1, le=60)
diff --git a/backend/app/governance.py b/backend/app/governance.py
index b37b62d3..a22354db 100644
--- a/backend/app/governance.py
+++ b/backend/app/governance.py
@@ -15,6 +15,8 @@ class Limits(BaseModel):
max_tokens: int | None = Field(None, ge=128)
max_outstanding_jobs: int | None = Field(None, ge=1)
max_disk_gb: int | None = Field(None, ge=50)
+ max_gpu_count: int | None = Field(None, ge=1)
+ max_cpu_count: int | None = Field(None, ge=2)
max_workers: int | None = Field(None, ge=1)
max_bootstraps: int | None = Field(None, ge=1)
require_credits: bool = False
@@ -73,7 +75,9 @@ def enforce(config, limits):
checks = [(len(config['models']), 'max_models'),
(len(config['scenarios']) or config.get('scenario_count',200), 'max_scenarios'),
(config['max_runtime_seconds'],'max_runtime_seconds'),
- (config['disk_gb'],'max_disk_gb')]
+ (config['disk_gb'] + config.get('volume_gb', 0),'max_disk_gb'),
+ (config.get('gpu_count', 1) if config.get('compute_type', 'gpu') == 'gpu' else 0, 'max_gpu_count'),
+ (config.get('cpu_count', 4) if config.get('compute_type') == 'cpu' else 0, 'max_cpu_count')]
advanced = AdvancedSpec.model_validate(config.get('advanced_spec',{}))
if advanced.dataset.count: checks.append((advanced.dataset.count,'max_scenarios'))
generation = advanced.collection.generation.model_dump()
diff --git a/backend/app/models.py b/backend/app/models.py
index e200b681..03a1aa56 100644
--- a/backend/app/models.py
+++ b/backend/app/models.py
@@ -35,6 +35,11 @@ class EvaluationRequest(BaseModel):
openrouter_key: SecretStr = Field(default=SecretStr(''), max_length=512)
hf_token: SecretStr = Field(default=SecretStr(''), max_length=512, exclude=True)
runpod_key: SecretStr = Field(default=SecretStr(''), max_length=512)
+ compute_type: Literal['gpu', 'cpu'] = 'gpu'
+ gpu_count: Literal[1, 2, 4, 8] = 1
+ cpu_count: Literal[2, 4, 8, 16, 32] = 4
+ cpu_flavor: Literal['cpu3c', 'cpu3g'] = 'cpu3g'
+ volume_gb: int = Field(default=0, ge=0, le=1000)
gpu_type: Literal['NVIDIA A40', 'NVIDIA RTX A6000', 'NVIDIA GeForce RTX 4090', 'NVIDIA A100 80GB PCIe', 'NVIDIA H100 80GB HBM3'] = 'NVIDIA A40'
disk_gb: int = Field(default=100, ge=50, le=1000)
engine: Literal['native', 'inspect'] = 'native'
@@ -57,6 +62,10 @@ def unique_inputs(self):
self.scenario_source = 'custom'
if self.scenario_source == 'custom' and not self.scenarios:
raise ValueError('Supply at least one custom scenario')
+ if self.compute_type == 'cpu' and self.gpu_count != 1:
+ raise ValueError('CPU evaluations cannot request multiple GPUs')
+ if self.engine == 'inspect' and self.gpu_count > 1:
+ raise ValueError('Multiple GPUs currently require the Native engine')
ids = [m.id for m in self.custom_models]
if len(set(ids)) != len(ids) or not set(ids).issubset(self.models):
raise ValueError('Custom model IDs must be unique and selected')
diff --git a/backend/app/progress.py b/backend/app/progress.py
index 7e96a58b..2c5a8c47 100644
--- a/backend/app/progress.py
+++ b/backend/app/progress.py
@@ -47,6 +47,9 @@ def progress(job, now=None):
title, detail, step = 'Completed', 'Rankings and transcripts are ready.', 6
else:
title, detail, step = ('Cancelled' if state == 'cancelled' else 'Failed'), (provider_detail if provider_error else ERRORS.get(job.get('error_code'), 'The evaluation stopped. See worker output for details.')), -1
+ if job.get('config', {}).get('compute_type') == 'cpu':
+ title = title.replace('GPU', 'CPU')
+ detail = detail.replace('GPU', 'CPU')
end = job.get('finished_at') or now
return {'title': title, 'detail': detail, 'step': step, 'checked_at': now,
'elapsed_seconds': max(0, end-job['created_at']),
diff --git a/backend/app/runpod.py b/backend/app/runpod.py
index 62ba54be..542834ee 100644
--- a/backend/app/runpod.py
+++ b/backend/app/runpod.py
@@ -14,7 +14,7 @@ class AllocationRejected(RuntimeError):
pass
-def gpu_availability(disk_gb=100):
+def gpu_availability(disk_gb=100, gpu_count=1):
# Public stock lookup: no user/provider credentials are sent or stored.
query = """query($input: GpuLowestPriceInput!) {
gpuTypes { id lowestPrice(input: $input) {
@@ -22,7 +22,7 @@ def gpu_availability(disk_gb=100):
} }
}"""
response = httpx.post('https://api.runpod.io/graphql', json={
- 'query': query, 'variables': {'input': {'gpuCount': 1, 'secureCloud': True,
+ 'query': query, 'variables': {'input': {'gpuCount': gpu_count, 'secureCloud': True,
'minCudaVersion': MIN_CUDA_VERSION, 'minDisk': disk_gb}}}, timeout=15)
response.raise_for_status()
body = response.json()
@@ -35,7 +35,7 @@ def gpu_availability(disk_gb=100):
rows.append({'id': gpu['id'], 'stock': stock if stock in ('High','Medium','Low','None') else 'Unknown',
'price_per_hour': price.get('uninterruptablePrice')})
return {'gpus': rows, 'checked_at': int(time.time()), 'min_cuda_version': MIN_CUDA_VERSION,
- 'disk_gb': disk_gb, 'cloud': 'SECURE'}
+ 'disk_gb': disk_gb, 'gpu_count': gpu_count, 'cloud': 'SECURE'}
class RunPod:
@@ -57,13 +57,29 @@ def create(self, job):
'VA_WORKER_TOKEN': worker_token(self.cfg.worker_secret, job['id']),
'OPENROUTER_API_KEY': self.cfg.openrouter_api_key,
'HF_TOKEN': self.cfg.hf_token}
+ config = job['config']
+ if config.get('compute_type') == 'cpu':
+ response = self.client.post('/pods', json={
+ 'name': self.name(job['id']), 'imageName': self.cfg.worker_image,
+ 'computeType': 'CPU', 'cloudType': 'SECURE',
+ 'vcpuCount': config.get('cpu_count', 4),
+ 'cpuFlavorIds': [config.get('cpu_flavor', 'cpu3g')],
+ 'containerDiskInGb': config.get('disk_gb', self.cfg.runpod_disk_gb),
+ 'volumeInGb': config.get('volume_gb', 0), 'volumeMountPath': '/workspace',
+ 'env': env, 'ports': [],
+ })
+ response.raise_for_status()
+ pod_id = response.json().get('id')
+ if not pod_id: raise RuntimeError('RunPod did not confirm CPU allocation')
+ return pod_id
+ env['EIGENBENCH_TENSOR_PARALLEL_SIZE'] = str(config.get('gpu_count', 1))
# GraphQL supports a minimum version; the REST enum omits newer CUDA versions.
payload = {'name': self.name(job['id']), 'imageName': self.cfg.worker_image,
'cloudType': 'SECURE', 'computeType': 'GPU',
'gpuTypeId': job['config'].get('gpu_type', self.cfg.runpod_gpu_type),
- 'gpuCount': self.cfg.runpod_gpu_count,
+ 'gpuCount': config.get('gpu_count', self.cfg.runpod_gpu_count),
'containerDiskInGb': job['config'].get('disk_gb', self.cfg.runpod_disk_gb),
- 'minCudaVersion': MIN_CUDA_VERSION, 'volumeInGb': 0, 'ports': '',
+ 'minCudaVersion': MIN_CUDA_VERSION, 'volumeInGb': config.get('volume_gb', 0), 'volumeMountPath': '/workspace', 'ports': '',
'startSsh': False, 'startJupyter': False,
'env': [{'key': key, 'value': value} for key, value in env.items()]}
response = self.client.post('https://api.runpod.io/graphql', json={
diff --git a/backend/app/spec.py b/backend/app/spec.py
index 2054ee20..9fa19e49 100644
--- a/backend/app/spec.py
+++ b/backend/app/spec.py
@@ -8,6 +8,8 @@
def build_spec(config, directory):
request = EvaluationRequest.model_validate({k: v for k, v in config.items() if k != 'model_refs'})
+ if request.compute_type == 'cpu' and any(isinstance(ref, dict) for ref in config['model_refs'].values()):
+ raise ValueError('CPU evaluations support API models only; Hugging Face models require GPU')
root = Path(directory)
spec = {
'name': request.name,
diff --git a/backend/app/upstream.py b/backend/app/upstream.py
index 8cd29bd6..96de1187 100644
--- a/backend/app/upstream.py
+++ b/backend/app/upstream.py
@@ -1,6 +1,6 @@
"""Pinned runner contract. Defaults are checked against upstream source in CI."""
REPOSITORY = 'https://github.com/jchang153/EigenBench'
-REVISION = '5e7520f71fd74fb3c884ae1a02534c8489348dbd'
+REVISION = 'c4ef51394d30ad9824eb76012db24e813f1a76c1'
MAX_LORA_RANK = 512
# Used for preview and limit validation only; omitted settings are resolved by EigenBench.
GENERATION_DEFAULTS = {
diff --git a/backend/tests/test_backend.py b/backend/tests/test_backend.py
index a6bc07e7..1d5e7894 100644
--- a/backend/tests/test_backend.py
+++ b/backend/tests/test_backend.py
@@ -332,3 +332,17 @@ def unavailable(job): raise GPUUnavailable()
assert view['progress']['title'] == 'Waiting for GPU availability'
assert 'Attempt 1' in view['progress']['detail']
assert 'Next retry' in view['progress']['detail']
+
+
+def test_compute_limits_and_cpu_local_models(service, monkeypatch):
+ from app.governance import enforce
+ from app.db import Forbidden
+ cfg,db,client=service
+ config=EvaluationRequest(**payload(gpu_count=4,volume_gb=200)).model_dump()
+ limits=db.effective_limits(USER)
+ with pytest.raises(Forbidden,match='max_gpu_count'):enforce(config,{**limits,'max_gpu_count':2})
+ with pytest.raises(Forbidden,match='max_disk_gb'):enforce(config,{**limits,'max_disk_gb':250})
+ with pytest.raises(ValidationError):EvaluationRequest(**payload(engine='inspect',gpu_count=2))
+ monkeypatch.setattr('app.api.resolve_models', lambda *args: {'a':{'provider':'hf_local'},'b':'org/b'})
+ result=client.post('/spec-preview',json=payload(compute_type='cpu'),headers={'Authorization':'Bearer '+USER})
+ assert result.status_code==422 and 'API models only' in result.json()['detail']
diff --git a/backend/tests/test_run_activity.py b/backend/tests/test_run_activity.py
index f4d47f10..8b9344a0 100644
--- a/backend/tests/test_run_activity.py
+++ b/backend/tests/test_run_activity.py
@@ -72,7 +72,7 @@ def create(self,job):
def test_stock_endpoint_requires_login_and_hides_provider_errors(service,monkeypatch):
cfg,db,api=service
assert api.get('/compute/availability').status_code==401
- def stock(disk):return {'disk_gb':disk,'gpus':[]}
+ def stock(disk, gpu_count=1):return {'disk_gb':disk,'gpus':[]}
monkeypatch.setattr('app.runpod.gpu_availability',stock)
result=api.get('/compute/availability?disk_gb=200',headers={'Authorization':'Bearer '+USER})
assert result.status_code==200 and result.json()['disk_gb']==200
diff --git a/backend/tests/test_runpod.py b/backend/tests/test_runpod.py
index 625f8090..05607b4a 100644
--- a/backend/tests/test_runpod.py
+++ b/backend/tests/test_runpod.py
@@ -58,3 +58,31 @@ def test_only_definitive_capacity_errors_are_retryable():
with pytest.raises(expected) as exc:
pods.create({'id':str(uuid4()),'config':{}})
assert type(exc.value) is expected
+
+
+def test_cpu_allocation_and_storage_payload():
+ pods=RunPod(Settings(environment='test',worker_secret='x'*40))
+ def handle(request):
+ assert request.url.path == '/v1/pods'
+ body=json.loads(request.content)
+ assert body['computeType']=='CPU' and body['vcpuCount']==8
+ assert body['cpuFlavorIds']==['cpu3g']
+ assert body['volumeInGb']==120 and body['containerDiskInGb']==60
+ assert 'gpuCount' not in body and 'allowedCudaVersions' not in body
+ return httpx.Response(200,json={'id':'cpu-pod'})
+ with httpx.Client(transport=httpx.MockTransport(handle),base_url='https://rest.runpod.io/v1') as client:
+ pods.client.close(); pods.client=client
+ assert pods.create({'id':str(uuid4()),'config':{'compute_type':'cpu','cpu_count':8,'volume_gb':120,'disk_gb':60}})=='cpu-pod'
+
+
+def test_multiple_gpus_reach_provider_and_engine_env():
+ pods=RunPod(Settings(environment='test',worker_secret='x'*40))
+ def handle(request):
+ body=json.loads(request.content)['variables']['input']
+ assert body['gpuCount']==4 and body['volumeInGb']==200
+ env={v['key']:v['value'] for v in body['env']}
+ assert env['EIGENBENCH_TENSOR_PARALLEL_SIZE']=='4'
+ return httpx.Response(200,json={'data':{'podFindAndDeployOnDemand':{'id':'multi-pod'}}})
+ with httpx.Client(transport=httpx.MockTransport(handle)) as client:
+ pods.client.close(); pods.client=client
+ assert pods.create({'id':str(uuid4()),'config':{'gpu_count':4,'volume_gb':200}})=='multi-pod'
diff --git a/next-src/src/components/EvaluationRunner.tsx b/next-src/src/components/EvaluationRunner.tsx
index cabb8545..41b9cd4d 100644
--- a/next-src/src/components/EvaluationRunner.tsx
+++ b/next-src/src/components/EvaluationRunner.tsx
@@ -11,7 +11,7 @@ import { RunMonitor, isActive } from './RunMonitor';
import { Penguin } from './Penguin';
import { ModelLogo } from './ModelLogo';
-type Model = { id: string; label: string };
+type Model = { id: string; label: string; provider?: string };
type CustomModel = { id: string; provider: string; repo_id: string; kind: string; subfolder: string; base_model_id: string };
const presetNames = Object.keys(CONSTITUTIONS_DATA).sort();
const label = (s: string) => s.replaceAll('_', ' ').replace(/^./, c => c.toUpperCase());
@@ -37,8 +37,10 @@ export function EvaluationRunner() {
const [source, setSource] = useState('airiskdilemmas'); const [count, setCount] = useState(200);
const [scenarioText, setScenarioText] = useState(''); const [fileName, setFileName] = useState('');
const [ownKeys, setOwnKeys] = useState(false); const [orKey, setOrKey] = useState(''); const [rpKey, setRpKey] = useState(''); const [hfToken, setHfToken] = useState('');
+ const [computeType, setComputeType] = useState('gpu'); const [gpuCount, setGPUCount] = useState(1);
+ const [cpuCount, setCPUCount] = useState(4); const [cpuFlavor, setCPUFlavor] = useState('cpu3g'); const [volume, setVolume] = useState(0);
const [gpu, setGPU] = useState(gpuTypes[0]); const [disk, setDisk] = useState(100);
- const [stock, setStock] = useState<{ gpus: { id: string; stock: string; price_per_hour: number | null }[]; checked_at: number; disk_gb: number } | null>(null);
+ const [stock, setStock] = useState<{ gpus: { id: string; stock: string; price_per_hour: number | null }[]; checked_at: number; disk_gb: number; gpu_count: number } | null>(null);
const [stockBusy, setStockBusy] = useState(false); const [stockError, setStockError] = useState('');
const [visibility, setVisibility] = useState('private');
const [accessError, setAccessError] = useState('');
@@ -135,7 +137,7 @@ export function EvaluationRunner() {
setCriteria(config.criteria.join('\n')); setSource(config.scenario_source);
setCount(config.scenario_count); setScenarioText(config.scenarios.map((text: string) => JSON.stringify({scenario: text})).join('\n'));
setFileName('Restored scenarios'); setVisibility(config.visibility);
- setOwnKeys(config.funding === 'own_keys'); setGPU(config.gpu_type); setDisk(config.disk_gb);
+ setOwnKeys(config.funding === 'own_keys'); setGPU(config.gpu_type); setDisk(config.disk_gb); setComputeType(config.compute_type || 'gpu'); setGPUCount(config.gpu_count || 1); setCPUCount(config.cpu_count || 4); setCPUFlavor(config.cpu_flavor || 'cpu3g'); setVolume(config.volume_gb || 0);
setOrKey(''); setRpKey(''); setHfToken(''); submission.current = null;
setTab('new'); setError('');
setNotice('Settings restored. Review the GPU and re-enter any personal keys, then submit a new evaluation.');
@@ -145,7 +147,7 @@ export function EvaluationRunner() {
async function checkStock() {
setStockBusy(true); setStockError(''); setStock(null);
try {
- const response = await request(`/compute/availability?disk_gb=${ownKeys || isAdmin ? disk : 100}`);
+ const response = await request(`/compute/availability?disk_gb=${ownKeys || isAdmin ? disk + volume : 100}&gpu_count=${ownKeys || isAdmin ? gpuCount : 1}`);
setStock(await response.json());
} catch (e) { setStockError((e as Error).message); }
finally { setStockBusy(false); }
@@ -187,9 +189,13 @@ export function EvaluationRunner() {
scenario_source: source, scenario_count: count, scenarios: source === 'custom' ? parseScenarios(scenarioText,limits.max_scenarios ?? Infinity) : [], visibility,
hf_token: hfToken,
funding: ownKeys ? 'own_keys' : 'service', ...(ownKeys && includeKeys ? { openrouter_key: orKey, runpod_key: rpKey } : {}),
+ compute_type: ownKeys || isAdmin ? computeType : 'gpu', gpu_count: ownKeys || isAdmin ? (computeType === 'cpu' ? 1 : gpuCount) : 1, cpu_count: ownKeys || isAdmin ? cpuCount : 4, cpu_flavor: ownKeys || isAdmin ? cpuFlavor : 'cpu3g', volume_gb: ownKeys || isAdmin ? volume : 0,
gpu_type: ownKeys || isAdmin ? gpu : gpuTypes[0], disk_gb: ownKeys || isAdmin ? disk : 100 };
}
const blockers: string[] = [];
+ if ((ownKeys || isAdmin) && computeType === 'cpu' && selected.some(id => models.some(m => m.id === id && m.provider === 'huggingface') || custom.some(m => m.id === id && m.provider === 'huggingface'))) blockers.push('CPU runs support API models only. Remove Hugging Face models or select GPU.');
+ if ((ownKeys || isAdmin) && computeType === 'gpu' && gpuCount > 1 && engine === 'inspect') blockers.push('Select Native to use multiple GPUs.');
+ if ((ownKeys || isAdmin) && limits.max_disk_gb !== null && disk + volume > limits.max_disk_gb) blockers.push('Container and workspace storage exceed your total storage limit.');
if (!name.trim()) blockers.push('Enter an evaluation name.');
if (selected.length < 2) blockers.push('Select at least two models.');
if ((limits.max_models !== null && selected.length > limits.max_models)) blockers.push(`Remove models to stay within your ${limits.max_models}-model limit.`);
@@ -286,7 +292,7 @@ export function EvaluationRunner() {
04
Compute & run
-
+
{ownKeys && <>Keys are encrypted and removed after confirmed GPU cleanup.>}
@@ -294,10 +300,20 @@ export function EvaluationRunner() {
{(['response','reflection','direct_rating'] as const).map(phase=>)}
Increase reflection tokens for reasoning models. Per-model overrides are available in Advanced configuration.
{engine==='native'&&}
+
+ {computeType === 'gpu' && <>
GPU
{stockError &&
{stockError}
}
-
-
+
+ >}
+
{visibility === 'public' && Completed rankings, scenarios, responses, and judgments will be visible to everyone. Public results are copied to Hugging Face. Unlisting removes them from the current listing, but repository history and downloaded copies remain public.}
buildRequest(false)} />
@@ -310,8 +326,8 @@ export function EvaluationRunner() {
}Runs continue in the background until complete or cancelled.{limits.max_runtime_seconds!==null && ` Admin runtime limit: ${Math.round(limits.max_runtime_seconds/60)} minutes.`}{!ownKeys && limits.require_credits && " Runs also stop when their reserved execution credits are used; unused time is returned."}
}
- {tab === 'runs' &&
Your evaluations
{!jobs.length &&
Your runs will appear here, with rankings and individual judgments.
}{jobs.map(job => { const open = openRuns[job.id] ?? isActive(job.state); const body = `run-body-${job.id}`; return
+ {tab === 'runs' &&
Your evaluations
{!jobs.length &&
Your runs will appear here, with rankings and individual judgments.