diff --git a/demo/Dockerfile b/demo/Dockerfile new file mode 100644 index 0000000..a4cd692 --- /dev/null +++ b/demo/Dockerfile @@ -0,0 +1,8 @@ +FROM golang:1.24 AS build +WORKDIR /src +COPY . . +RUN go build -o /out/inventory ./... + +FROM gcr.io/distroless/static +COPY --from=build /out/inventory /inventory +ENTRYPOINT ["/inventory"] diff --git a/demo/config.yaml b/demo/config.yaml index 8214675..d32667f 100644 --- a/demo/config.yaml +++ b/demo/config.yaml @@ -5,7 +5,8 @@ metadata: spec: driver: gpu.example.com parameters: - sharing: none - memory: 16Gi + sharing: time-sliced + memory: 24Gi + replicas: 4 nodeSelector: accelerator: nvidia diff --git a/demo/go.sum b/demo/go.sum index 489ccc9..c16f218 100644 --- a/demo/go.sum +++ b/demo/go.sum @@ -1,4 +1,5 @@ -example.com/dra v1.3.0 h1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa= -example.com/dra v1.3.0/go.mod h1:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb= +example.com/dra v1.4.0 h1:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee= +example.com/dra v1.4.0/go.mod h1:fffffffffffffffffffffffffffffffffffffffffff= k8s.io/api v0.30.0 h1:ccccccccccccccccccccccccccccccccccccccccccc= k8s.io/api v0.30.0/go.mod h1:ddddddddddddddddddddddddddddddddddddddddddd= +k8s.io/klog/v2 v2.120.1 h1:ggggggggggggggggggggggggggggggggggggggggggg= diff --git a/demo/gpu.go b/demo/gpu.go index 9fcf12a..09acfc1 100644 --- a/demo/gpu.go +++ b/demo/gpu.go @@ -3,14 +3,19 @@ package gpu import ( "context" + "errors" "fmt" ) +// ErrNodeNotFound is returned when a node has no inventory yet. +var ErrNodeNotFound = errors.New("node not found") + // DeviceInfo describes a single GPU exposed to a pod. type DeviceInfo struct { Name string Driver string UUID string + Shared bool } // StatusController reconciles GPU claims against node inventory. @@ -21,16 +26,24 @@ type StatusController struct { func (c *StatusController) getGPUStatus(info DeviceInfo, node string) (string, error) { devices, ok := c.nodes[node] if !ok { - return "", fmt.Errorf("node %q not found", node) + return "", fmt.Errorf("%w: %q", ErrNodeNotFound, node) } for _, d := range devices { if d.UUID == info.UUID { + if d.Shared { + return "shared", nil + } return "allocated", nil } } return "pending", nil } +// Count returns how many devices a node exposes. +func (c *StatusController) Count(node string) int { + return len(c.nodes[node]) +} + func (c *StatusController) Reconcile(ctx context.Context, node string) error { _, err := c.getGPUStatus(DeviceInfo{}, node) return err diff --git a/demo/old_helper.rs b/demo/helper.rs similarity index 74% rename from demo/old_helper.rs rename to demo/helper.rs index 28f3f5e..41240a3 100644 --- a/demo/old_helper.rs +++ b/demo/helper.rs @@ -6,3 +6,7 @@ pub fn format_uuid(raw: &str) -> String { pub fn is_valid(uuid: &str) -> bool { uuid.len() == 36 && uuid.chars().filter(|c| *c == '-').count() == 4 } + +pub fn short(uuid: &str) -> &str { + uuid.split('-').next().unwrap_or(uuid) +} diff --git a/demo/legacy.rb b/demo/legacy.rb deleted file mode 100644 index bf52dda..0000000 --- a/demo/legacy.rb +++ /dev/null @@ -1,10 +0,0 @@ -# Legacy ruby reporter, superseded by the Go controller. -class Reporter - def initialize(nodes) - @nodes = nodes - end - - def report - @nodes.map { |n| "#{n}: ok" }.join("\n") - end -end diff --git a/demo/logo.png b/demo/logo.png new file mode 100644 index 0000000..67042d6 Binary files /dev/null and b/demo/logo.png differ diff --git a/demo/metrics.swift b/demo/metrics.swift new file mode 100644 index 0000000..c39c975 --- /dev/null +++ b/demo/metrics.swift @@ -0,0 +1,16 @@ +import Foundation + +struct GPUMetrics { + let uuid: String + let utilization: Double + let memoryUsed: UInt64 + + var isSaturated: Bool { + utilization > 0.9 + } +} + +func summarize(_ samples: [GPUMetrics]) -> String { + let busy = samples.filter { $0.isSaturated }.count + return "\(busy)/\(samples.count) GPUs saturated" +} diff --git a/demo/queries b/demo/queries new file mode 100644 index 0000000..9fb4963 --- /dev/null +++ b/demo/queries @@ -0,0 +1,5 @@ +SELECT node, COUNT(*) AS gpus, SUM(memory_gb) AS total_memory +FROM inventory +WHERE status = 'ready' +GROUP BY node +ORDER BY total_memory DESC; diff --git a/demo/server.py b/demo/server.py index 2c60903..cc992be 100644 --- a/demo/server.py +++ b/demo/server.py @@ -1,6 +1,6 @@ """A tiny async status server for the DRA controller.""" import asyncio -from dataclasses import dataclass +from dataclasses import dataclass, field @dataclass @@ -8,6 +8,7 @@ class Claim: name: str driver: str ready: bool = False + devices: list[str] = field(default_factory=list) class Server: @@ -18,5 +19,10 @@ async def add(self, claim: Claim) -> None: await asyncio.sleep(0) self.claims.append(claim) + async def ready(self, name: str) -> None: + for c in self.claims: + if c.name == name: + c.ready = True + def pending(self) -> list[Claim]: return [c for c in self.claims if not c.ready] diff --git a/demo/ui.tsx b/demo/ui.tsx new file mode 100644 index 0000000..ab43dc4 --- /dev/null +++ b/demo/ui.tsx @@ -0,0 +1,34 @@ +// Copyright 2026 Daniil Antoshin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { createSignal, For } from "solid-js"; + +interface Device { + uuid: string; + status: "allocated" | "pending" | "shared"; +} + +export function DeviceList(props: { devices: Device[] }) { + const [filter, setFilter] = createSignal(""); + const shown = () => + props.devices.filter((d) => d.uuid.includes(filter())); + return ( +
+ setFilter(e.currentTarget.value)} /> + +
+ ); +}