Self-hosted inference¶
Hawk can serve open-weights models on its own GPUs: EC2 Capacity Block nodes running vLLM, in any region, behind one
HTTPS endpoint that Middleman calls like any other provider. It is off unless the stack sets hawk:inference.
Middleman ─ HTTPS ─> internal ALB ─> vllm-router (ECS, hawk's VPC) ─┬─ IPv6 peering ─> cell us-east-1: GPU nodes
└─ IPv6 peering ─> cell …: GPU nodes
How it works¶
- One endpoint. Middleman calls
https://<hostnameLabel>.<privateDomain>/v1and knows nothing about cells or nodes. Only Middleman's security group reaches the ALB, which forwards/v1/*only. The routers decide where each request goes (by defaultcache_aware, which keeps a conversation on the node that holds its prefix cache), across every node of every cell. Current router builds key each request by its prompt tokens, which they get by having a node render it (POST /v1/chat/completions/renderand its/v1/completionsand/v1/responsessiblings: the chat template and tokenizer applied, nothing generated). Releases up to v0.1.16.1 key chat onsession_params.session_id, which Middleman doesn't send, and so balance it by load alone. - Cells. Each GPU region gets a VPC of its own, peered with hawk's. Its GPU subnet is IPv6-only and its route table holds the local route, the S3 gateway endpoint (weights) and the peering routes back to the router tier, nothing else: no internet, no NAT. Security groups name the router tier's IPv6 ranges, since they can't reference groups across regions. EFA-only interfaces on every network card carry inter-node traffic in a group that admits only itself.
- No keys. The lab uses
credential_kind: none: Middleman sends no credential, and its key-host allowlist admits the endpoint underNO_CREDENTIAL(added automatically). The network is the identity. - No way in. Nodes have no SSH key, no SSM agent (Image Builder removes it after the build), and an instance role that can only read the cell's bucket and write its own status there. Nothing on a node changes except through a deploy.
- Nodes. The engine runs as a rootless container under rootless containerd, with the GPUs, EFA devices and host network. Everything a node runs is baked into its AMI by EC2 Image Builder (built in hawk's region, which has NAT, and copied to the cell's), so nodes never pull images or reach the internet. A node's root volume restores at a provisioned rate rather than on first read, and weights sync from the cell's bucket to instance-store NVMe at boot, at the node's network bandwidth.
- Engine updates. A change to a cell's engine arguments or model reaches the running nodes in place, in minutes. The deploy writes the engine configuration to the cell's bucket, which starts the cell's rollout (a Step Functions state machine). It tags one node at a time with the new version, which the node sees through instance metadata within a second; the node copies any new weights while its engine still serves, restarts the engine, and reports back through the bucket. A node whose engine doesn't serve goes back to the last configuration that did, and the rollout then takes every node back to the cell's current configuration and fails. Nodes launched later start on the last configuration a rollout completed.
- Node updates. A change to the node image (including the engine image) builds a new AMI and a new launch template version, and the Auto Scaling group's Rolling instance refresh replaces the nodes one at a time. The block has no spare slot, so each node is terminated before its replacement launches, and the replacement syncs its weights from S3 again. It also gets a new address, which the routers' worker-sync sidecar picks up.
- Capacity Blocks. A cell always asks for
nodesof the block's slots (all of them by default), whether or not the block is active. Before the block starts, after it ends, or while something else that launches into the same reservation holds a slot, the group's launches fail and it keeps retrying. If the group runs fewer nodes than it wants for 30 minutes, a Datadog monitor notifies the stack's notification target, and a person decides what to do. The monitor reads the group'sAWS/AutoScalingmetrics, so Datadog's AWS integration must collect that namespace (add it tohawk:datadogAwsIntegrationMetricsif that list is set, or wherever the integration is managed). EC2 itself starts terminating the nodes 30 minutes before the block ends. On a clean shutdown the engine gets SIGTERM and drains for up to 60 seconds (--shutdown-timeout), so only requests still running after that are cut. After about a day of failed launches, Auto Scaling may suspend the group's launches. If that happens, resume them (aws autoscaling resume-processes --auto-scaling-group-name <group>), and deploy a new block's cell close to the block's start. - Router deploys. A deploy that changes the routers (a new router image, say) starts new ones, then drains each old one for up to an hour, the ALB's limit. A stream still running after that is cut.
- Worker list. A worker-sync sidecar in each router task asks EC2 for every cell's running nodes every 15
seconds. It adds a node to its router once the engine passes
/healthand removes nodes that are no longer running, so new and replaced nodes join without a deploy. Until the first node is ready, the router's only worker is a placeholder that answers inference requests with 503. - Telemetry. Each node's OpenTelemetry Collector sends the engine's metrics and logs to the routers' Datadog agents through the ALB, over the peering.
Setting it up¶
- Buy the Capacity Block, and decide how many of its slots the cell runs (
nodes; two or more so updates keep serving). - Set
hawk:inference(seePulumi.example.yaml) and deploy. The first deploy creates each cell's weights bucket and builds the cell's AMI, which takes most of an hour. -
Mirror the weights into the cell's bucket and set the prefix it prints as the cell's
engine.modelS3Prefix:hawk inference mirror-weights <org>/<model> --revision <branch-tag-or-commit> --cell <cell> --env <env>It resolves the revision to a commit and starts the stack's weights mirror, a Fargate task that streams every file from Hugging Face into
models/<org>/<model>/<commit>/, many at once, checking each file's hash before it lands. Rerunning it skips the files already there. It needs your AWS credentials, withssm:GetParameteron/<env>/inference/weights-mirror,ecs:RunTaskon the mirror's task definition andiam:PassRoleon its two roles (<env>-inference-weights-mirror-execand-task), and, to follow the task,ecs:DescribeTasksandlogs:GetLogEventson/ecs/<env>-inference-weights-mirror. It fetches only public, ungated models. Weights from elsewhere can be uploaded under any prefix of the bucket instead. Nodes wait for their weights at boot; the routers add each node once its engine serves. 4. Add the labs and models to Middleman. A lab serves one API, so add a lab and a model for each: theanthropicone is what Claude Code calls (itsbase_urlhas no/v1, since Middleman appends/v1/messages).hawk proxy labs add self-hosted --config '{"api": "openai", "base_url": "https://inference.<privateDomain>/v1", "credential_kind": "none", "provider": "self-hosted"}' hawk proxy labs add self-hosted-anthropic --config '{"api": "anthropic", "base_url": "https://inference.<privateDomain>", "credential_kind": "none", "provider": "self-hosted"}' hawk proxy models add <public-name> --group <group> --config '{"lab": "self-hosted", "danger_name": "<servedModelName>", "are_details_secret": true, "is_zdr": true, "raw_cot": true}' hawk proxy models add <public-name>-anthropic --group <group> --config '{"lab": "self-hosted-anthropic", "danger_name": "<servedModelName>", "are_details_secret": true, "is_zdr": true, "raw_cot": true}'raw_cotistruefor a model whose responses carry its unsummarized reasoning, as an open-weight reasoning model's do through vLLM, andfalsefor one that returns none.The labs'
provider: self-hostedmarks their models as served on your own infrastructure: Hawk's/trafficAPI never serves those models' request and response bodies, since they may be sent other models' raw chain-of-thought. That is decided from the model's current lab, so don't later re-point one of these model names at an external lab, or change these labs'provider; register a new name (or lab) for the external route instead (security).