Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ before:
- go mod download
builds:
- env:
- CGO_ENABLED=0
- CGO_ENABLED=1
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We now always need CGO

goos:
- darwin
- linux
Expand Down
30 changes: 25 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
.DEFAULT_GOAL := fast-build
VERSION := dev-$(shell git rev-parse HEAD | cut -c 1-8)

# Cross-compilation via Docker (golang:1.24 native Linux container).
# When arch=<GOOS>/<GOARCH> is provided, spin up a container that matches
# the target platform so CGO uses the native Linux gcc/GNU ld toolchain
_GOMODCACHE := $(shell go env GOMODCACHE)
ifdef arch
_CROSS_GOOS := $(word 1,$(subst /, ,$(arch)))
_CROSS_GOARCH := $(word 2,$(subst /, ,$(arch)))
_BUILD_PREFIX := docker run --rm \
--platform $(_CROSS_GOOS)/$(_CROSS_GOARCH) \
-v $(CURDIR):/app \
-v $(_GOMODCACHE):/go/pkg/mod \
-e CGO_ENABLED=1 \
-e GOPRIVATE=github.com/brevdev/* \
-e GONOSUMDB=github.com/brevdev/* \
-w /app \
golang:1.24
else
_BUILD_PREFIX := CGO_ENABLED=1
endif

.PHONY: fast-build
fast-build: ## go build -o brev
$(call print-target)
echo ${VERSION}
CGO_ENABLED=0 go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
CGO_ENABLED=1 go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"

.PHONY: local
local: ## build with env wrapper (use: make local env=dev0|dev1|dev2|stg arch=linux/amd64, or make local for defaults)
$(call print-target)
ifdef env
@echo "Building with env=$(env) wrapper..."
@echo ${VERSION}
$(if $(arch),GOOS=$(word 1,$(subst /, ,$(arch))) GOARCH=$(word 2,$(subst /, ,$(arch))),) CGO_ENABLED=0 go build -o brev-local -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
$(_BUILD_PREFIX) go build -o brev-local -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
@echo '#!/bin/sh' > brev
@echo '# Auto-generated wrapper with environment overrides' >> brev
@echo 'export BREV_CONSOLE_URL="https://localhost.nvidia.com:3000"' >> brev
Expand All @@ -26,7 +46,7 @@ ifdef env
@chmod +x brev
else
@echo "Building without environment overrides (using config.go defaults)..."
$(if $(arch),GOOS=$(word 1,$(subst /, ,$(arch))) GOARCH=$(word 2,$(subst /, ,$(arch))),) CGO_ENABLED=0 go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
$(_BUILD_PREFIX) go build -o brev -ldflags "-X github.com/brevdev/brev-cli/pkg/cmd/version.Version=${VERSION}"
endif

.PHONY: install-dev
Expand Down Expand Up @@ -305,8 +325,8 @@ develop-with-nix:
update-devplane-deps: ## update devplane dependencies (use: make update-devplane-deps commit=<hash-or-tag>, defaults to latest)
@COMMIT=$${commit:-latest}; \
echo "Updating devplane dependencies to: $$COMMIT"; \
go get -u github.com/brevdev/dev-plane@$$COMMIT; \
GOPRIVATE=github.com/brevdev/* go get -u github.com/brevdev/dev-plane@$$COMMIT; \
go get buf.build/gen/go/brevdev/devplane/grpc/go@$$COMMIT; \
go get buf.build/gen/go/brevdev/devplane/protocolbuffers/go@$$COMMIT; \
go mod tidy; \
GOPRIVATE=github.com/brevdev/* go mod tidy; \
echo "Successfully updated to $$COMMIT"
Comment on lines -308 to 356
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unrelated but this allows for fetching internal dependencies without relying on the GOPRIVATE to have been set by the user.

5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ go 1.24.0

require (
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260228021043-887d38e1b474.2
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260305005117-3cacb6388cd4.1
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260305201249-af5106090c8a.1
connectrpc.com/connect v1.19.1
github.com/NVIDIA/go-nvml v0.13.0-1
github.com/alessio/shellescape v1.4.1
github.com/brevdev/parse v0.0.11
github.com/briandowns/spinner v1.16.0
Expand Down Expand Up @@ -150,7 +151,7 @@ require (
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/sys v0.40.0
golang.org/x/term v0.39.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/protobuf v1.36.11
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260228021043-887d38e1b474.2 h1:Sq0kIa/xKzScbJcqB5EbPVhOL0QYHPr3araQaupL2lk=
buf.build/gen/go/brevdev/devplane/connectrpc/go v1.19.1-20260228021043-887d38e1b474.2/go.mod h1:Yh34p9aADmWsKv2umYlMpnCZuBmNBE9N+HImgRriJXM=
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260305005117-3cacb6388cd4.1 h1:3Y3FI5kbM4uacawy5dySjVTPSbu2BJMO42eQHf2wz+g=
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260305005117-3cacb6388cd4.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260305201249-af5106090c8a.1 h1:d+kY4OSI/WV2eBuO5G7ezCQ8RiLtDOIrUbtmZOKi5Kw=
buf.build/gen/go/brevdev/devplane/protocolbuffers/go v1.36.11-20260305201249-af5106090c8a.1/go.mod h1:V/y7Wxg0QvU4XPVwqErF5NHLobUT1QEyfgrGuQIxdPo=
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1 h1:6amhprQmCKJ4wgJ6ngkh32d9V+dQcOLUZ/SfHdOnYgo=
buf.build/gen/go/brevdev/protoc-gen-gotag/protocolbuffers/go v1.36.11-20220906235457-8b4922735da5.1/go.mod h1:O+pnSHMru/naTMrm4tmpBoH3wz6PHa+R75HR7Mv8X2g=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
Expand Down Expand Up @@ -53,6 +53,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/NVIDIA/go-nvml v0.13.0-1 h1:OLX8Jq3dONuPOQPC7rndB6+iDmDakw0XTYgzMxObkEw=
github.com/NVIDIA/go-nvml v0.13.0-1/go.mod h1:+KNA7c7gIBH7SKSJ1ntlwkfN80zdx8ovl4hrK3LmPt4=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
Expand Down
12 changes: 6 additions & 6 deletions pkg/cmd/register/device_registration_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ const (
// DeviceRegistration is the persistent identity file for a registered device.
// Fields align with the AddNodeResponse from dev-plane.
type DeviceRegistration struct {
ExternalNodeID string `json:"external_node_id"`
DisplayName string `json:"display_name"`
OrgID string `json:"org_id"`
DeviceID string `json:"device_id"`
RegisteredAt string `json:"registered_at"`
NodeSpec NodeSpec `json:"node_spec"`
ExternalNodeID string `json:"external_node_id"`
DisplayName string `json:"display_name"`
OrgID string `json:"org_id"`
DeviceID string `json:"device_id"`
RegisteredAt string `json:"registered_at"`
HardwareProfile HardwareProfile `json:"hardware_profile"`
}

// RegistrationStore defines the contract for persisting device registration data.
Expand Down
10 changes: 5 additions & 5 deletions pkg/cmd/register/device_registration_store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func Test_SaveAndLoadRegistration_RoundTrip(t *testing.T) {
OrgID: "org_xyz",
DeviceID: "device-uuid-123",
RegisteredAt: "2026-02-25T00:00:00Z",
NodeSpec: NodeSpec{
HardwareProfile: HardwareProfile{
CPUCount: &cpuCount,
RAMBytes: &ramBytes,
Architecture: "arm64",
Expand Down Expand Up @@ -59,11 +59,11 @@ func Test_SaveAndLoadRegistration_RoundTrip(t *testing.T) {
if loaded.DeviceID != reg.DeviceID {
t.Errorf("DeviceID mismatch: got %s, want %s", loaded.DeviceID, reg.DeviceID)
}
if loaded.NodeSpec.Architecture != "arm64" {
t.Errorf("Architecture mismatch: got %s", loaded.NodeSpec.Architecture)
if loaded.HardwareProfile.Architecture != "arm64" {
t.Errorf("Architecture mismatch: got %s", loaded.HardwareProfile.Architecture)
}
if loaded.NodeSpec.CPUCount == nil || *loaded.NodeSpec.CPUCount != 12 {
t.Errorf("CPUCount mismatch: got %v", loaded.NodeSpec.CPUCount)
if loaded.HardwareProfile.CPUCount == nil || *loaded.HardwareProfile.CPUCount != 12 {
t.Errorf("CPUCount mismatch: got %v", loaded.HardwareProfile.CPUCount)
}
}

Expand Down
178 changes: 178 additions & 0 deletions pkg/cmd/register/gpu_nvml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
//go:build linux || windows

package register

import (
"fmt"

"github.com/NVIDIA/go-nvml/pkg/nvml"
)

// archName returns the GPU architecture name for the given CUDA compute capability.
func archName(major, minor int) string {
switch major {
case 1:
return "Tesla"
case 2:
return "Fermi"
case 3:
return "Kepler"
case 5:
return "Maxwell"
case 6:
return "Pascal"
case 7:
if minor >= 5 {
return "Turing"
}
return "Volta"
case 8:
if minor >= 9 {
return "Ada Lovelace"
}
return "Ampere"
case 9:
return "Hopper"
case 10:
return "Blackwell"
case 12:
return "Vera Rubin"
default:
return ""
}
}

// probeGPUsNVML uses NVML to detect GPUs and interconnects.
// Returns (nil, nil) if NVML is unavailable (e.g. no driver installed).
func probeGPUsNVML() ([]GPU, []Interconnect) {
ret := nvml.Init()
if ret != nvml.SUCCESS {
return nil, nil
}
defer func() { _ = nvml.Shutdown() }()

count, ret := nvml.DeviceGetCount()
if ret != nvml.SUCCESS || count == 0 {
return nil, nil
}

type gpuKey struct {
model string
arch string
mem int64
}
counts := make(map[gpuKey]int32)
var order []gpuKey
var interconnects []Interconnect

for i := 0; i < count; i++ {
device, ret := nvml.DeviceGetHandleByIndex(i)
if ret != nvml.SUCCESS {
continue
}

name, ret := device.GetName()
if ret != nvml.SUCCESS {
name = "Unknown"
}

var memBytes int64
memInfo, ret := device.GetMemoryInfo()
if ret == nvml.SUCCESS {
memBytes = int64(memInfo.Total)
}

arch := ""
major, minor, ret := device.GetCudaComputeCapability()
if ret == nvml.SUCCESS {
if name := archName(major, minor); name != "" {
arch = name
} else {
arch = fmt.Sprintf("sm_%d%d", major, minor)
}
}

key := gpuKey{model: name, arch: arch, mem: memBytes}
if counts[key] == 0 {
order = append(order, key)
}
counts[key]++

// Probe NVLink interconnects for this device.
interconnects = append(interconnects, probeNVLink(device, i)...)

// Probe PCIe interconnect for this device.
if ic := probePCIe(device, i); ic != nil {
interconnects = append(interconnects, *ic)
}
}

gpus := make([]GPU, 0, len(order))
for _, key := range order {
mem := key.mem
g := GPU{
Model: key.model,
Architecture: key.arch,
Count: counts[key],
}
if mem > 0 {
g.MemoryBytes = &mem
}
gpus = append(gpus, g)
}

return gpus, interconnects
}

// probeNVLink checks NVLink connections for a device.
func probeNVLink(device nvml.Device, deviceIdx int) []Interconnect {
var ics []Interconnect
activeLinks := 0

// NVLink link count varies by architecture; try up to 18 links.
var nvlinkVersion uint32
for link := 0; link < 18; link++ {
state, ret := device.GetNvLinkState(link)
if ret != nvml.SUCCESS {
break
}
if state == nvml.FEATURE_ENABLED {
activeLinks++
if nvlinkVersion == 0 {
ver, ret := device.GetNvLinkVersion(link)
if ret == nvml.SUCCESS {
nvlinkVersion = ver
}
}
}
}

if activeLinks > 0 {
ics = append(ics, Interconnect{
Type: "NVLink",
Device: fmt.Sprintf("GPU %d", deviceIdx),
ActiveLinks: activeLinks,
Version: nvlinkVersion,
})
}

return ics
}

// probePCIe reads PCIe generation and width for a device.
func probePCIe(device nvml.Device, deviceIdx int) *Interconnect {
gen, ret := device.GetCurrPcieLinkGeneration()
if ret != nvml.SUCCESS {
return nil
}
width, ret := device.GetCurrPcieLinkWidth()
if ret != nvml.SUCCESS {
return nil
}
return &Interconnect{
Type: "PCIe",
Device: fmt.Sprintf("GPU %d", deviceIdx),
Generation: gen,
Width: width,
}
}
Loading
Loading