Skip to content

Right-size actor sandboxes to declared ActorTemplate resource limits - #679

Merged
Benjamin Elder (BenTheElder) merged 9 commits into
agent-substrate:mainfrom
chw120:microvm-rightsizing
Aug 14, 2026
Merged

Right-size actor sandboxes to declared ActorTemplate resource limits#679
Benjamin Elder (BenTheElder) merged 9 commits into
agent-substrate:mainfrom
chw120:microvm-rightsizing

Conversation

@chw120

@chw120 Chenyi Wang (chw120) commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Actors previously ran in sandboxes sized to the whole node; there was no way to declare how much CPU/memory a given actor should get. This adds an explicit, immutable sizing knob on the ActorTemplate and plumbs it into the sandbox's OCI spec for both the gVisor and micro-VM runtimes.

APIActorTemplate.spec.resources (*corev1.ResourceRequirements). The limits size the sandbox and are baked into the immutable spec; the CRD and generated code are regenerated accordingly.

internal/sizing (shared by both runtimes) — new SandboxSize value (FromLimits / VCPUs / ApplyToOCISpec). ApplyToOCISpec writes CPU quota+period and the memory limit onto the OCI spec, and is a no-op when neither dimension is set, so 0 means "unconstrained". VCPUs rounds milliCPU up to whole
vCPUs.

Plumbing — ateapi reads the template limits (actorResourceLimitstmpl.Spec.Resources) and supplies CpuMilli/MemoryBytes over the actor RPCs (ateapi → atelet → ateom); ateom applies them — gVisor via the cgroup leaf (runsc --cpu-num-from-quota provisions the sentry vCPU count), micro-VM via the guest VmConfig. The two fields are carried on the proto messages.

Scheduling — worker capacity is taken from the WorkerPool's per-worker limits and advertised on the Worker; the scheduler only places an actor on a worker whose capacity >= the actor's declared limits. A missing worker or actor
dimension is treated as unconstrained, so placement is never blocked by absent data.

Docs & demos — document the model in api-guide.md; the counter, sandbox, and micro-VM demos declare actor limits, and their WorkerPool comments now describe the real model (worker limits size the worker pod + advertise scheduling capacity; the sandbox itself is sized by ActorTemplate.spec.resources).

  • Tests pass (unit tests for sizing + scheduling; e2e suite in internal/e2e/suites/sizing resumes an actor and asserts, via the probe fixture's /resources endpoint, that the running sandbox observes the declared CPU/memory from the inside)
  • Appropriate changes to documentation are included in the PR

@BenTheElder

Copy link
Copy Markdown
Collaborator

This PR looks like it implements exactly what we proposed, but I think the premise was flawed (my bad!)

I think maybe instead what we should actually do is:

  1. ActorTemplate starts specifying resource limits.
  2. Scheduling considers this to select a worker with >= resource limits.
  3. We supply these to the actor RPCs and pass them through to the sandbox.

In the current state we will wind up heavily pinning to the worker it first ran on but maybe not considering it when scheduling again?

cc Julian Gutierrez Oschmann (@juli4n)

3) should be reusable from the current state, but I think 1/2 might need to replace the downward API approach.

Chenyi Wang (chw120) added a commit to chw120/substrate that referenced this pull request Aug 5, 2026
Implements BenTheElder's 3-step redesign (PR agent-substrate#679), replacing the
downward-API pod-sizing approach:

1. ActorTemplate.Spec.Resources declares per-actor compute limits, baked
   into the immutable spec (so it can seed microVM snapshots).
2. Scheduling gates worker selection on advertised capacity: the syncer
   publishes each worker's ateom-container limits as Worker capacity, and
   the scheduler skips workers whose capacity is below the actor's limits
   (zero constraint or zero capacity = unconstrained, graceful fallback).
3. Limits flow over the actor RPCs (ateapi -> atelet -> ateom) via new
   cpu_milli/memory_bytes fields on Run/Restore/RunWorkload/RestoreWorkload
   requests, applied to the OCI spec by internal/sizing.

internal/sizing loses env Discover(); sizes now come from RPC via
FromLimits(), and its PodSize type is renamed SandboxSize (the size is no
longer derived from the pod). atecontroller no longer injects
resource-fieldRef env.

