Skip to content

Launch parity with Go spawn: arch, GPU AMI, hibernation, Spot stop, ClientToken (#129) - #135

Merged
scttfrdmn merged 1 commit into
mainfrom
fix/129-launch-parity
Nov 11, 2026
Merged

scttfrdmn merged 1 commit into
mainfrom
fix/129-launch-parity

Conversation

@scttfrdmn

Copy link
Copy Markdown
Contributor

Fixes #129

Stacked on #134 (#125), which is stacked on #133 (#128). This PR targets main so CI runs, so its diff includes those two commits until they merge. Review only the top commit, Launch parity with Go spawn…. Merge in order: #133, #134, this one.

It is stacked because the stop/hibernate gating and the NotRunning/OneTimeSpot/NotHibernatable errors extend #133's per-id card actions and #134's SpawnError.

Every choice below cites the Go source. They are pinned by Go parity vectors rendered from the published spawn@v0.150.0: scripts/gen-go-fixtures.sh now also writes src/aws/testdata/launch-parity.go.json. Regenerating the existing fixtures at 0.150.0 left them byte-identical.

1. Architecture: trn/inf are x86_64

  • The architecture comes from EC2 DescribeInstanceTypes, preferring arm64 when a type advertises both, as Go's resolveArchitecture does (pkg/aws/ami.go).
  • When that call fails, it falls back to archForInstanceType (Go: DetectArchitecture).
  • The fallback no longer calls trn*/inf* arm64. It agrees with Go's static list on every family that list names (vector test). It also recognises Graviton families Go's list lacks (im4gn, is4gen, i4g, x8g, a1); Go gets those right only through EC2.
  • As in Go, Neuron types get the AL2023 CPU image. Go says "a Neuron workload should pass an explicit --ami".

2. GPU types get an NVIDIA-driver image

  • isNvidiaGpuInstance is Go's exact DetectGPUInstance family set.
  • Those types resolve Go's SSM parameter /aws/service/deeplearning/ami/{x86_64|arm64}/base-oss-nvidia-driver-gpu-amazon-linux-2023/latest/ami-id. That is the DLAMI "Base OSS Nvidia Driver GPU AMI (AL2023)", as in Go's GetAL2023AMI.
  • g5g gets the arm64 image.
  • If the lookup fails, the launch is refused with AWS's code kept on the error, never a fallback to the CPU image. An explicit ami is used as given.
  • Mirrors a Go gap: the vectors show Go's set misses p6-b200, p5en (and by the same exact-match rule p6e-gb200, gr6, g6f). Filed as DetectGPUInstance misses p6-b200, p6e-gb200, p5en, gr6 and g6f: auto-AMI gives them the CPU image (no NVIDIA driver) spawn#1579. spawn-ts matches Go on purpose and will follow when Go's set is fixed and the vectors are regenerated.

3. Hibernation only where it can succeed

  • Hibernation is requested when hibernateOnIdle or onComplete: "hibernate" is set (Go HibernationRequested).
  • It is refused before RunInstances unless DescribeInstanceTypes says HibernationSupported. It is also refused if that call fails, as Go's preflight does (cmd/launch_preflight.go:233).
  • Otherwise the launch sends HibernationOptions: {Configured: true} plus BlockDeviceMappings with an encrypted gp3 root:
    • on the AMI's RootDeviceName, from DescribeImages ImageIds (Go launchInfoFromAMI, #284);
    • sized RAM rounded up to GiB + 10 (Go hibernationRootGiB);
    • never below the AMI's snapshot (Go buildBlockDevices).
  • ManagedInstance.hibernationConfigured is read from HibernationOptions.Configured.
  • Spot plus hibernateOnIdle is refused like the other stop-based Spot lifecycles. spawn-ts makes only one-time requests, where Go would make it persistent (NeedsStoppable).

4. Stop/hibernate not offered or allowed where EC2 refuses

stopBlocker(inst, action) holds Go's rules (cmd/state.go):

  • not running → NotRunning;
  • one-time Spot (spotrequest.IsOneTime: tag ≠ persistent) → OneTimeSpot, in Go's oneTimeSpotStopError wording;
  • hibernate where hibernationConfigured === false → NotHibernatable.

client.stop and client.hibernate throw these as SpawnError before any call. The terminal's stop and hibernate refuse the same way, and the Dashboard doesn't render those buttons.

5. ClientToken (spore-host#673)

  • RunInstances.ClientToken is LaunchInput.clientToken when given, else a fresh spawn-ts-<uuid> (Go: "spawn-" + rand.Text(), zone_fallback.go:326). Invalid tokens are refused before any AWS call.
  • SpawnClient.launch shares one in-flight request per token, so a double-click is one RunInstances call.
  • newLaunchToken() mints a token. isAmbiguousLaunchError(err) says whether a retry should keep it: keep it after a network error or 5xx with no instance id; rotate it after a definite 4xx, or after an instance was created and then terminated.
  • The Dashboard form keeps one token per intent, resets it on edit or success, and disables Launch while one runs.

New IAM the portal role will need

These are also new SDK commands in the bundle, so the portal's closed iam/command-actions.ts gate will flag them on the bump. That is the intended outcome.

  • ec2:DescribeInstanceTypes, Resource *: every launch. A denial is tolerated for architecture but refuses hibernating launches.
  • ssm:GetParameter on arn:aws:ssm:*::parameter/aws/service/deeplearning/ami/*: GPU launches only.

Tests

  • src/aws/launch-parity.test.ts:
    • the Go vectors for architecture, GPU set, SSM parameters, root sizing, HibernationRequested, NeedsStoppable and IsOneTime;
    • fake-EC2+SSM launches: trn/inf → x86_64 image (from EC2, and on fallback); EC2 arch beats the table;
    • g6 and p5 → DLAMI x86 parameter, g5g → arm64; a GPU SSM denial refuses with no RunInstances;
    • hibernation mappings (RAM+10, snapshot floor, root device name), refusal on an unsupported type or a failed lookup, nothing sent when not hibernating, Spot+hibernate refused, HibernationOptions read back;
    • ClientToken pass-through, a fresh token per launch, invalid tokens refused before any call.
  • src/core/launch-intent.test.ts:
    • double-click → 1 provider launch; a settled retry → the same instance; distinct intents → distinct instances;
    • ambiguous versus definite failures;
    • one-time Spot stop/hibernate refused with no call; NotHibernatable; NotRunning in Go's words; the terminal stop refuses one-time Spot.
  • dashboard.test.ts: a one-time Spot card has no stop/hibernate; a non-hibernatable card has no hibernate; a double submit gives one instance; the token is kept or rotated as above.

Breaking for existing consumers

Noted in the CHANGELOG: hibernate isn't offered or allowed on non-hibernatable instances, and stop/hibernate refuse one-time Spot and non-running instances. EC2 rejected all of these anyway.

What's left on #129 (needs a live check, so not claimed here)

  • Whether the DLAMI image actually boots spored and the GPU shows in nvidia-smi has not been verified on a real GPU host.
  • Hibernation hasn't been exercised on a real instance.
  • The CPU image is still found with DescribeImages (AL2023, kernel 6.1), not Go's SSM al2023-ami-kernel-default parameter. This is pre-existing and kept so CPU launches need no SSM grant.

…lientToken (#129)

- Architecture from EC2 DescribeInstanceTypes (Go resolveArchitecture),
  falling back to a static table that agrees with Go's DetectArchitecture;
  trn/inf are x86_64, not arm64.
- NVIDIA GPU families (Go's DetectGPUInstance set) get the DLAMI NVIDIA
  driver image via Go's SSM parameter; a failed lookup refuses the launch.
- hibernateOnIdle / on-complete hibernate set HibernationOptions and an
  encrypted gp3 root on the AMI's root device sized RAM+10 GiB, refused
  up front on types EC2 says can't hibernate (Go preflight).
- stop/hibernate refuse one-time Spot, non-running, and (hibernate) not
  hibernation-configured instances; the Dashboard hides those buttons.
- RunInstances carries a ClientToken: LaunchInput.clientToken or a fresh
  one; one in-flight request per token; the Dashboard keeps one token per
  launch intent (spore-host#673).
- Go parity vectors rendered from spawn v0.150.0 by gen-go-fixtures.sh.

Fixes #129
@scttfrdmn
scttfrdmn force-pushed the fix/129-launch-parity branch from 2ea7b5e to edca348 Compare November 11, 2026 05:03
@scttfrdmn
scttfrdmn merged commit 71cd109 into main Nov 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant