Angshul's picture
Upload 14 files
a42d9ad verified
Raw History Blame Contribute Delete
7.99 kB
"""Bounded HTTP job service for local checkpoint manifests."""
from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager
import fcntl
import json
import logging
from pathlib import Path
import secrets
from threading import BoundedSemaphore
from uuid import UUID, uuid4
from fastapi import Depends, FastAPI, HTTPException, Request
from fastapi.responses import FileResponse
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, ConfigDict, Field
from .core import _kernel
from .files import prune_manifest
class JobRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
manifest: str = Field(min_length=1, max_length=1024)
seed: int = Field(default=0, ge=0, lt=1 << 64)
class CheckpointJobRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
checkpoint: str = Field(default=".", min_length=1, max_length=1024)
keep: int = Field(strict=True, ge=0, lt=1 << 63)
moments_manifest: str | None = Field(default=None, min_length=1, max_length=1024)
seed: int = Field(default=0, ge=0, lt=1 << 64)
def _write_state(directory, run_id, state, **fields):
path = directory / "state.json"
tmp = directory / ".state.json.tmp"
tmp.write_text(json.dumps(dict(state=state, run_id=run_id, **fields)))
tmp.replace(path)
def create_app(input_root, job_root, *, workers=1, queue_size=2, threads_per_job=1,
max_weights=100_000_000, token=None, require_token=False,
checkpoint_root=None):
"""Create a single-process HTTP service with configurable CPU and queue limits."""
if min(workers, threads_per_job, max_weights) < 1 or queue_size < 0:
raise ValueError("invalid worker, queue, thread, or weight limit")
inputs = Path(input_root).resolve(strict=True)
jobs = Path(job_root).resolve()
checkpoints = Path(checkpoint_root).resolve(strict=True) if checkpoint_root is not None else None
if jobs.is_relative_to(inputs) or inputs.is_relative_to(jobs):
raise ValueError("input and job roots must be separate directories")
@asynccontextmanager
async def lifespan(app):
jobs.mkdir(parents=True, exist_ok=True)
with (jobs / ".server.lock").open("a+b") as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError as exc:
raise RuntimeError("another service already owns this job directory") from exc
_kernel()
app.state.run_id = uuid4().hex
app.state.slots = BoundedSemaphore(workers + queue_size)
with ThreadPoolExecutor(max_workers=workers, thread_name_prefix="prune") as pool:
app.state.pool = pool
yield
app = FastAPI(title="Tensor Pruning", version="1.0.0", lifespan=lifespan)
bearer = HTTPBearer(auto_error=False)
api_key = APIKeyHeader(name="X-Tensor-Pruning-Token", auto_error=False)
def authenticate(credentials: HTTPAuthorizationCredentials | None = Depends(bearer),
space_token: str | None = Depends(api_key)):
if not token:
raise HTTPException(503, "The operator must configure the API token")
supplied = space_token if space_token is not None else (credentials.credentials if credentials else "")
if not secrets.compare_digest(supplied.encode(), token.encode()):
raise HTTPException(401, "A valid bearer token is required",
headers={"WWW-Authenticate": "Bearer"})
protected = [Depends(authenticate)] if token or require_token else []
@app.get("/health")
def health():
return {"status": "ok"}
def read_job(job_id, request):
directory = jobs / str(job_id)
try:
state = json.loads((directory / "state.json").read_text())
except FileNotFoundError as exc:
raise HTTPException(404, "Unknown job") from exc
run_id = state.pop("run_id")
if run_id != request.app.state.run_id and state["state"] in {"queued", "running"}:
state["state"] = "interrupted"
return directory, state
def enqueue(request, operation):
state = request.app.state
if not state.slots.acquire(blocking=False):
raise HTTPException(429, "Pruning queue is full", headers={"Retry-After": "1"})
job_id = uuid4()
directory = jobs / str(job_id)
def run():
try:
_write_state(directory, state.run_id, "running")
result = operation(directory)
_write_state(directory, state.run_id, "completed", result=result)
except (ValueError, TypeError, OSError, MemoryError) as exc:
_write_state(directory, state.run_id, "failed", error=str(exc))
except Exception:
logging.getLogger(__name__).exception("Pruning job failed")
_write_state(directory, state.run_id, "failed", error="Internal pruning failure")
finally:
state.slots.release()
try:
directory.mkdir()
_write_state(directory, state.run_id, "queued")
state.pool.submit(run)
except Exception:
state.slots.release()
raise
return {"id": str(job_id), "state": "queued"}
@app.post("/v1/jobs", status_code=202, dependencies=protected)
def submit(body: JobRequest, request: Request):
manifest = Path(body.manifest)
if manifest.is_absolute() or not (inputs / manifest).resolve().is_relative_to(inputs):
raise HTTPException(400, "Manifest must be inside the input root")
return enqueue(request, lambda directory: prune_manifest(
inputs, body.manifest, directory / "output", threads=threads_per_job,
seed=body.seed, max_weights=max_weights))
if checkpoints is not None:
from .checkpoint import prune_checkpoint
@app.post("/v1/checkpoint-jobs", status_code=202, dependencies=protected)
def submit_checkpoint(body: CheckpointJobRequest, request: Request):
relative = Path(body.checkpoint)
checkpoint = (checkpoints / relative).resolve()
if relative.is_absolute() or not checkpoint.is_relative_to(checkpoints):
raise HTTPException(400, "Checkpoint must be inside the model mount")
if not checkpoint.is_dir():
raise HTTPException(404, "Mounted checkpoint directory not found")
return enqueue(request, lambda directory: prune_checkpoint(
checkpoint, directory / "output", keep=body.keep, workspace=directory,
max_weights=max_weights, input_root=inputs,
moments_manifest=body.moments_manifest, threads=threads_per_job, seed=body.seed))
@app.get("/v1/jobs/{job_id}", dependencies=protected)
def status(job_id: UUID, request: Request):
_, state = read_job(job_id, request)
return {"id": str(job_id), **state}
@app.get("/v1/jobs/{job_id}/masks/{index}", dependencies=protected,
response_class=FileResponse, responses={
200: {"content": {"application/octet-stream": {
"schema": {"type": "string", "format": "binary"}
}}}
})
def mask(job_id: UUID, index: int, request: Request):
directory, state = read_job(job_id, request)
if state["state"] != "completed":
raise HTTPException(409, "The job has no completed masks")
projections = state["result"]["projections"]
if not 0 <= index < len(projections):
raise HTTPException(404, "Unknown projection index")
filename = projections[index]["mask"]
return FileResponse(directory / "output" / filename,
filename=filename, media_type="application/octet-stream")
return app