Skip to content

Commit 05e4ad0

Browse files
committed
feat: enhance platform handling in studio commands and library management, default to linux/amd64 for container images
1 parent f8ef227 commit 05e4ad0

9 files changed

Lines changed: 117 additions & 39 deletions

File tree

cmd/ggo/studio/studio.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ Examples:
223223
cmd.Flags().StringVar(&dockerHost, "docker-host", "", "Custom Docker socket path (e.g., unix:///path/to/docker.sock)")
224224
cmd.Flags().StringArrayVarP(&command, "command", "c", nil, "Container startup command or ENTRYPOINT args (can be specified multiple times)")
225225
cmd.Flags().StringVar(&endpoint, "endpoint", "", "Override GPU worker endpoint URL")
226-
cmd.Flags().StringVar(&platform, "platform", "", "Container image platform (e.g., linux/amd64, linux/arm64). Default: auto-detect from VM/host")
226+
cmd.Flags().StringVar(&platform, "platform", "", "Container image platform (e.g., linux/amd64, linux/arm64). Default: linux/amd64")
227227

228228
return cmd
229229
}
@@ -253,9 +253,17 @@ func runCreate(cmd *cobra.Command, args []string) error {
253253
klog.Infof("Resolved share link: worker_id=%s vendor=%s connection_url=%s",
254254
shareInfo.WorkerID, shareInfo.HardwareVendor, shareInfo.ConnectionURL)
255255

256+
// Determine target arch from platform flag (default: amd64)
257+
targetArch := "amd64"
258+
if platform != "" {
259+
if parts := strings.SplitN(platform, "/", 2); len(parts) == 2 {
260+
targetArch = parts[1]
261+
}
262+
}
263+
256264
// Download required GPU client libraries before creating studio
257265
// Filter by vendor from share info to avoid downloading unnecessary libraries
258-
if err := ensureRemoteGPUClientLibs(ctx, out, shareInfo.HardwareVendor); err != nil {
266+
if err := ensureRemoteGPUClientLibs(ctx, out, shareInfo.HardwareVendor, targetArch); err != nil {
259267
cmd.SilenceUsage = true
260268
klog.Errorf("Failed to ensure GPU client libraries: error=%v", err)
261269
return fmt.Errorf("failed to download GPU client libraries: %w", err)
@@ -307,16 +315,16 @@ func runCreate(cmd *cobra.Command, args []string) error {
307315

308316
// ensureRemoteGPUClientLibs downloads remote-gpu-client libraries if not already present
309317
// vendorSlug filters by vendor (e.g., "nvidia", "amd") to avoid downloading unnecessary libraries
318+
// targetArch specifies the CPU architecture (e.g., "amd64", "arm64") for the target container platform
310319
// Note: Studio environments run in Linux containers, so we always download Linux libraries
311-
// using the current CPU architecture (arm64 or amd64)
312-
func ensureRemoteGPUClientLibs(ctx context.Context, out *tui.Output, vendorSlug string) error {
320+
func ensureRemoteGPUClientLibs(ctx context.Context, out *tui.Output, vendorSlug, targetArch string) error {
313321
depsMgr := deps.NewManager()
314322

315323
// Target library types that are needed for GPU client functionality
316324
targetTypes := []string{deps.LibraryTypeRemoteGPUClient, deps.LibraryTypeVGPULibrary}
317325

318326
if !out.IsJSON() {
319-
out.Printf("Downloading GPU client libraries for %s...\n", vendorSlug)
327+
out.Printf("Downloading GPU client libraries for %s (linux/%s)...\n", vendorSlug, targetArch)
320328
}
321329

322330
progressFn := func(lib deps.Library, downloaded, total int64) {
@@ -326,9 +334,9 @@ func ensureRemoteGPUClientLibs(ctx context.Context, out *tui.Output, vendorSlug
326334
}
327335
}
328336

329-
// Studio environments always run in Linux containers, so we need Linux libraries
330-
// Use current CPU architecture (arm64/amd64) since container arch matches host
331-
libs, err := depsMgr.EnsureLibrariesByTypesForPlatform(ctx, targetTypes, vendorSlug, "linux", "", progressFn)
337+
// Studio environments always run in Linux containers
338+
// Download libraries for the specified target architecture
339+
libs, err := depsMgr.EnsureLibrariesByTypesForPlatform(ctx, targetTypes, vendorSlug, "linux", targetArch, progressFn)
332340
if err != nil {
333341
return fmt.Errorf("failed to ensure GPU client libraries: %w", err)
334342
}
@@ -396,6 +404,12 @@ func buildCreateOptions(name string, shareInfo *api.SharePublicInfo) (*studio.Cr
396404
}
397405
}
398406

407+
// Default platform to linux/amd64 for studio containers
408+
effectivePlatform := platform
409+
if effectivePlatform == "" {
410+
effectivePlatform = "linux/amd64"
411+
}
412+
399413
return &studio.CreateOptions{
400414
Name: name,
401415
Mode: studioMode,
@@ -412,7 +426,7 @@ func buildCreateOptions(name string, shareInfo *api.SharePublicInfo) (*studio.Cr
412426
},
413427
Command: command,
414428
Endpoint: endpointOverride,
415-
Platform: platform,
429+
Platform: effectivePlatform,
416430
UseLocalGPU: gpuWorkerURL == "" && (studioMode == studio.ModeDocker || studioMode == studio.ModeWSL || studioMode == studio.ModeAuto),
417431
}, nil
418432
}

internal/deps/deps.go

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -661,29 +661,38 @@ func (m *Manager) ComputeUpdateDiff() (*UpdateDiff, error) {
661661
return diff, nil
662662
}
663663

664-
// DownloadLibrary downloads a library to the cache directory
664+
// DownloadLibrary downloads a library to the default (flat) libs directory
665665
func (m *Manager) DownloadLibrary(ctx context.Context, lib Library, progressFn func(downloaded, total int64)) error {
666666
m.mu.Lock()
667667
defer m.mu.Unlock()
668668

669-
return m.downloadLibraryUnsafe(ctx, lib, progressFn)
669+
return m.downloadLibraryToDir(ctx, lib, m.paths.LibsDir(), progressFn)
670670
}
671671

672-
func (m *Manager) downloadLibraryUnsafe(ctx context.Context, lib Library, progressFn func(downloaded, total int64)) error {
672+
// DownloadLibraryToDir downloads a library to a specific libs directory.
673+
func (m *Manager) DownloadLibraryToDir(ctx context.Context, lib Library, libsDir string, progressFn func(downloaded, total int64)) error {
674+
m.mu.Lock()
675+
defer m.mu.Unlock()
676+
677+
return m.downloadLibraryToDir(ctx, lib, libsDir, progressFn)
678+
}
679+
680+
// downloadLibraryToDir downloads a library to a specific libs directory.
681+
// Shared libraries (.so/.dll) go to libsDir; binaries go to cache root.
682+
func (m *Manager) downloadLibraryToDir(ctx context.Context, lib Library, libsDir string, progressFn func(downloaded, total int64)) error {
673683
// Ensure cache directory exists
674684
cacheDir := m.paths.CacheDir()
675685
if err := os.MkdirAll(cacheDir, 0755); err != nil {
676686
return fmt.Errorf("failed to create cache directory: %w", err)
677687
}
678688

679689
// Ensure libs directory exists for shared libraries
680-
libsDir := m.paths.LibsDir()
681690
if err := os.MkdirAll(libsDir, 0755); err != nil {
682691
return fmt.Errorf("failed to create libs directory: %w", err)
683692
}
684693

685694
// Destination path - shared libraries go to libs dir, binaries to cache dir
686-
destPath := m.GetLibraryPath(lib.Name)
695+
destPath := m.GetLibraryPathInDir(lib.Name, libsDir)
687696
tmpPath := destPath + ".tmp"
688697

689698
// Check if already downloaded with correct version
@@ -866,7 +875,7 @@ func (m *Manager) DownloadAllRequired(ctx context.Context, progressFn func(lib L
866875
}
867876
}
868877

869-
if err := m.downloadLibraryUnsafe(ctx, lib, libProgressFn); err != nil {
878+
if err := m.downloadLibraryToDir(ctx, lib, m.paths.LibsDir(), libProgressFn); err != nil {
870879
result.Status = DownloadStatusFailed
871880
result.Error = err.Error()
872881
klog.Errorf("Failed to download library: name=%s error=%v", lib.Name, err)
@@ -917,6 +926,15 @@ func (m *Manager) GetLibraryPath(name string) string {
917926
return filepath.Join(m.paths.CacheDir(), name)
918927
}
919928

929+
// GetLibraryPathInDir returns the path to a library in a specific libs directory.
930+
// For shared libraries, uses the given libsDir; for binaries, uses cache root.
931+
func (m *Manager) GetLibraryPathInDir(name string, libsDir string) string {
932+
if isSharedLibrary(name) {
933+
return filepath.Join(libsDir, name)
934+
}
935+
return filepath.Join(m.paths.CacheDir(), name)
936+
}
937+
920938
// GetLibsDir returns the directory for .so/.dll library files
921939
// This directory is used for LD_LIBRARY_PATH, ld.so.conf, and ld.so.preload
922940
func (m *Manager) GetLibsDir() string {
@@ -1115,9 +1133,28 @@ func (m *Manager) EnsureLibrariesByTypes(ctx context.Context, libTypes []string,
11151133

11161134
// EnsureLibrariesByTypesForPlatform ensures ALL libraries of the specified types exist and are downloaded for a specific platform
11171135
// targetOS and targetArch specify the target platform (e.g., "linux", "arm64")
1118-
// If both are empty, uses the current platform
1119-
// This is useful when running on macOS but needing Linux libraries for containers
1136+
// If both are empty, uses the current platform and the flat libs directory (agent/worker path).
1137+
// If targetOS is specified, uses arch-specific subdirectory (e.g., libs/linux-amd64/) to avoid
1138+
// collisions between different architectures (studio/use/launch path).
11201139
func (m *Manager) EnsureLibrariesByTypesForPlatform(ctx context.Context, libTypes []string, vendorSlug, targetOS, targetArch string, progressFn func(lib Library, downloaded, total int64)) ([]Library, error) {
1140+
// Resolve effective platform values
1141+
effectiveOS := targetOS
1142+
effectiveArch := targetArch
1143+
if effectiveOS == "" {
1144+
effectiveOS = runtime.GOOS
1145+
}
1146+
if effectiveArch == "" {
1147+
effectiveArch = runtime.GOARCH
1148+
}
1149+
1150+
// Determine the libs directory:
1151+
// - If targetOS was explicitly specified, use arch-specific subdir (studio/use/launch)
1152+
// - Otherwise, use flat libs dir (agent/worker)
1153+
libsDir := m.paths.LibsDir()
1154+
if targetOS != "" {
1155+
libsDir = m.paths.LibsDirForPlatform(effectiveOS, effectiveArch)
1156+
}
1157+
11211158
// Ensure deps manifest exists and is up to date for the target platform
11221159
if err := m.ensureDepsManifestForPlatform(ctx, targetOS, targetArch); err != nil {
11231160
return nil, err
@@ -1167,7 +1204,7 @@ func (m *Manager) EnsureLibrariesByTypesForPlatform(ctx context.Context, libType
11671204
var toDownload []Library
11681205

11691206
for _, lib := range targetLibs {
1170-
filePath := m.GetLibraryPath(lib.Name)
1207+
filePath := m.GetLibraryPathInDir(lib.Name, libsDir)
11711208
downloadedLib, exists := downloaded.Libraries[lib.Key()]
11721209

11731210
needsDownload := false
@@ -1186,7 +1223,7 @@ func (m *Manager) EnsureLibrariesByTypesForPlatform(ctx context.Context, libType
11861223
}
11871224
}
11881225

1189-
klog.V(4).Infof("Libraries to download: %d out of %d total (libs will go to: %s)", len(toDownload), len(targetLibs), m.paths.LibsDir())
1226+
klog.V(4).Infof("Libraries to download: %d out of %d total (libs will go to: %s)", len(toDownload), len(targetLibs), libsDir)
11901227

11911228
// Download missing libraries
11921229
for _, lib := range toDownload {
@@ -1196,8 +1233,8 @@ func (m *Manager) EnsureLibrariesByTypesForPlatform(ctx context.Context, libType
11961233
}
11971234
}
11981235

1199-
klog.Infof("Downloading library: name=%s version=%s type=%s", lib.Name, lib.Version, lib.Type)
1200-
if err := m.DownloadLibrary(ctx, lib, libProgressFn); err != nil {
1236+
klog.Infof("Downloading library: name=%s version=%s type=%s to=%s", lib.Name, lib.Version, lib.Type, libsDir)
1237+
if err := m.DownloadLibraryToDir(ctx, lib, libsDir, libProgressFn); err != nil {
12011238
return nil, fmt.Errorf("failed to download library %s: %w", lib.Name, err)
12021239
}
12031240
}

internal/platform/paths.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,19 @@ func (p *Paths) LibDir() string {
7272
// This is separate from CacheDir which contains all downloaded files (including binaries)
7373
// The libs directory is used for LD_LIBRARY_PATH, ld.so.conf, and ld.so.preload
7474
// All platforms: ~/.gpugo/cache/libs (or CacheDir/libs)
75+
// Used by agent/worker which always runs on the native host architecture.
7576
func (p *Paths) LibsDir() string {
7677
return filepath.Join(p.cacheDir, "libs")
7778
}
7879

80+
// LibsDirForPlatform returns the arch-specific libs directory for a given OS and architecture.
81+
// This is used by studio/use/launch which may download libraries for a different platform
82+
// (e.g., linux/amd64 libs on an arm64 Mac).
83+
// Layout: ~/.gpugo/cache/libs/{os}-{arch} (e.g., ~/.gpugo/cache/libs/linux-amd64)
84+
func (p *Paths) LibsDirForPlatform(targetOS, arch string) string {
85+
return filepath.Join(p.cacheDir, "libs", targetOS+"-"+arch)
86+
}
87+
7988
// BinDir returns the directory for binaries
8089
// All platforms: ~/.gpugo/bin (or UserDir/bin)
8190
func (p *Paths) BinDir() string {

internal/studio/backend_apple.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ func (b *AppleContainerBackend) Create(ctx context.Context, opts *CreateOptions)
132132
StudioName: opts.Name,
133133
GPUWorkerURL: gpuWorkerURL,
134134
HardwareVendor: opts.HardwareVendor,
135+
Platform: opts.Platform,
135136
MountUserHome: false, // /Users is mounted directly into the container
136137
SkipFileMounts: true,
137138
}

internal/studio/backend_colima.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,7 @@ func (b *ColimaBackend) Create(ctx context.Context, opts *CreateOptions) (*Envir
425425
StudioName: opts.Name,
426426
GPUWorkerURL: gpuWorkerURL,
427427
HardwareVendor: opts.HardwareVendor,
428+
Platform: opts.Platform,
428429
MountUserHome: false, // /Users is mounted directly into the container
429430
}
430431

internal/studio/backend_docker.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ func (b *DockerBackend) Create(ctx context.Context, opts *CreateOptions) (*Envir
203203
StudioName: opts.Name,
204204
GPUWorkerURL: gpuWorkerURL,
205205
HardwareVendor: opts.HardwareVendor,
206+
Platform: opts.Platform,
206207
MountUserHome: !opts.NoUserVolume,
207208
}
208209

internal/studio/backend_wsl.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ func (b *WSLBackend) Create(ctx context.Context, opts *CreateOptions) (*Environm
266266
StudioName: opts.Name,
267267
GPUWorkerURL: gpuWorkerURL,
268268
HardwareVendor: opts.HardwareVendor,
269+
Platform: opts.Platform,
269270
MountUserHome: !opts.NoUserVolume,
270271
}
271272

internal/studio/container_setup.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8-
"runtime"
98
"strings"
109

1110
"github.com/NexusGPU/gpu-go/internal/deps"
@@ -21,6 +20,10 @@ type ContainerSetupConfig struct {
2120
GPUWorkerURL string
2221
// HardwareVendor is the GPU vendor (nvidia, amd, hygon)
2322
HardwareVendor string
23+
// Platform is the container platform (e.g., "linux/amd64", "linux/arm64")
24+
// Used to determine which arch-specific libs to download and mount.
25+
// If empty, defaults to linux/amd64.
26+
Platform string
2427
// MountUserHome indicates whether to mount the user's home directory
2528
MountUserHome bool
2629
// SkipSSHMounts disables mounting SSH key files into the container
@@ -43,7 +46,7 @@ type ContainerSetupResult struct {
4346

4447
// SetupContainerGPUEnv sets up the GPU environment for a container
4548
// This performs the same setup as `ggo use` does for Linux:
46-
// 1. Downloads GPU client libraries for Linux (using host CPU arch)
49+
// 1. Downloads GPU client libraries for Linux (using target platform arch)
4750
// 2. Sets up GPU environment (env vars, ld.so.preload, ld.so.conf.d)
4851
// 3. Optionally mounts user home directory
4952
func SetupContainerGPUEnv(ctx context.Context, config *ContainerSetupConfig) (*ContainerSetupResult, error) {
@@ -56,10 +59,20 @@ func SetupContainerGPUEnv(ctx context.Context, config *ContainerSetupConfig) (*C
5659
normalizedName := platform.NormalizeName(config.StudioName)
5760
vendor := ParseVendor(config.HardwareVendor)
5861

62+
// Parse target arch from platform string (e.g., "linux/amd64" → "amd64")
63+
targetArch := "amd64" // default
64+
if config.Platform != "" {
65+
if parts := strings.SplitN(config.Platform, "/", 2); len(parts) == 2 {
66+
targetArch = parts[1]
67+
}
68+
}
69+
// Arch-specific libs directory (e.g., ~/.gpugo/cache/libs/linux-amd64/)
70+
libsDir := paths.LibsDirForPlatform("linux", targetArch)
71+
5972
// Step 1: Download GPU client libraries for Linux (container target)
60-
// Libraries are downloaded for Linux with host CPU architecture
73+
// Libraries are downloaded for Linux with the target CPU architecture
6174
if config.GPUWorkerURL != "" {
62-
if err := ensureGPUClientLibraries(ctx, vendor); err != nil {
75+
if err := ensureGPUClientLibraries(ctx, vendor, targetArch); err != nil {
6376
klog.Warningf("Failed to download GPU client libraries: %v (continuing anyway)", err)
6477
} else {
6578
result.LibrariesDownloaded = true
@@ -72,6 +85,7 @@ func SetupContainerGPUEnv(ctx context.Context, config *ContainerSetupConfig) (*C
7285
Vendor: vendor,
7386
ConnectionURL: config.GPUWorkerURL,
7487
CachePath: paths.CacheDir(),
88+
LibsPath: libsDir,
7589
LogPath: paths.StudioLogsDir(normalizedName),
7690
StudioName: normalizedName,
7791
IsContainer: true,
@@ -89,7 +103,7 @@ func SetupContainerGPUEnv(ctx context.Context, config *ContainerSetupConfig) (*C
89103

90104
if config.SkipFileMounts {
91105
result.EnvVars["LD_LIBRARY_PATH"] = "/opt/gpugo/libs"
92-
if preload := buildContainerLDPreload(paths.LibsDir(), vendor); preload != "" {
106+
if preload := buildContainerLDPreload(libsDir, vendor); preload != "" {
93107
result.EnvVars["LD_PRELOAD"] = preload
94108
}
95109
}
@@ -134,7 +148,7 @@ func SetupContainerGPUEnv(ctx context.Context, config *ContainerSetupConfig) (*C
134148

135149
// Step 5: Download and mount GPU binary (like nvidia-smi) to /usr/local/bin/
136150
if !config.SkipFileMounts && config.GPUWorkerURL != "" && config.HardwareVendor != "" {
137-
gpuBinMount, err := ensureAndMountGPUBinary(ctx, paths, config.HardwareVendor)
151+
gpuBinMount, err := ensureAndMountGPUBinary(ctx, paths, config.HardwareVendor, targetArch)
138152
if err != nil {
139153
klog.Warningf("Failed to setup GPU binary mount: %v (continuing without it)", err)
140154
} else if gpuBinMount != nil {
@@ -152,9 +166,9 @@ func SetupContainerGPUEnv(ctx context.Context, config *ContainerSetupConfig) (*C
152166

153167
// ensureAndMountGPUBinary downloads GPU binary (like nvidia-smi) and returns a volume mount
154168
// The binary is mounted to /usr/local/bin/ in the container
155-
func ensureAndMountGPUBinary(ctx context.Context, paths *platform.Paths, vendorSlug string) (*VolumeMount, error) {
156-
// Studios run in Linux containers, so download Linux binary with host CPU arch
157-
binPath, err := deps.EnsureGPUBinaryForPlatform(ctx, paths, vendorSlug, "linux", runtime.GOARCH)
169+
func ensureAndMountGPUBinary(ctx context.Context, paths *platform.Paths, vendorSlug, targetArch string) (*VolumeMount, error) {
170+
// Studios run in Linux containers, so download Linux binary with target CPU arch
171+
binPath, err := deps.EnsureGPUBinaryForPlatform(ctx, paths, vendorSlug, "linux", targetArch)
158172
if err != nil {
159173
return nil, fmt.Errorf("failed to ensure GPU binary: %w", err)
160174
}
@@ -180,8 +194,8 @@ func ensureAndMountGPUBinary(ctx context.Context, paths *platform.Paths, vendorS
180194
}
181195

182196
// ensureGPUClientLibraries downloads GPU client libraries for Linux containers
183-
// Libraries are downloaded for Linux platform with the current host CPU architecture
184-
func ensureGPUClientLibraries(ctx context.Context, vendor GPUVendor) error {
197+
// Libraries are downloaded for Linux platform with the specified target CPU architecture
198+
func ensureGPUClientLibraries(ctx context.Context, vendor GPUVendor, targetArch string) error {
185199
depsMgr := deps.NewManager()
186200

187201
// Target library types needed for GPU client functionality
@@ -198,11 +212,11 @@ func ensureGPUClientLibraries(ctx context.Context, vendor GPUVendor) error {
198212
vendorSlug = "hygon"
199213
}
200214

201-
klog.Infof("Downloading GPU client libraries for %s (linux/%s)...", vendorSlug, runtime.GOARCH)
215+
klog.Infof("Downloading GPU client libraries for %s (linux/%s)...", vendorSlug, targetArch)
202216

203217
// Studios run in Linux containers, so we always download Linux libraries
204-
// CPU architecture matches the host (arm64 on Apple Silicon, amd64 on Intel/AMD)
205-
_, err := depsMgr.EnsureLibrariesByTypesForPlatform(ctx, targetTypes, vendorSlug, "linux", "", nil)
218+
// Use the specified target architecture from the platform flag
219+
_, err := depsMgr.EnsureLibrariesByTypesForPlatform(ctx, targetTypes, vendorSlug, "linux", targetArch, nil)
206220
if err != nil {
207221
return fmt.Errorf("failed to ensure GPU client libraries: %w", err)
208222
}

internal/studio/libdownloader.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ func DetectArchitecture() Architecture {
6464

6565
// Normalize architecture names
6666
switch arch.Arch {
67-
case "amd64":
68-
arch.Arch = "amd64"
69-
case "arm64":
70-
arch.Arch = "arm64"
67+
case ArchAmd64:
68+
arch.Arch = ArchAmd64
69+
case ArchArm64:
70+
arch.Arch = ArchArm64
7171
case "386":
7272
arch.Arch = "386"
7373
default:

0 commit comments

Comments
 (0)