Skip to content

Commit d6bf46a

Browse files
Andrey Cheptsovclaude
andcommitted
Show host GPU driver on fleet instances
Currently, the only way to know which GPU driver a fleet instance runs is to provision it and run a task there. Now shim detects the host GPU driver version on start (`nvidia-smi` for NVIDIA, `amd-smi` for AMD, `/sys/module/tenstorrent/version` for Tenstorrent) and reports it via the new `GET /api/instance/info` endpoint. The server stores it in `JobProvisioningData`, so that no migration is needed, and exposes it as `Instance.gpu_driver` and as a DRIVER column in `dstack fleet -v`. Notes: - Instance info reports facts observed by shim, as opposed to `/api/components`, which reports software managed by shim. - GPU hosts are asked on every instance check, as the facts change when shim restarts, e.g., after a driver upgrade, which the server does not necessarily observe. The provisioning data is only updated if the driver changed. - SSH fleets are also asked at deploy time, so that the driver is known as soon as the instance is ready. - The driver stays unknown on hosts without GPUs, if detection fails, and on backends where shim does not run (runpod, vastai, kubernetes). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent ccef71f commit d6bf46a

27 files changed

Lines changed: 1001 additions & 43 deletions

File tree

runner/docs/shim.openapi.yaml

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ openapi: 3.1.2
22

33
info:
44
title: dstack-shim API
5-
version: v2/0.20.1
5+
version: v2/0.20.30
66
x-logo:
77
url: https://avatars.githubusercontent.com/u/54146142?s=260
88
description: >
@@ -86,6 +86,22 @@ paths:
8686
schema:
8787
$ref: "#/components/schemas/InstanceHealthResponse"
8888