docs/api-guide.md is updated: the WorkerPool resources section documents
worker capacity (the scheduling envelope), and a new ActorTemplate
spec.resources section documents actor right-sizing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@chw120
Chenyi Wang (chw120) force-pushed the microvm-rightsizing branch 2 times, most recently from 6f01be6 to 58d0da9 Compare August 6, 2026 00:51
@chw120 Chenyi Wang (chw120) changed the title Right-size sandbox to the worker pod's CPU/memory Right-size actor sandboxes to declared ActorTemplate resource limits Aug 6, 2026

@BenTheElder Benjamin Elder (BenTheElder) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick agentic pass on the latest version:

presubmit also surfaces some typos

Comment thread cmd/ateom-microvm/run.go Outdated
vcpus = v
}
if sz.MemoryBytes > 0 {
if m := int(sz.MemoryBytes/(1024*1024)) - s.memReserveMiB; m > 0 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 should-fix 🟡 – When the reserve swallows the whole limit this silently falls back to the kata default, which is larger than what the actor declared. guestConfig defaults to 2048 MiB, so an actor declaring 256Mi — exactly the default reserve — takes the m > 0 branch as false and boots a 2 GiB VM, 8x its declared size. The scheduler already placed it against 256Mi, so a worker sized for that actor is overcommitted by the same factor and the pod OOMs rather than the actor being capped.

The band just above the reserve is the other half: 320Mi leaves 64 MiB of guest RAM, which will not boot a kata guest, and the failure surfaces as a boot hang rather than a sizing error.

Both cases are quiet. Failing the run with a message naming the declared limit and the reserve would make them obvious, and a minimum on spec.resources at admission would catch them before an actor is ever scheduled — there is no validation on that field today.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Ben for reviewing the PR. I updated the code with the detailed error message and the validation check. Please review again.

@chw120
Chenyi Wang (chw120) force-pushed the microvm-rightsizing branch 2 times, most recently from dc1c37c to 69f77e9 Compare August 7, 2026 20:42
Dmitry Berkovich (dberkov) pushed a commit that referenced this pull request Aug 8, 2026
copyFile used a plain `io.Copy`, which reads a hole as zeroes and writes
it back as data. The biggest thing it copies is a guest memory image,
which is mostly unallocated: staging a local checkpoint for a restore
inflated a 164MiB snapshot into its full 2GiB logical size. (Note: That
itself is a bug, see
#679)

That costs disk and I/O on every local checkpoint restore, and it
compounds, because cloud-hypervisor then loads the whole image and the
next checkpoint it writes is dense in turn. Five pause cycles on one
actor left 8.2GB of local checkpoints where a few hundred MiB would do.

Copy only the populated extents, located with `SEEK_DATA` and
`SEEK_HOLE`, and fall back to the dense copy when the filesystem cannot
report holes.

> It's a good idea to open an issue first for discussion.

- [x] Tests pass
- [x] Appropriate changes to documentation are included in the PR
@BenTheElder

Copy link
Copy Markdown
Collaborator

[needs a rebase, a lot changed]

Comment thread demos/counter/counter-microvm.yaml.tmpl Outdated
# sized by the ActorTemplate's spec.resources below, not by these; keep the
# actor memory below this limit so the VMM reserve fits (see internal/sizing).
resources:
requests:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given that we snapshot/restore, I'm not sure we should support requests, vs only limits for now?

unless we play with the balloon driver, uVM has to have max memory allocated up front, and then on restore it must have the same size.

from my past experiments, the balloon can be pretty slow.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SG! limits is only being used in scheduling. Removed requests in demos to avoid the confusion.

Comment thread cmd/ateom-gvisor/runsc.go
// Provision the sentry's vCPU count from the cgroup CPU quota written by
// sizing.ApplyToOCISpec, so the sandbox is sized to the pod's limit (runsc
// otherwise sizes to all host CPUs). Global flag: before the subcommand.
"--cpu-num-from-quota",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 blocking 🔴 – -root was dropped from cmdCreate. This --cpu-num-from-quota line replaced "-root", ateompath.RunSCStateDir(r.actorUID) instead of being added alongside it.

Every other subcommand still passes it — cmdStart (line 161), cmdCheckpoint, cmdFsCheckpoint, cmdRestore, cmdDelete, cmdState — so runsc create now writes container state to runsc's default root while runsc start looks in <actor>/runsc-state. They no longer agree on where state lives, which should break gVisor cold boot at the start step.

cmdRestore at line 267 shows the intended shape: -root kept, the new flag added after it.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Restored it.

Comment thread cmd/ateom-microvm/run.go
if v := sz.VCPUs(); v > 0 {
vcpus = v
}
memMiB, err = resolveGuestMemMiB(sz.MemoryBytes, s.memReserveMiB, memMiB)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 should-fix 🟡 – The guest container's memory cgroup and the VM's RAM get sized from different numbers. buildActorContainers ran above with p.size, so ApplyToOCISpec already set the container's Memory.Limit to the full declared limit, while this line gives the VM that limit minus the VMM reserve. A 1Gi actor ends up with a 1024Mi cgroup cap inside a VM holding 768Mi of RAM.

The cap can then never bind. The workload exhausts guest RAM before it reaches the cgroup limit, so instead of a clean OOM kill you get the guest kernel's OOM killer or a hang — the failure mode minGuestMemMiB exists to prevent. Sizing the container cgroup to the post-reserve guest RAM would make the limit meaningful.

restoreFullScope calls buildActorContainers with p.size too, so the same gap applies to a FULL-scope restore.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated code to size guest container memory cgroup to post-reserve guest RAM.

// for gVisor, the kata config for the micro-VM).
//
// +optional
Resources *corev1.ResourceRequirements `json:"resources,omitempty"`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Human edit: upgrading to should-fix 🟡


🤖 question 🟢 – The field takes a full corev1.ResourceRequirements, but only Limits is ever read (actorResourceLimits in workflow_resume.go). Nothing rejects requests, so a user can set them, watch them persist into the spec, and get no effect. Claims is accepted on the same terms. The doc comment says requests aren't consulted, which only helps the people who read it.

If your requests-vs-limits question on the demo template lands on "limits only for now", a CEL rule rejecting resources.requests would turn a silent no-op into an error at create time. Worth settling either way, since this is in the immutable spec and can't be corrected on an existing template.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more validations to reject it.


// The object storage URI of the ActorTemplate's golden snapshot.
// Set only when scope is SNAPSHOT_SCOPE_DATA_ON_GOLDEN. Mirrors the
// snapshot_uri contract (field 8).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit 🟢 – A line went missing here. This previously read "Set only when scope is SNAPSHOT_SCOPE_DATA_ON_GOLDEN. Mirrors the snapshot_uri contract (field 8)." and now reads "...golden snapshot." followed by a dangling "snapshot_uri contract (field 8)." The deletion looks incidental to this PR, and it drops the only statement of when the field is set.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored.

Chenyi Wang (chw120) added a commit to chw120/substrate that referenced this pull request Aug 12, 2026
- gvisor: restore missing -root flag in runsc cmdCreate
- microvm: size guest container memory cgroup to post-reserve guest RAM
- actortemplate: reject requests and claims via CEL validation rules
- demos: simplify WorkerPool template resources to limits only
- proto: restore missing comment line on golden_snapshot_uri in ateom.proto
Chenyi Wang (chw120) added a commit to chw120/substrate that referenced this pull request Aug 12, 2026
- gvisor: restore missing -root flag in runsc cmdCreate
- microvm: size guest container memory cgroup to post-reserve guest RAM
- actortemplate: reject requests and claims via CEL validation rules
- demos: simplify WorkerPool template resources to limits only
- proto: restore missing comment line on golden_snapshot_uri in ateom.proto

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly resolved now, two more agent findings. thanks for continuing to work on this

Comment thread cmd/ateom-microvm/run.go Outdated
}
memMiB, err := resolveGuestMemMiB(sz.MemoryBytes, s.memReserveMiB, 0)
if err != nil {
return sz

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 should-fix 🟡 – This swallows the error and returns the size unreduced, which is the mismatch the fix was for. When resolveGuestMemMiB fails, sz.MemoryBytes is still the outer declared limit, so the guest container cgroup goes back to being larger than the VM's RAM.

Cold boot is protected because it checks resolveGuestMemMiB's error itself and aborts. restoreFullScope doesn't — the VM size comes from the snapshot — so the reachable case is raising --vmm-mem-reserve-mib after an actor was snapshotted: the next restore quietly gets the bad pairing instead of a clear failure. TestGuestSize covers the reduce and unset paths but not this one.

Returning the error, or at least logging, would keep it from failing silently.

Related: coldBootActor at line 403 does the same translation inline (guestSize := sz; guestSize.MemoryBytes = memMiB * MiB) rather than calling this helper. Two implementations that agree on the happy path and disagree on this one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated line 403 to call guestSize().

Comment thread cmd/ateom-microvm/run.go Outdated
// limits: CPU passes through unchanged (kata-agent sets the CFS quota), while
// memory is reduced by the VMM reserve so the container cgroup limit inside the
// guest matches the guest VM's actual RAM rather than the (larger) outer limit.
func (s *AteomService) guestSize(sz sizing.SandboxSize) sizing.SandboxSize {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 question 🟢 – With this change the container cgroup limit lands at exactly the guest's total RAM, so it still can't bind before the guest kernel OOMs — the kernel, agent and init need some of that RAM, so the workload hits the guest OOM killer just short of its cgroup limit. Closer than before, but the clean per-container OOM kill is still out of reach.

It also applies uniformly: every container in a multi-container actor gets a limit equal to the whole guest RAM, so they aren't bounded relative to each other. That may well be the intent — the VM's RAM is the real envelope and the per-container cgroup is belt-and-braces — but if so it's worth saying in the guestSize comment, since "right-sized to the actor's limits" reads as if the cgroup is doing the enforcing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more comments to clarify it. If it's better to subtract the guest-side overhead, I can make changes in this or following PR.

Comment thread pkg/proto/ateapipb/ateapi.proto Outdated
// only places an actor on a worker whose capacity is >= the actor's declared
// resource limits. Zero means "unknown/unset": treated as unconstrained so
// placement is not blocked (matching the pre-capacity behavior).
int64 cpu_milli_capacity = 12; // CPU capacity in millicores.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe wrap these two fields in a message for better logical grouping?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created message WorkerCapacity.

Chenyi Wang (chw120) added a commit to chw120/substrate that referenced this pull request Aug 12, 2026
microvm: guestSize returns an error instead of the unreduced size when
resolveGuestMemMiB fails. restoreFullScope now checks it, so raising
--vmm-mem-reserve-mib after an actor was snapshotted fails loudly rather
than pairing the guest with a container cgroup limit larger than the VM's
RAM. coldBootActor calls the helper instead of reimplementing the same
translation inline, and TestGuestSize covers the error path.

Document in guestSize why the per-container cgroup limit is the whole
guest RAM: the VM's RAM is the real envelope and the cgroup is
belt-and-braces, so the guest OOM killer fires just before the cgroup
limit binds and multi-container actors are not bounded relative to each
other.

ateapi: group the worker capacity fields into a WorkerCapacity message
(cpu_milli, memory_bytes), matching the field naming used for limits in
ateom.proto and atelet.proto.
Chenyi Wang (chw120) added a commit to chw120/substrate that referenced this pull request Aug 12, 2026
- gvisor: restore missing -root flag in runsc cmdCreate
- microvm: size guest container memory cgroup to post-reserve guest RAM
- actortemplate: reject requests and claims via CEL validation rules
- demos: simplify WorkerPool template resources to limits only
- proto: restore missing comment line on golden_snapshot_uri in ateom.proto
Chenyi Wang (chw120) added a commit to chw120/substrate that referenced this pull request Aug 12, 2026
microvm: guestSize returns an error instead of the unreduced size when
resolveGuestMemMiB fails. restoreFullScope now checks it, so raising
--vmm-mem-reserve-mib after an actor was snapshotted fails loudly rather
than pairing the guest with a container cgroup limit larger than the VM's
RAM. coldBootActor calls the helper instead of reimplementing the same
translation inline, and TestGuestSize covers the error path.

Document in guestSize why the per-container cgroup limit is the whole
guest RAM: the VM's RAM is the real envelope and the cgroup is
belt-and-braces, so the guest OOM killer fires just before the cgroup
limit binds and multi-container actors are not bounded relative to each
other.

ateapi: group the worker capacity fields into a WorkerCapacity message
(cpu_milli, memory_bytes), matching the field naming used for limits in
ateom.proto and atelet.proto.
Comment thread cmd/ateom-microvm/run.go
// An error means the declared limit cannot be honored (see resolveGuestMemMiB);
// callers must not fall back to the unreduced size, which is the mismatch this
// translation exists to avoid.
func (s *AteomService) guestSize(sz sizing.SandboxSize) (sizing.SandboxSize, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 question 🟢 – Memory gets a VMM reserve, a guest floor and an admission check; CPU gets none of that, so the same declared number means different things on the two runtimes.

On gVisor the sentry is the workload and sits in the same cgroup leaf, so a 2000m limit covers the sandbox too. On the micro-VM the guest cgroup is capped at 2000m, but cloud-hypervisor's vCPU threads, virtiofsd and ateom are host processes sharing the worker pod's CPU quota alongside it — the same reason memory needs vmmMemReserveMiB. The scheduler compares the declared limit against worker capacity with >=, so an actor may be placed with the whole worker's CPU and leave nothing for the VMM.

CPU is compressible, so this throttles rather than fails, which is presumably why there is no reserve. Worth saying so in this comment though — "passes through unchanged" reads as "the workload gets what it asked for", and on this runtime it gets somewhat less.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[we should defer this to follow-up but I think it probably needs considering]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the comment.

@BenTheElder

Copy link
Copy Markdown
Collaborator

this PR might be affecting e2e?

Chenyi Wang (chw120) added a commit to chw120/substrate that referenced this pull request Aug 13, 2026
- gvisor: restore missing -root flag in runsc cmdCreate
- microvm: size guest container memory cgroup to post-reserve guest RAM
- actortemplate: reject requests and claims via CEL validation rules
- demos: simplify WorkerPool template resources to limits only
- proto: restore missing comment line on golden_snapshot_uri in ateom.proto
Chenyi Wang (chw120) added a commit to chw120/substrate that referenced this pull request Aug 13, 2026
microvm: guestSize returns an error instead of the unreduced size when
resolveGuestMemMiB fails. restoreFullScope now checks it, so raising
--vmm-mem-reserve-mib after an actor was snapshotted fails loudly rather
than pairing the guest with a container cgroup limit larger than the VM's
RAM. coldBootActor calls the helper instead of reimplementing the same
translation inline, and TestGuestSize covers the error path.

Document in guestSize why the per-container cgroup limit is the whole
guest RAM: the VM's RAM is the real envelope and the cgroup is
belt-and-braces, so the guest OOM killer fires just before the cgroup
limit binds and multi-container actors are not bounded relative to each
other.

ateapi: group the worker capacity fields into a WorkerCapacity message
(cpu_milli, memory_bytes), matching the field naming used for limits in
ateom.proto and atelet.proto.
@chw120

Copy link
Copy Markdown
Collaborator Author

FYI, the CI failure is due to known flaky tests (tracked in #675 and #619) and is unrelated to the changes in this PR.

@chw120

Copy link
Copy Markdown
Collaborator Author

FYI, the CI failure is due to known flaky tests (tracked in #675 and #619) and is unrelated to the changes in this PR.

Benjamin Elder (@BenTheElder)

Actors previously ran in sandboxes sized to the whole node; there was no way to
declare how much CPU/memory a given actor should get. This adds an explicit,
immutable sizing knob on the ActorTemplate and plumbs it all the way into the
sandbox's OCI spec, for both the gVisor and micro-VM runtimes.

- API: ActorTemplate.spec.resources (*corev1.ResourceRequirements). The Limits
  size the sandbox and are baked into the immutable spec; the CRD and generated
  code are regenerated accordingly.
- internal/sizing: new SandboxSize value (FromLimits / VCPUs / ApplyToOCISpec).
  ApplyToOCISpec writes CPU quota+period and the memory limit onto the OCI spec
  and is a no-op when neither dimension is set, so 0 means "unconstrained".
- Plumbing: ateapi reads the template limits (actorResourceLimits →
  tmpl.Spec.Resources), supplies CpuMilli/MemoryBytes over the actor RPCs
  (ateapi → atelet → ateom), and ateom applies them — gVisor via the cgroup leaf
  (runsc --cpu-num-from-quota provisions the sentry vCPU count), micro-VM via the
  guest spec. Proto messages carry the two fields.
- Scheduling: worker capacity is taken from the WorkerPool's per-worker limits
  and the scheduler only places an actor on a worker whose capacity >= the
  actor's declared limits; a missing worker or actor dimension is treated as
  unconstrained so placement is never blocked by absent data.
- Tests: unit tests for sizing and scheduling, plus an e2e suite
  (internal/e2e/suites/sizing) that resumes an actor and asserts, via the probe
  fixture's new /resources endpoint, that the running sandbox observes the
  declared CPU/memory from the inside.
- Docs & demos: document the model in api-guide.md; the counter, sandbox, and
  micro-VM demos declare actor limits and their WorkerPool comments now describe
  the real model (worker limits size the worker pod + advertise capacity; the
  sandbox is sized by ActorTemplate.spec.resources).
Fail cold boot with a clear error (naming the declared limit, the reserve,
and the guest minimum) instead of silently falling back to the larger kata
default when the VMM reserve leaves too little guest RAM to boot. Add a
matching admission floor on ActorTemplate.spec.resources.limits.memory
(>= VMM reserve + guest minimum) for sandboxClass microvm so the case is
caught at create time, before scheduling.
- gvisor: restore missing -root flag in runsc cmdCreate
- microvm: size guest container memory cgroup to post-reserve guest RAM
- actortemplate: reject requests and claims via CEL validation rules
- demos: simplify WorkerPool template resources to limits only
- proto: restore missing comment line on golden_snapshot_uri in ateom.proto
microvm: guestSize returns an error instead of the unreduced size when
resolveGuestMemMiB fails. restoreFullScope now checks it, so raising
--vmm-mem-reserve-mib after an actor was snapshotted fails loudly rather
than pairing the guest with a container cgroup limit larger than the VM's
RAM. coldBootActor calls the helper instead of reimplementing the same
translation inline, and TestGuestSize covers the error path.

Document in guestSize why the per-container cgroup limit is the whole
guest RAM: the VM's RAM is the real envelope and the cgroup is
belt-and-braces, so the guest OOM killer fires just before the cgroup
limit binds and multi-container actors are not bounded relative to each
other.

ateapi: group the worker capacity fields into a WorkerCapacity message
(cpu_milli, memory_bytes), matching the field naming used for limits in
ateom.proto and atelet.proto.
…lable

guestSize's "CPU passes through unchanged" was about the number, not about what
the workload gets. The declared limit reaches the guest cgroup intact, but
cloud-hypervisor's vCPU threads, virtiofsd and ateom are host processes drawing
on the same worker-pod CPU quota, and the scheduler's capacity check (>=) sets
none of it aside, so the host throttles the guest before its own quota ever
binds. Say so, along with why the asymmetry with memory is deliberate: an
unreduced memory limit takes the pod past its own limit and gets it OOM-killed,
whereas CPU is compressible and the shortfall only costs throughput. A CPU
reserve is left for a follow-up.

The demo worker pools declared limits and no requests, so Kubernetes copied the
limits into the requests: the counter pool asked for 5 CPUs and the micro-VM
pool for 4 more, against the single 4-vCPU kind node the e2e job runs on, which
leaves worker pods Pending. Capacity is advertised from the limits, so a request
below the limit changes nothing an actor can see: give each pool a CPU request
well under its limit, and leave the memory request at the limit because that
memory is really occupied.
The sized probe fixture set spec.pauseImage on its ActorTemplate, but agent-substrate#848
moved that field to SandboxConfig. The CRD decodes strictly, so `ko apply`
failed with

  strict decoding error: unknown field "spec.pauseImage"

and the sizing suite died in deploySizedProbe before it ever asserted
anything. The pause image comes from the SandboxConfig the WorkerPool
resolves, exactly as it does for the plain probe fixture next door, so the
field is simply dropped rather than moved.

While here, give the sized probe the same readyz gate the plain probe has:
the suite makes a single un-retried GET /resources after resuming, so the
golden snapshot must be taken with the listener already up.
@BenTheElder
Benjamin Elder (BenTheElder) merged commit 71b0063 into agent-substrate:main Aug 14, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants