Skip to main content

VitaScale: a tiny autoscaling simulator built to fit an OpenEnv validator

Share:XLinkedInHN
Cover for VitaScale: a tiny autoscaling simulator built to fit an OpenEnv validator

What VitaScale actually is

vitascale is a small OpenEnv-format simulation for cloud autoscaling agents. You get a fleet of between 2 and 30 instances, a 24-hour day compressed to 720 one-minute steps, and a policy that has to decide, at every step, whether to scale up, scale down, do nothing, or migrate load. The environment scores the policy on SLA compliance, cost, and a handful of resilience terms. The whole thing is packaged as a FastAPI service with /reset, /step, /state, and /tasks endpoints, wrapped in a Docker image, and pointed at a Hugging Face Space at kaushikss-vitascale.hf.space.

I built it as an OpenEnv submission. That is what the README says, and that is genuinely all I can claim about the target. I do not want to overstate what "OpenEnv" is here because the repo does not commit to a program. What I can say is that the format has strict expectations about how an environment exposes tasks and how a baseline inference.py writes its logs to stdout, and a good chunk of my commit history is me learning what those expectations were.

The physics of one step

There are four action types, all typed as Pydantic Literals: scale_up, scale_down, do_nothing, migrate_load. Each carries a num_instances field clamped to [0, 20], and the resulting fleet size is clamped to [2, 30]. Every instance has a fixed capacity of 175 requests per minute. Every instance costs $0.10 per step. There are 720 steps. The maximum theoretical reward is 720.0, one point per step if the policy is perfect.

flowchart LR
  A[state at t] --> B[policy chooses action]
  B --> C{action type}
  C -->|scale_up| D[fleet += n, clamp 2..30]
  C -->|scale_down| D
  C -->|do_nothing| D
  C -->|migrate_load| D
  D --> E[load trace for minute t]
  E --> F[capacity = fleet * 175 req/min]
  F --> G[SLA, cost, stability terms]
  G --> H[reward, next state, t += 1]

175 requests per minute per instance, $0.10 per instance per step, 720 minutes for a day. I picked those numbers because they gave me a well-shaped reward surface in a small notebook, not because they mean anything about a real cloud. I do not want to pretend otherwise.

Deterministic events, three tracks

Traces are generated in load_traces.py with hardcoded seeds: random.Random(42) for easy, 123 for medium, 777 for hard. That is deliberate. If the environment is going to be a benchmark, two agents need to see the exact same day.

The diurnal load is a sum of two Gaussians centred on a workday. The primary peak is at 14:00 with a multiplier of 1400 requests per minute, the secondary at 10:00 with 800, and a base of 400 the rest of the day. On top of that, the hard track has a fixed event schedule, written out as a plain dict from minute to event type:

{
  150:  "node_down",
  345:  "node_down",
  520:  "price_spike",
  690:  "node_down",
  780:  "carbon_peak",
  920:  "cascade_failure",
  1050: "price_spike",
  1200: "node_down",
  1340: "carbon_peak",
}

Nothing in there is randomised across runs. If your agent survives minute 920 on one seed, it will survive minute 920 on every seed. That is the point.

The hard track also carries four curriculum injections at minutes 180, 420, 780, and 1100. During each window, the load is forced to 600 req/min and the grader rewards the policy for staying at or below 10 instances. This is meant to teach patience: the burst is short, the temptation is to over-provision, and the grader punishes that. Without it, every policy I tried converged on the same "keep 30 instances up all day" attractor.

Grader math and one small bug fix

Each track has its own reward weighting.

Easy is SLA 40, cost 35, stability 25. Medium is SLA 35, cost 30, burst response 20, recovery 15. Hard is SLA 25, cost 25, survival 20, adaptive response 15, curriculum 15. The weights sum to 100 on every track.

There is one thing about the grader I want to mention because it took a real commit to fix. Scores are clamped to the open interval (0.001, 0.999), strictly inside (0, 1). The relevant commit message reads clamp grader scores to (0.001, 0.999), strictly inside (0, 1) (I paraphrase without its em-dash so the rest of this post can stay em-dash free). The reason is that the OpenEnv format expects a probability, and a probability of exactly 0 or exactly 1 is degenerate for a downstream logit or an information-theoretic score. Clamping to the open interval means every episode has a well-defined log score, no matter how catastrophic or how perfect. It sounds like a boring line of code. It fixed a real crash in a downstream tool.

The README claims baseline scores of 0.82 on easy, 0.68 on medium, and 0.76 on hard. I want to be careful here. Those are the numbers in the README. I have not sat down to independently reproduce them in the state the repo is in on 2026-07-09, and the exact model behind the LLM-driven baseline depends on which OpenAI-compatible endpoint the runner points at. So: those are claims, not results I would swear to in a table.

The hybrid baseline

inference.py is a hybrid policy. Every 5 steps, it calls an OpenAI-compatible chat model, by default gpt-4o-mini, at whatever API_BASE_URL the environment variable points at, with a token from HF_TOKEN. Temperature is 0.0. The model is asked to return a single JSON action. In between LLM calls, and any time the model call fails or returns something unparseable, a rule-based fallback takes over.

The rule fallback is intentionally dumb: if CPU utilisation is above 0.80 or load is above 85 percent of current capacity, scale up. If load is below 35 percent of capacity and CPU is below 0.30, scale down. Otherwise do nothing. That is enough to keep the baseline alive during a load spike even when the model call times out, and it stops the policy from thrashing when nothing interesting is happening.

The 5-step interval was a cost decision. One LLM call per step on 720 steps per episode is a lot of tokens for a benchmark run. One call per five steps, with a stateless rule fallback filling the gaps, keeps the run cheap and still gives the LLM a chance to steer during the tricky windows.

Fighting the validator

The most honest thing I can say about the commit history is that it is dominated by format compliance work. Twenty commits, all mine. The first is Initial VitaScale OpenEnv submission. After that, in order, is a run that reads roughly like this: Match sample inference format: JSON structured logs, Match exact OpenEnv sample stdout format, Fix: hardcoded paths, test port, add setuptools packages config, Align inference output with latest OpenEnv rules, the clamp fix above, and fix: restore score= field in [END] log + clamp scores strictly to (0.001, 0.999).

The environment logic itself did not really change after the first two days. What kept moving was the exact bytes that ended up on stdout. Whether the [END] line included a score= field. Whether structured logs were emitted per step or per episode. Whether paths in the Dockerfile were absolute or relative. Whether the test suite bound to a fixed port or picked one. If you look at the code, VitaScale is a small environment. If you look at the diff-by-diff history, most of the effort went into making a validator happy about the shape of the output.

I do not think this is a bad thing. Format compliance is how benchmarks become comparable. But it does mean that "how much of this repo is the interesting simulation" and "how much of this repo is the interesting simulation, expressed in a way an OpenEnv validator will accept" are two different questions with two different answers.

What I do not know

I do not know whether VitaScale was accepted by whichever program the OpenEnv submission was targeting, and I do not want to speculate. I have not verified the 0.82 / 0.68 / 0.76 baseline scores in this write-up. I have not tried to justify the choice of 175 req/min per instance or $0.10 per step as anything other than tuning knobs. I have not read architecture.png. I have not compared VitaScale to other autoscaling environments the way a proper related-work section would. If any of that changes, I will come back and edit this post.

Cite as: Saravanan, K. (2026). VitaScale: a tiny autoscaling simulator built to fit an OpenEnv validator. Kaushik Saravanan. https://www.kaushik.cv/blog/vitascale-openenv-autoscaling