mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 19:01:42 +00:00
98fc01764b
* feat(ecs): thread resourceRequirements GPU count through conv, ContainerDef, AssignContainer and RunSpec * feat(ecs): add GPU placement dimension — InstanceRecord capacity, fits/reserve/release, STRINGSET resource reporting * feat(ecs): agent GPU discovery via nvidia-smi, pinning ledger, device-UUID report-back and DescribeTasks gpuIds * feat(ecs): inject assigned GPU devices into containerd OCI spec via CDI (oci.WithCDIDevices) * test(ecs): add GPU task e2e (nvidia-smi container, gpuIds assertion) to gpu e2e suite * test(gpu): decouple base-AMI requirement from GPU fixture so ECS e2e runs without the ubuntu EC2 image * test(gpu): pick smallest advertised GPU instance type to fit a single host * fix(ecs): preserve pinned GPU UUIDs across STOPPED state change so DescribeTasks reports gpuIds for one-shot tasks
156 lines
4.0 KiB
Go
156 lines
4.0 KiB
Go
package main
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
func TestGPULedger_PinReturnsDistinctUUIDs(t *testing.T) {
|
|
l := newGPULedger([]string{"GPU-a", "GPU-b", "GPU-c"})
|
|
got, err := l.Pin("t-1/web", 2)
|
|
if err != nil {
|
|
t.Fatalf("Pin: %v", err)
|
|
}
|
|
if len(got) != 2 {
|
|
t.Fatalf("want 2 UUIDs, got %v", got)
|
|
}
|
|
if got[0] == got[1] {
|
|
t.Errorf("pinned UUIDs must be distinct, got %v", got)
|
|
}
|
|
if len(l.free) != 1 {
|
|
t.Errorf("free = %d, want 1 remaining", len(l.free))
|
|
}
|
|
}
|
|
|
|
func TestGPULedger_PinInsufficientFreeErrors(t *testing.T) {
|
|
l := newGPULedger([]string{"GPU-a"})
|
|
_, err := l.Pin("t-1/web", 2)
|
|
if err == nil {
|
|
t.Fatal("want error when requesting more GPUs than free")
|
|
}
|
|
if len(l.free) != 1 {
|
|
t.Errorf("failed pin must not consume any device, free = %d", len(l.free))
|
|
}
|
|
}
|
|
|
|
func TestGPULedger_PinZeroIsNoop(t *testing.T) {
|
|
l := newGPULedger([]string{"GPU-a"})
|
|
got, err := l.Pin("t-1/web", 0)
|
|
if err != nil || got != nil {
|
|
t.Errorf("Pin(0) = %v, %v; want nil, nil", got, err)
|
|
}
|
|
}
|
|
|
|
func TestGPULedger_ReleaseTaskFreesOnlyThatTasksDevices(t *testing.T) {
|
|
l := newGPULedger([]string{"GPU-a", "GPU-b", "GPU-c", "GPU-d"})
|
|
if _, err := l.Pin(gpuKey("t-1", "web"), 2); err != nil {
|
|
t.Fatalf("pin t-1: %v", err)
|
|
}
|
|
if _, err := l.Pin(gpuKey("t-2", "worker"), 1); err != nil {
|
|
t.Fatalf("pin t-2: %v", err)
|
|
}
|
|
if len(l.free) != 1 {
|
|
t.Fatalf("free = %d, want 1 before release", len(l.free))
|
|
}
|
|
|
|
l.ReleaseTask("t-1")
|
|
if len(l.free) != 3 {
|
|
t.Errorf("free = %d, want 3 after releasing t-1's two devices", len(l.free))
|
|
}
|
|
if _, ok := l.pinned[gpuKey("t-2", "worker")]; !ok {
|
|
t.Error("t-2's pin should survive releasing t-1")
|
|
}
|
|
if _, ok := l.pinned[gpuKey("t-1", "web")]; ok {
|
|
t.Error("t-1's pin should be gone after release")
|
|
}
|
|
}
|
|
|
|
// Multi-device pinning across concurrent task assigns must hand out disjoint
|
|
// UUID sets — no two tasks may ever share a device.
|
|
func TestGPULedger_ConcurrentPinsHandOutDistinctUUIDs(t *testing.T) {
|
|
const nDevices = 20
|
|
uuids := make([]string, nDevices)
|
|
for i := range uuids {
|
|
uuids[i] = "GPU-" + string(rune('a'+i))
|
|
}
|
|
l := newGPULedger(uuids)
|
|
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
seen := make(map[string]bool)
|
|
errs := 0
|
|
for i := range nDevices {
|
|
wg.Add(1)
|
|
go func(n int) {
|
|
defer wg.Done()
|
|
key := gpuKey("t-concurrent", string(rune('A'+n)))
|
|
got, err := l.Pin(key, 1)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if err != nil {
|
|
errs++
|
|
return
|
|
}
|
|
for _, u := range got {
|
|
if seen[u] {
|
|
t.Errorf("UUID %s handed out to more than one concurrent pin", u)
|
|
}
|
|
seen[u] = true
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
if errs != 0 {
|
|
t.Errorf("want all %d pins to succeed (exactly enough devices), got %d errors", nDevices, errs)
|
|
}
|
|
if len(seen) != nDevices {
|
|
t.Errorf("distinct UUIDs handed out = %d, want %d", len(seen), nDevices)
|
|
}
|
|
if len(l.free) != 0 {
|
|
t.Errorf("free = %d, want 0 after all devices pinned", len(l.free))
|
|
}
|
|
}
|
|
|
|
// Releasing a task's devices concurrently with new pins never double-hands a
|
|
// UUID: a device is only ever free or pinned to exactly one key at a time.
|
|
func TestGPULedger_ConcurrentPinAndReleaseNoDoubleAssignment(t *testing.T) {
|
|
l := newGPULedger([]string{"GPU-a", "GPU-b"})
|
|
var wg sync.WaitGroup
|
|
for round := range 50 {
|
|
wg.Add(2)
|
|
taskID := "t-" + string(rune('a'+round%26))
|
|
go func(id string) {
|
|
defer wg.Done()
|
|
_, _ = l.Pin(gpuKey(id, "web"), 1)
|
|
}(taskID)
|
|
go func(id string) {
|
|
defer wg.Done()
|
|
l.ReleaseTask(id)
|
|
}(taskID)
|
|
}
|
|
wg.Wait()
|
|
// Invariant: every currently-pinned UUID appears in exactly one pinned entry
|
|
// and never simultaneously in free.
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
freeSet := make(map[string]bool, len(l.free))
|
|
for _, u := range l.free {
|
|
if freeSet[u] {
|
|
t.Errorf("duplicate UUID %s in free list", u)
|
|
}
|
|
freeSet[u] = true
|
|
}
|
|
pinnedSet := make(map[string]bool)
|
|
for _, uuids := range l.pinned {
|
|
for _, u := range uuids {
|
|
if pinnedSet[u] {
|
|
t.Errorf("UUID %s pinned under more than one key", u)
|
|
}
|
|
if freeSet[u] {
|
|
t.Errorf("UUID %s is both free and pinned", u)
|
|
}
|
|
pinnedSet[u] = true
|
|
}
|
|
}
|
|
}
|