89+
/instance/info:
90+
get:
91+
summary: Get instance info
92+
description: >
93+
(since [0.20.30](https://github.com/dstackai/dstack/releases/tag/0.20.30))
94+
Returns facts about the host observed by shim, e.g., the GPU driver version.
95+
Unlike `/components`, the reported entities are not managed by shim.
96+
tags: [Instance]
97+
responses:
98+
"200":
99+
description: ""
100+
content:
101+
application/json:
102+
schema:
103+
$ref: "#/components/schemas/InstanceInfoResponse"
104+
89105
/components:
90106
get:
91107
summary: Get components
@@ -504,6 +520,24 @@ components:
504520
$ref: "#/components/schemas/DCGMHealth"
505521
additionalProperties: false
506522

523+
InstanceInfoResponse:
524+
title: shim.api.InstanceInfoResponse
525+
type: object
526+
properties:
527+
gpu_vendor:
528+
description: Host GPU vendor. Omitted on hosts without GPUs.
529+
type: string
530+
examples:
531+
- nvidia
532+
gpu_driver_version:
533+
description: >
534+
Host GPU driver version. Omitted on hosts without GPUs
535+
or if detection failed.
536+
type: string
537+
examples:
538+
- 570.86.15
539+
additionalProperties: false
540+
507541
ComponentListResponse:
508542
title: shim.api.ComponentListResponse
509543
type: object

runner/internal/shim/api/api_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import (
55
"sync"
66

77
"github.com/dstackai/dstack/runner/internal/shim"
8+
"github.com/dstackai/dstack/runner/internal/shim/host"
89
)
910

1011
type DummyRunner struct {
1112
tasks map[string]bool
13+
gpus []host.GpuInfo
1214
mu sync.Mutex
1315
}
1416

@@ -46,6 +48,10 @@ func (ds *DummyRunner) Resources(context.Context) shim.Resources {
4648
return shim.Resources{}
4749
}
4850

51+
func (ds *DummyRunner) Gpus(context.Context) []host.GpuInfo {
52+
return ds.gpus
53+
}
54+
4955
func NewDummyRunner() *DummyRunner {
5056
return &DummyRunner{
5157
tasks: map[string]bool{},

runner/internal/shim/api/handlers.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ func (s *ShimServer) InstanceHealthHandler(w http.ResponseWriter, r *http.Reques
5151
return &response, nil
5252
}
5353

54+
func (s *ShimServer) InstanceInfoHandler(w http.ResponseWriter, r *http.Request) (interface{}, error) {
55+
response := InstanceInfoResponse{}
56+
// GPUs are detected once on startup, so this is not an expensive call.
57+
// The driver is a host-wide property, hence any GPU can be used as the source.
58+
if gpus := s.runner.Gpus(r.Context()); len(gpus) > 0 {
59+
response.GpuVendor = string(gpus[0].Vendor)
60+
response.GpuDriverVersion = gpus[0].DriverVersion
61+
}
62+
63+
return &response, nil
64+
}
65+
5466
func (s *ShimServer) TaskListHandler(w http.ResponseWriter, r *http.Request) (interface{}, error) {
5567
tasks := s.runner.TaskList()
5668
return &TaskListResponse{tasks}, nil

runner/internal/shim/api/handlers_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"testing"
88

99
commonapi "github.com/dstackai/dstack/runner/internal/common/api"
10+
"github.com/dstackai/dstack/runner/internal/common/gpu"
11+
"github.com/dstackai/dstack/runner/internal/shim/host"
1012
)
1113

1214
func TestHealthcheck(t *testing.T) {
@@ -29,6 +31,50 @@ func TestHealthcheck(t *testing.T) {
2931
}
3032
}
3133

34+
// TestInstanceInfo goes through the router to also cover the endpoint registration
35+
func TestInstanceInfo(t *testing.T) {
36+
request := httptest.NewRequest("GET", "/api/instance/info", nil)
37+
responseRecorder := httptest.NewRecorder()
38+
39+
runner := NewDummyRunner()
40+
runner.gpus = []host.GpuInfo{
41+
{Vendor: gpu.GpuVendorNvidia, Name: "T4", Vram: 16384, DriverVersion: "570.86.15"},
42+
}
43+
server := NewShimServer(context.Background(), ":12346", "0.0.1.dev2", runner, nil, nil, nil, nil)
44+
45+
server.httpServer.Handler.ServeHTTP(responseRecorder, request)
46+
47+
if responseRecorder.Code != 200 {
48+
t.Errorf("Want status '%d', got '%d'", 200, responseRecorder.Code)
49+
}
50+
51+
expected := `{"gpu_vendor":"nvidia","gpu_driver_version":"570.86.15"}`
52+
53+
if strings.TrimSpace(responseRecorder.Body.String()) != expected {
54+
t.Errorf("Want '%s', got '%s'", expected, responseRecorder.Body.String())
55+
}
56+
}
57+
58+
func TestInstanceInfoWithoutGpus(t *testing.T) {
59+
request := httptest.NewRequest("GET", "/api/instance/info", nil)
60+
responseRecorder := httptest.NewRecorder()
61+
62+
server := NewShimServer(context.Background(), ":12347", "0.0.1.dev2", NewDummyRunner(), nil, nil, nil, nil)
63+
64+
f := commonapi.JSONResponseHandler(server.InstanceInfoHandler)
65+
f(responseRecorder, request)
66+
67+
if responseRecorder.Code != 200 {
68+
t.Errorf("Want status '%d', got '%d'", 200, responseRecorder.Code)
69+
}
70+
71+
expected := "{}"
72+
73+
if strings.TrimSpace(responseRecorder.Body.String()) != expected {
74+
t.Errorf("Want '%s', got '%s'", expected, responseRecorder.Body.String())
75+
}
76+
}
77+
3278
func TestTaskSubmit(t *testing.T) {
3379
server := NewShimServer(context.Background(), ":12340", "0.0.1.dev2", NewDummyRunner(), nil, nil, nil, nil)
3480
requestBody := `{

runner/internal/shim/api/schemas.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ type InstanceHealthResponse struct {
1919
DCGM *dcgm.Health `json:"dcgm"`
2020
}
2121

22+
// InstanceInfoResponse reports facts about the host observed by shim. Fields are
23+
// omitted if the corresponding fact is not applicable or could not be detected.
24+
type InstanceInfoResponse struct {
25+
GpuVendor string `json:"gpu_vendor,omitempty"`
26+
GpuDriverVersion string `json:"gpu_driver_version,omitempty"`
27+
}
28+
2229
type TaskListResponse struct {
2330
Tasks []*shim.TaskListItem `json:"tasks"`
2431
}

runner/internal/shim/api/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/dstackai/dstack/runner/internal/shim"
1414
"github.com/dstackai/dstack/runner/internal/shim/components"
1515
"github.com/dstackai/dstack/runner/internal/shim/dcgm"
16+
"github.com/dstackai/dstack/runner/internal/shim/host"
1617
)
1718

1819
type TaskRunner interface {
@@ -22,6 +23,7 @@ type TaskRunner interface {
2223
Remove(ctx context.Context, taskID string) error
2324

2425
Resources(context.Context) shim.Resources
26+
Gpus(context.Context) []host.GpuInfo
2527
TaskList() []*shim.TaskListItem
2628
TaskInfo(taskID string) shim.TaskInfo
2729
}
@@ -85,6 +87,7 @@ func NewShimServer(
8587
r.AddHandler("GET", "/api/healthcheck", s.HealthcheckHandler)
8688
r.AddHandler("POST", "/api/shutdown", s.ShutdownHandler)
8789
r.AddHandler("GET", "/api/instance/health", s.InstanceHealthHandler)
90+
r.AddHandler("GET", "/api/instance/info", s.InstanceInfoHandler)
8891
r.AddHandler("GET", "/api/components", s.ComponentListHandler)
8992
r.AddHandler("POST", "/api/components/install", s.ComponentInstallHandler)
9093
r.AddHandler("GET", "/api/tasks", s.TaskListHandler)

runner/internal/shim/docker.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,12 @@ func (d *DockerRunner) Resources(ctx context.Context) Resources {
324324
}
325325
}
326326

327+
// Gpus returns the GPUs detected at startup without collecting other host
328+
// resources, making it suitable for frequently called paths.
329+
func (d *DockerRunner) Gpus(ctx context.Context) []host.GpuInfo {
330+
return d.gpus
331+
}
332+
327333
func (d *DockerRunner) TaskList() []*TaskListItem {
328334
tasks := d.tasks.List()
329335
result := make([]*TaskListItem, 0, len(tasks))

0 commit comments

Comments
 (0)