swarm-server / server /app.py
abhimittal's picture
Deploy swarm parameter server
752f957 verified
Raw
History Blame Contribute Delete
4.98 kB
"""FastAPI parameter server — the swarm's "Parameter Server" on a Hugging Face Space.
Endpoints
---------
GET /health liveness probe
GET /config model + swarm config (so a worker can self-verify it matches)
GET /weights current global weights (safetensors) + X-Model-Version header
POST /gradients upload gradients (safetensors) -> aggregated -> maybe a step
GET /status JSON snapshot: version, step, pending, mode, last_loss
GET /checkpoint download current weights as a safetensors file
Run locally: uvicorn server.app:app --host 0.0.0.0 --port 7860
On HF Spaces: the Dockerfile runs exactly that command on port 7860.
"""
from __future__ import annotations
import io
from typing import Optional
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import JSONResponse, Response, StreamingResponse
from server.aggregator import (
GradientAggregator,
InvalidGradientError,
StaleGradientError,
)
from server.state import GlobalState
from swarm import protocol as P
from swarm.config import ModelConfig, SwarmConfig
def create_app() -> FastAPI:
swarm_cfg = SwarmConfig().validate()
state = GlobalState.bootstrap(ModelConfig(), swarm_cfg)
aggregator = GradientAggregator(state)
app = FastAPI(title="Swarm Parameter Server", version="0.1.0")
app.state.global_state = state
app.state.aggregator = aggregator
app.state.swarm_cfg = swarm_cfg
def _check_auth(authorization: Optional[str]) -> None:
token = swarm_cfg.token
if not token:
return
if authorization != P.bearer(token):
raise HTTPException(status_code=401, detail="invalid or missing token")
@app.get(P.EP_HEALTH)
def health():
return {"status": "ok"}
@app.get(P.EP_CONFIG)
def config():
return {
"model": state.model_cfg.to_dict(),
"mode": swarm_cfg.mode,
"world_size": swarm_cfg.world_size,
"auth_required": bool(swarm_cfg.token),
}
@app.get(P.EP_STATUS)
def status():
return aggregator.status()
@app.get(P.EP_WEIGHTS)
async def weights():
data, version = await run_in_threadpool(_serialize_weights, state)
return Response(
content=data,
media_type=P.CONTENT_TYPE,
headers={P.H_MODEL_VERSION: str(version)},
)
@app.get(P.EP_CHECKPOINT)
async def checkpoint():
data, version = await run_in_threadpool(_serialize_weights, state)
return StreamingResponse(
io.BytesIO(data),
media_type=P.CONTENT_TYPE,
headers={
P.H_MODEL_VERSION: str(version),
"Content-Disposition": f'attachment; filename="global_v{version}.safetensors"',
},
)
@app.post(P.EP_GRADIENTS)
async def gradients(
request: Request,
x_model_version: int = Header(...),
x_loss: Optional[float] = Header(default=None),
x_worker_id: Optional[str] = Header(default=None),
authorization: Optional[str] = Header(default=None),
):
_check_auth(authorization)
body = await request.body()
try:
result = await run_in_threadpool(
_process_gradients, aggregator, body, x_model_version, x_loss
)
except StaleGradientError as e:
raise HTTPException(status_code=409, detail=str(e))
except InvalidGradientError as e:
raise HTTPException(status_code=400, detail=str(e))
return JSONResponse(
{
"accepted": result.accepted,
"applied": result.applied,
"version": result.version,
"step": result.step,
"pending": result.pending,
"last_loss": result.last_loss,
"worker_id": x_worker_id,
}
)
return app
def _serialize_weights(state: GlobalState):
# Read version and weights together; a concurrent step may bump version between
# the two, which is harmless — the worker reports whichever version it received.
version = state.version
data = P.serialize_tensors(state.weights_state_dict())
return data, version
def _process_gradients(
aggregator: GradientAggregator, body: bytes, version: int, loss: Optional[float]
):
grads = P.deserialize_tensors(body)
return aggregator.submit(grads, worker_version=version, loss=loss)
app = create_app()
def main() -> None:
"""Console-script entrypoint: ``swarm-server``."""
import os
import uvicorn
uvicorn.run(
"server.app:app",
host=os.environ.get("SWARM_HOST", "0.0.0.0"),
port=int(os.environ.get("SWARM_PORT", os.environ.get("PORT", 7860))),
log_level=os.environ.get("SWARM_LOG_LEVEL", "info"),
)
if __name__ == "__main__":
main()