@samitouri / QOSAMI-WSL / commits / 58139c50

Implement GPU support (#40371)

* Save state * Merge remote-tracking branch 'origin/feature/wsl-for-apps' into user/oneblue/wslc-gpu * Save state * Save state * Change --gpu flag to --gpus all for GPU container support - Rename --gpu (boolean flag) to --gpus (value argument) matching Docker CLI - Only accept 'all' as value (case-insensitive); display localized error otherwise - Add argument validation in ArgumentValidation.cpp (early rejection) - Add GPU LD_LIBRARY_PATH tests for containers (set, pre-existing, trailing colon) - Add GPU LD_LIBRARY_PATH tests for exec on GPU containers - Add CLI argument validation unit tests for --gpus Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Improve tests * Add SDK test coverage for GPU container support Validate that containers created via the WSLC SDK with both session (WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU) and container (WSLC_CONTAINER_FLAG_ENABLE_GPU) flags have: - /dev/dxg character device available - GPU drivers directory mounted at /usr/lib/wsl/drivers - GPU libraries directory mounted at /usr/lib/wsl/lib - LD_LIBRARY_PATH set correctly for init and exec processes - LD_LIBRARY_PATH appended when pre-existing value is provided - No double colon when pre-existing value has trailing colon Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add SDK test coverage * Simplify tests * Apply PR feedback * Fix e2e HelpCommand tests for --gpus rename Add --gpus option to expected help output in container create and run e2e tests. The option was renamed from --gpu (flag) to --gpus (value). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Update tests * Apply PR suggestions * Update localization --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Blue committed May 1, 2026 at 10:40 UTC 58139c50ed8c5d6dde31f7fe2081f456def401e0
22 files changed +302 -10
localization/strings/en-US/Resources.resw
+8
@@ -2571,6 +2571,14 @@ On first run, creates the file with all settings commented out at their defaults
2571 <data name="WSLCCLI_ForwardArgsDescription" xml:space="preserve">
2572 <value>Arguments to pass to container's init process</value>
2573 </data>
2574 + <data name="WSLCCLI_GpusArgDescription" xml:space="preserve">
2575 + <value>Add GPU devices to the container ('all' to pass all GPUs)</value>
2576 + <comment>{Locked="all"}</comment>
2577 + </data>
2578 + <data name="WSLCCLI_GpusInvalidValue" xml:space="preserve">
2579 + <value>Invalid {} value: '{}'. Only 'all' is supported.</value>
2580 + <comment>{FixedPlaceholder="{}"}Command line arguments, file names and string inserts should not be translated</comment>
2581 + </data>
2582 <data name="WSLCCLI_ImageForceArgDescription" xml:space="preserve">
2583 <value>Delete images even if they are being used</value>
2584 </data>
src/windows/inc/docker_schema.h
+12 -2
@@ -186,6 +186,15 @@ struct Mount
186 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Mount, Name, Target, Source, Type, ReadOnly);
187 };
188
189 +struct DeviceMapping
190 +{
191 + std::string PathOnHost;
192 + std::string PathInContainer;
193 + std::string CgroupPermissions;
194 +
195 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(DeviceMapping, PathOnHost, PathInContainer, CgroupPermissions);
196 +};
197 +
198 struct PortMapping
199 {
200 std::string HostIp;
@@ -205,8 +214,9 @@ struct HostConfig
214 std::optional<std::vector<std::string>> DnsOptions;
215 std::optional<std::vector<std::string>> Binds;
216 std::map<std::string, std::string> Tmpfs;
217 + std::vector<DeviceMapping> Devices;
218
209 - NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HostConfig, Mounts, PortBindings, NetworkMode, Init, Dns, DnsSearch, DnsOptions, Binds, Tmpfs);
219 + NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(HostConfig, Mounts, PortBindings, NetworkMode, Init, Dns, DnsSearch, DnsOptions, Binds, Tmpfs, Devices);
220 };
221
222 struct CreateContainer
@@ -521,4 +531,4 @@ struct CreateImageProgress
531 NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(CreateImageProgress, status, id, progressDetail, errorDetail);
532 };
533
524 -} // namespace wsl::windows::common::docker_schema
\ No newline at end of file
534 +} // namespace wsl::windows::common::docker_schema
src/windows/service/exe/WSLCSessionManager.cpp
+3 -1
@@ -100,7 +100,9 @@ private:
100 Settings.MaximumStorageSizeMb = userSettings.Get<settings::Setting::SessionStorageSizeMb>();
101 Settings.BootTimeoutMs = wsl::windows::wslc::DefaultBootTimeoutMs;
102 Settings.NetworkingMode = userSettings.Get<settings::Setting::SessionNetworkingMode>();
103 - Settings.FeatureFlags = WslcFeatureFlagsNone;
103 +
104 + // TODO: Add a config setting to opt-out of GPU support.
105 + Settings.FeatureFlags = WslcFeatureFlagsGPU;
106 WI_SetFlagIf(Settings.FeatureFlags, WslcFeatureFlagsDnsTunneling, userSettings.Get<settings::Setting::SessionDnsTunneling>());
107 WI_SetFlagIf(
108 Settings.FeatureFlags,
src/windows/service/inc/wslc.idl
+2 -2
@@ -243,8 +243,8 @@ typedef struct _WSLCContainerNetwork
243 typedef enum _WSLCContainerFlags
244 {
245 WSLCContainerFlagsNone = 0,
246 - WSLCContainerFlagsRm = 1, // Delete the container when it exits. TODO: Implement.
247 - WSLCContainerFlagsGpu = 2, // Enable GPU access. TODO: implement.
246 + WSLCContainerFlagsRm = 1, // Delete the container when it exits.
247 + WSLCContainerFlagsGpu = 2, // Enable GPU access.
248 WSLCContainerFlagsInit = 4, // Run the container under an init process.
249 WSLCContainerFlagsPublishAll = 8, // Publish all exposed ports.
250 } WSLCContainerFlags;
src/windows/wslc/arguments/ArgumentDefinitions.h
+1
@@ -56,6 +56,7 @@ _(File, "file", L"f", Kind::Value, L
56 _(Follow, "follow", L"f", Kind::Flag, Localization::WSLCCLI_FollowArgDescription()) \
57 _(Format, "format", NO_ALIAS, Kind::Value, Localization::WSLCCLI_FormatArgDescription()) \
58 _(ForwardArgs, "arguments", NO_ALIAS, Kind::Forward, Localization::WSLCCLI_ForwardArgsDescription()) \
59 +_(Gpus, "gpus", NO_ALIAS, Kind::Value, Localization::WSLCCLI_GpusArgDescription()) \
60 /*_(GroupId, "groupid", NO_ALIAS, Kind::Value, Localization::WSLCCLI_GroupIdArgDescription())*/ \
61 _(Help, "help", WSLC_CLI_HELP_ARG, Kind::Flag, Localization::WSLCCLI_HelpArgDescription()) \
62 _(Hostname, "hostname", L"h", Kind::Value, Localization::WSLCCLI_HostnameArgDescription()) \
src/windows/wslc/arguments/ArgumentValidation.cpp
+15
@@ -44,6 +44,10 @@ void Argument::Validate(const ArgMap& execArgs) const
44 validation::ValidateIntegerFromString<LONGLONG>(execArgs.GetAll<ArgType::Time>(), m_name);
45 break;
46
47 + case ArgType::Gpus:
48 + validation::ValidateGpus(execArgs.GetAll<ArgType::Gpus>(), m_name);
49 + break;
50 +
51 case ArgType::Volume:
52 validation::ValidateVolumeMount(execArgs.GetAll<ArgType::Volume>());
53 break;
@@ -191,4 +195,15 @@ InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstri
195 }
196 }
197
198 +void ValidateGpus(const std::vector<std::wstring>& values, const std::wstring& argName)
199 +{
200 + for (const auto& value : values)
201 + {
202 + if (!IsEqual(value, L"all"))
203 + {
204 + throw ArgumentException(Localization::WSLCCLI_GpusInvalidValue(argName, value));
205 + }
206 + }
207 +}
208 +
209 } // namespace wsl::windows::wslc::validation
src/windows/wslc/arguments/ArgumentValidation.h
+1
@@ -63,6 +63,7 @@ FormatType GetFormatTypeFromString(const std::wstring& input, const std::wstring
63
64 InspectType GetInspectTypeFromString(const std::wstring& input, const std::wstring& argName);
65
66 +void ValidateGpus(const std::vector<std::wstring>& values, const std::wstring& argName);
67 void ValidateVolumeMount(const std::vector<std::wstring>& values);
68
69 } // namespace wsl::windows::wslc::validation
\ No newline at end of file
src/windows/wslc/commands/ContainerCreateCommand.cpp
+1
@@ -41,6 +41,7 @@ std::vector<Argument> ContainerCreateCommand::GetArguments() const
41 Argument::Create(ArgType::Env, false, NO_LIMIT),
42 Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
43 // Argument::Create(ArgType::GroupId),
44 + Argument::Create(ArgType::Gpus),
45 Argument::Create(ArgType::Hostname),
46 Argument::Create(ArgType::Interactive),
47 Argument::Create(ArgType::Label, false, NO_LIMIT),
src/windows/wslc/commands/ContainerRunCommand.cpp
+1
@@ -41,6 +41,7 @@ std::vector<Argument> ContainerRunCommand::GetArguments() const
41 Argument::Create(ArgType::Entrypoint),
42 Argument::Create(ArgType::Env, false, NO_LIMIT),
43 Argument::Create(ArgType::EnvFile, false, NO_LIMIT),
44 + Argument::Create(ArgType::Gpus),
45 Argument::Create(ArgType::Hostname),
46 Argument::Create(ArgType::Interactive),
47 Argument::Create(ArgType::Label, false, NO_LIMIT),
src/windows/wslc/services/ContainerModel.h
+1
@@ -37,6 +37,7 @@ struct ContainerOptions
37 bool Remove = false;
38 bool TTY = false;
39 bool PublishAll = false;
40 + bool Gpu = false;
41 std::vector<std::string> Ports;
42 std::vector<std::wstring> Volumes;
43 std::string WorkingDirectory;
src/windows/wslc/services/ContainerService.cpp
+1
@@ -46,6 +46,7 @@ static wsl::windows::common::RunningWSLCContainer CreateInternal(Session& sessio
46 auto containerFlags = WSLCContainerFlagsNone;
47 WI_SetFlagIf(containerFlags, WSLCContainerFlagsRm, options.Remove);
48 WI_SetFlagIf(containerFlags, WSLCContainerFlagsPublishAll, options.PublishAll);
49 + WI_SetFlagIf(containerFlags, WSLCContainerFlagsGpu, options.Gpu);
50
51 wsl::windows::common::WSLCContainerLauncher containerLauncher(
52 image, options.Name, options.Arguments, options.EnvironmentVariables, WSLCContainerNetworkTypeBridged, processFlags);
src/windows/wslc/tasks/ContainerTasks.cpp
+5
@@ -261,6 +261,11 @@ void SetContainerOptionsFromArgs(CLIExecutionContext& context)
261 options.PublishAll = true;
262 }
263
264 + if (context.Args.Contains(ArgType::Gpus))
265 + {
266 + options.Gpu = true;
267 + }
268 +
269 if (context.Args.Contains(ArgType::Volume))
270 {
271 auto volumes = context.Args.GetAll<ArgType::Volume>();
src/windows/wslcsession/WSLCContainer.cpp
+49
@@ -375,6 +375,29 @@ void ValidateNamedVolumes(
375 }
376 }
377
378 +void ConfigureLdPathForGpu(std::vector<std::string>& Env)
379 +{
380 + static constexpr std::string_view ldLibraryPathPrefix = "LD_LIBRARY_PATH=";
381 + auto it = std::ranges::find_if(Env, [](const std::string& e) { return e.starts_with(ldLibraryPathPrefix); });
382 +
383 + if (it != Env.end())
384 + {
385 + // If the user already has an LD_LIBRARY_PATH, append the GPU library paths to it.
386 + auto ldPath = it->substr(ldLibraryPathPrefix.size());
387 + if (!ldPath.empty() && !ldPath.ends_with(":"))
388 + {
389 + it->append(":");
390 + }
391 +
392 + it->append(WSLCVirtualMachine::c_gpuLibrariesPath);
393 + }
394 + else
395 + {
396 + // Otherwise create a new entry.
397 + Env.emplace_back(std::format("LD_LIBRARY_PATH={}", WSLCVirtualMachine::c_gpuLibrariesPath));
398 + }
399 +}
400 +
401 } // namespace
402
403 ContainerPortMapping::ContainerPortMapping(VMPortMapping&& VmMapping, uint16_t ContainerPort) :
@@ -996,6 +1019,11 @@ void WSLCContainerImpl::Exec(const WSLCProcessOptions* Options, LPCSTR DetachKey
1019 request.DetachKeys = DetachKeys;
1020 }
1021
1022 + if (WI_IsFlagSet(m_containerFlags, WSLCContainerFlagsGpu))
1023 + {
1024 + ConfigureLdPathForGpu(request.Env);
1025 + }
1026 +
1027 try
1028 {
1029 auto result = m_dockerClient.CreateExec(m_id, request);
@@ -1319,6 +1347,27 @@ std::unique_ptr<WSLCContainerImpl> WSLCContainerImpl::Create(
1347
1348 ProcessNamedVolumes(containerOptions, sessionVolumes, request);
1349
1350 + // Configure GPU support if requested.
1351 + if (WI_IsFlagSet(containerOptions.Flags, WSLCContainerFlagsGpu))
1352 + {
1353 + THROW_HR_IF_MSG(
1354 + HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED),
1355 + !virtualMachine.FeatureEnabled(WslcFeatureFlagsGPU),
1356 + "WSLCContainerFlagsGpu requires GPU support enabled on the session");
1357 +
1358 + if (!request.HostConfig.Binds.has_value())
1359 + {
1360 + request.HostConfig.Binds = std::vector<std::string>{};
1361 + }
1362 +
1363 + request.HostConfig.Binds->push_back(std::format("{0}:{0}:ro", WSLCVirtualMachine::c_gpuLibrariesPath));
1364 + request.HostConfig.Binds->push_back(std::format("{0}:{0}:ro", WSLCVirtualMachine::c_gpuDriversPath));
1365 +
1366 + request.HostConfig.Devices.push_back({"/dev/dxg", "/dev/dxg", "rwm"});
1367 +
1368 + ConfigureLdPathForGpu(request.Env);
1369 + }
1370 +
1371 // Prepare port mappings from container options.
1372 std::vector<_WSLCPortMapping> ports;
1373 for (ULONG i = 0; i < containerOptions.PortsCount; i++)
src/windows/wslcsession/WSLCVirtualMachine.cpp
+1 -1
@@ -293,7 +293,7 @@ void WSLCVirtualMachine::Initialize()
293 Mount(m_initChannel, modulesDevice.c_str(), "", "ext4", "ro", WSLC_MOUNT::KernelModules);
294
295 // Configure GPU mounts if enabled
296 - MountGpuLibraries("/usr/lib/wsl/lib", "/usr/lib/wsl/drivers");
296 + MountGpuLibraries(c_gpuLibrariesPath, c_gpuDriversPath);
297
298 // Configure cold discard hint size for page reporting.
299 // This sets the minimum order of pages that will be reported as free to the hypervisor.
src/windows/wslcsession/WSLCVirtualMachine.h
+5 -2
@@ -109,6 +109,9 @@ private:
109 class WSLCVirtualMachine
110 {
111 public:
112 + static inline const char* c_gpuLibrariesPath = "/usr/lib/wsl/lib";
113 + static inline const char* c_gpuDriversPath = "/usr/lib/wsl/drivers";
114 +
115 struct ConnectedSocket
116 {
117 int Fd = -1;
@@ -162,6 +165,8 @@ public:
165 return m_vmId;
166 }
167
168 + bool FeatureEnabled(WSLCFeatureFlags Flag) const;
169 +
170 private:
171 void MapRelayPort(_In_ int Family, _In_ unsigned short WindowsPort, _In_ unsigned short LinuxPort, _In_ bool Remove);
172
@@ -178,8 +183,6 @@ private:
183 int* Errno = nullptr,
184 const TPrepareCommandLine& PrepareCommandLine = [](const auto&) {});
185
181 - bool FeatureEnabled(WSLCFeatureFlags Flag) const;
182 -
186 std::tuple<int32_t, int32_t, wsl::shared::SocketChannel> Fork(
187 wsl::shared::SocketChannel& Channel, enum WSLC_FORK::ForkType Type, ULONG TtyRows = 0, ULONG TtyColumns = 0);
188 int32_t ExpectClosedChannelOrError(wsl::shared::SocketChannel& Channel);
test/windows/WSLCTests.cpp
+78
@@ -3331,6 +3331,84 @@ class WSLCTests
3331 }
3332 }
3333
3334 + WSLC_TEST_METHOD(ContainerGpu)
3335 + {
3336 +
3337 + // Validate that setting the GPU flag on a non-GPU session fails.
3338 + {
3339 + WSLCContainerLauncher launcher("debian:latest", "test-container-gpu-fail");
3340 + launcher.SetContainerFlags(WSLCContainerFlagsGpu);
3341 +
3342 + auto [hr, _] = launcher.LaunchNoThrow(*m_defaultSession);
3343 + VERIFY_ARE_EQUAL(hr, HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
3344 + }
3345 +
3346 + auto restore = ResetTestSession();
3347 +
3348 + auto settings = GetDefaultSessionSettings(L"container-gpu-test", true);
3349 + WI_SetFlag(settings.FeatureFlags, WslcFeatureFlagsGPU);
3350 +
3351 + auto session = CreateSession(settings);
3352 +
3353 + // Validate that the GPU is correctly configured for containers init process.
3354 + {
3355 + WSLCContainerLauncher launcher(
3356 + "debian:latest", "test-container-init-gpu", {"/bin/sh", "-c", "test -c /dev/dxg && echo $LD_LIBRARY_PATH"});
3357 + launcher.SetContainerFlags(WSLCContainerFlagsGpu);
3358 +
3359 + auto container = launcher.Launch(*session);
3360 +
3361 + ValidateContainerOutput(container, {{1, "/usr/lib/wsl/lib\n"}}, 0);
3362 + }
3363 +
3364 + // Validate that GPU resources are available inside a container when WSLCContainerFlagsGpu is set.
3365 + {
3366 + WSLCContainerLauncher launcher("debian:latest", "test-container-gpu", {"sleep", "99999"});
3367 + launcher.SetContainerFlags(WSLCContainerFlagsGpu);
3368 +
3369 + auto container = launcher.Launch(*session);
3370 +
3371 + auto expect = [&](const std::vector<std::string> command,
3372 + int exitCode,
3373 + const std::map<int, std::string>& expectedOutput = {},
3374 + const std::vector<std::string>& env = {}) {
3375 + auto process = WSLCProcessLauncher({}, command, env).Launch(container.Get());
3376 + ValidateProcessOutput(process, expectedOutput, exitCode);
3377 + };
3378 +
3379 + // Validate that /dev/dxg is available as a character device.
3380 + expect({"/bin/sh", "-c", "test -c /dev/dxg"}, 0);
3381 +
3382 + // Validate that the GPU library directory is mounted and contains libraries.
3383 + expect({"/bin/sh", "-c", "test -d /usr/lib/wsl/lib && ls /usr/lib/wsl/lib | grep -q ."}, 0);
3384 +
3385 + // Validate that the GPU drivers directory is mounted and accessible.
3386 + expect({"/bin/sh", "-c", "test -d /usr/lib/wsl/drivers"}, 0);
3387 +
3388 + // Validate that the GPU mount points are read-only.
3389 + expect({"/usr/bin/touch", "/usr/lib/wsl/lib/test"}, 1);
3390 + expect({"/usr/bin/touch", "/usr/lib/wsl/drivers/test"}, 1);
3391 +
3392 + // Validate that LD_LIBRARY_PATH is set to include the GPU library path.
3393 + expect({"/bin/sh", "-c", "echo $LD_LIBRARY_PATH"}, 0, {{1, "/usr/lib/wsl/lib\n"}});
3394 +
3395 + // Validate that exec with a pre-existing LD_LIBRARY_PATH appends the GPU path.
3396 + expect({"/bin/sh", "-c", "echo $LD_LIBRARY_PATH"}, 0, {{1, "/custom/path:/usr/lib/wsl/lib\n"}}, {"LD_LIBRARY_PATH=/custom/path"});
3397 +
3398 + // Validate that exec with a trailing colon in LD_LIBRARY_PATH doesn't produce a double colon.
3399 + expect({"/bin/sh", "-c", "echo $LD_LIBRARY_PATH"}, 0, {{1, "/custom/path:/usr/lib/wsl/lib\n"}}, {"LD_LIBRARY_PATH=/custom/path:"});
3400 + expect({"/bin/sh", "-c", "echo $LD_LIBRARY_PATH"}, 0, {{1, "/usr/lib/wsl/lib\n"}}, {"LD_LIBRARY_PATH="});
3401 + }
3402 +
3403 + // Validate that containers without the GPU flag do not have GPU resources.
3404 + {
3405 + WSLCContainerLauncher launcher("debian:latest", "test-container-no-gpu", {"/bin/sh", "-c", "test -c /dev/dxg"});
3406 + auto container = launcher.Launch(*session);
3407 +
3408 + ValidateContainerOutput(container, {{1, ""}}, 1);
3409 + }
3410 + }
3411 +
3412 WSLC_TEST_METHOD(Modules)
3413 {
3414 // Sanity check.
test/windows/WslcSdkTests.cpp
+43
@@ -2394,4 +2394,47 @@ class WslcSdkTests
2394 pullOptions.uri = "///invalid-registry-url///";
2395 VERIFY_ARE_EQUAL(WslcPullSessionImage(m_defaultSession, &pullOptions, nullptr), E_INVALIDARG);
2396 }
2397 +
2398 + WSLC_TEST_METHOD(ContainerGpu)
2399 + {
2400 + // Validate that creating a GPU container on a session without GPU support fails.
2401 + {
2402 + WslcContainerSettings containerSettings;
2403 + VERIFY_SUCCEEDED(WslcInitContainerSettings("debian:latest", &containerSettings));
2404 + VERIFY_SUCCEEDED(WslcSetContainerSettingsFlags(&containerSettings, WSLC_CONTAINER_FLAG_ENABLE_GPU));
2405 +
2406 + UniqueContainer container;
2407 + VERIFY_ARE_EQUAL(WslcCreateContainer(m_defaultSession, &containerSettings, &container, nullptr), HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED));
2408 + }
2409 +
2410 + // Create a GPU-enabled session.
2411 + std::filesystem::path gpuStorage = m_storagePath / "wslc-gpu-session-storage";
2412 + auto cleanupStorage = wil::scope_exit_log(WI_DIAGNOSTICS_INFO, [&] {
2413 + std::error_code error;
2414 + std::filesystem::remove_all(gpuStorage, error);
2415 + });
2416 +
2417 + WslcSessionSettings sessionSettings;
2418 + VERIFY_SUCCEEDED(WslcInitSessionSettings(L"wslc-gpu-test", gpuStorage.c_str(), &sessionSettings));
2419 + VERIFY_SUCCEEDED(WslcSetSessionSettingsFeatureFlags(&sessionSettings, WSLC_SESSION_FEATURE_FLAG_ENABLE_GPU));
2420 +
2421 + WslcVhdRequirements vhdReqs{};
2422 + vhdReqs.sizeBytes = 4096ull * 1024 * 1024;
2423 + vhdReqs.type = WSLC_VHD_TYPE_DYNAMIC;
2424 + VERIFY_SUCCEEDED(WslcSetSessionSettingsVhd(&sessionSettings, &vhdReqs));
2425 +
2426 + UniqueSession gpuSession;
2427 + VERIFY_SUCCEEDED(WslcCreateSession(&sessionSettings, &gpuSession, nullptr));
2428 + THROW_IF_FAILED(WslcLoadSessionImageFromFile(gpuSession.get(), GetTestImagePath("debian:latest").c_str(), nullptr, nullptr));
2429 +
2430 + // Validate /dev/dxg is available and LD_LIBRARY_PATH is set via the container init command.
2431 + {
2432 + const char* initArgv[] = {"/bin/sh", "-c", "test -c /dev/dxg && echo $LD_LIBRARY_PATH"};
2433 +
2434 + auto output = RunContainerAndCapture(
2435 + gpuSession.get(), "debian:latest", {initArgv[0], initArgv[1], initArgv[2]}, WSLC_CONTAINER_FLAG_ENABLE_GPU);
2436 +
2437 + VERIFY_ARE_EQUAL(output.stdoutOutput, "/usr/lib/wsl/lib\n");
2438 + }
2439 + }
2440 };
test/windows/wslc/CommandLineTestCases.h
+10
@@ -116,6 +116,16 @@ COMMAND_LINE_TEST_CASE(L"run --dns 1.1.1.1 --dns-search example.com --dns-option
116 COMMAND_LINE_TEST_CASE(L"run --dns", L"run", false) // Missing value for --dns
117 COMMAND_LINE_TEST_CASE(L"run --dns-search", L"run", false) // Missing value for --dns-search
118 COMMAND_LINE_TEST_CASE(L"run --dns-option", L"run", false) // Missing value for --dns-option
119 +// GPU tests for container run
120 +COMMAND_LINE_TEST_CASE(L"run --gpus all ubuntu", L"run", true)
121 +COMMAND_LINE_TEST_CASE(L"container run --gpus all ubuntu sh", L"run", true)
122 +COMMAND_LINE_TEST_CASE(L"run --gpus invalid ubuntu", L"run", false) // Only 'all' is supported
123 +COMMAND_LINE_TEST_CASE(L"run --gpus", L"run", false) // Missing value for --gpus
124 +// GPU tests for container create
125 +COMMAND_LINE_TEST_CASE(L"create --gpus all ubuntu", L"create", true)
126 +COMMAND_LINE_TEST_CASE(L"container create --gpus all ubuntu sh", L"create", true)
127 +COMMAND_LINE_TEST_CASE(L"create --gpus none ubuntu", L"create", false) // Only 'all' is supported
128 +COMMAND_LINE_TEST_CASE(L"create --gpus", L"create", false) // Missing value for --gpus
129 COMMAND_LINE_TEST_CASE(L"exec cont1 echo Hello", L"exec", true)
130 COMMAND_LINE_TEST_CASE(L"exec cont1", L"exec", false) // Missing required command argument
131 COMMAND_LINE_TEST_CASE(L"container exec -it cont1 sh -c \"echo a && echo b\"", L"exec", true) // docker exec example
test/windows/wslc/WSLCCLIArgumentUnitTests.cpp
+8 -1
@@ -139,6 +139,13 @@ class WSLCCLIArgumentUnitTests
139 VERIFY_THROWS(validation::GetFormatTypeFromString(L"xml"), ArgumentException);
140 VERIFY_NO_THROW(validation::ValidateFormatTypeFromString({L"json", L"table"}, L"formatArg"));
141 VERIFY_THROWS(validation::ValidateFormatTypeFromString({L"JSON", L"TABLE", L"csv"}, L"formatArg"), ArgumentException);
142 +
143 + // Verify GPU device argument
144 + VERIFY_NO_THROW(validation::ValidateGpus({L"all"}, L"gpusArg"));
145 + VERIFY_THROWS(validation::ValidateGpus({L"none"}, L"gpusArg"), ArgumentException);
146 + VERIFY_THROWS(validation::ValidateGpus({L"0"}, L"gpusArg"), ArgumentException);
147 + VERIFY_THROWS(validation::ValidateGpus({L"gpu0"}, L"gpusArg"), ArgumentException);
148 + VERIFY_THROWS(validation::ValidateGpus({L""}, L"gpusArg"), ArgumentException);
149 }
150
151 // Test: Verify EnumVariantMap behavior with ArgTypes.
@@ -211,4 +218,4 @@ class WSLCCLIArgumentUnitTests
218 VERIFY_ARE_EQUAL(argsContainer.GetCount(), 0);
219 }
220 };
214 -} // namespace WSLCCLIArgumentUnitTests
\ No newline at end of file
221 +} // namespace WSLCCLIArgumentUnitTests
test/windows/wslc/WSLCCLIExecutionUnitTests.cpp
+55 -1
@@ -284,6 +284,60 @@ class WSLCCLIExecutionUnitTests
284 VERIFY_ARE_EQUAL(std::string("/app"), options.WorkingDirectory);
285 }
286
287 + TEST_METHOD(RunCommand_ParseGpusAll_SetsGpuOption)
288 + {
289 + auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus all ubuntu sh");
290 +
291 + ContainerRunCommand command{L""};
292 + CLIExecutionContext context;
293 + command.ParseArguments(invocation, context.Args);
294 + command.ValidateArguments(context.Args);
295 +
296 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
297 +
298 + const auto& options = context.Data.Get<Data::ContainerOptions>();
299 + VERIFY_IS_TRUE(options.Gpu);
300 + }
301 +
302 + TEST_METHOD(RunCommand_ParseGpusInvalid_ThrowsArgumentException)
303 + {
304 + auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus invalid ubuntu sh");
305 +
306 + ContainerRunCommand command{L""};
307 + CLIExecutionContext context;
308 + command.ParseArguments(invocation, context.Args);
309 +
310 + VERIFY_THROWS_SPECIFIC(
311 + command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
312 + }
313 +
314 + TEST_METHOD(CreateCommand_ParseGpusAll_SetsGpuOption)
315 + {
316 + auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus all ubuntu sh");
317 +
318 + ContainerCreateCommand command{L""};
319 + CLIExecutionContext context;
320 + command.ParseArguments(invocation, context.Args);
321 + command.ValidateArguments(context.Args);
322 +
323 + wsl::windows::wslc::task::SetContainerOptionsFromArgs(context);
324 +
325 + const auto& options = context.Data.Get<Data::ContainerOptions>();
326 + VERIFY_IS_TRUE(options.Gpu);
327 + }
328 +
329 + TEST_METHOD(CreateCommand_ParseGpusInvalid_ThrowsArgumentException)
330 + {
331 + auto invocation = CreateInvocationFromCommandLine(L"wslc --gpus none ubuntu sh");
332 +
333 + ContainerCreateCommand command{L""};
334 + CLIExecutionContext context;
335 + command.ParseArguments(invocation, context.Args);
336 +
337 + VERIFY_THROWS_SPECIFIC(
338 + command.ValidateArguments(context.Args), wsl::windows::wslc::ArgumentException, [](const auto&) { return true; });
339 + }
340 +
341 // Test: Command Line test parsing all cases defined in CommandLineTestCases.h
342 // This test verifies the command line parsing logic used by the CLI and executes the same
343 // code as the CLI up to the point of command execution, including parsing and argument validtion.
@@ -353,4 +407,4 @@ class WSLCCLIExecutionUnitTests
407 }
408 }
409 };
356 -} // namespace WSLCCLIExecutionUnitTests
\ No newline at end of file
410 +} // namespace WSLCCLIExecutionUnitTests
test/windows/wslc/e2e/WSLCE2EContainerCreateTests.cpp
+1
@@ -728,6 +728,7 @@ private:
728 << L" --entrypoint Specifies the container init process executable\r\n"
729 << L" -e,--env Key=Value pairs for environment variables\r\n"
730 << L" --env-file File containing key=value pairs of env variables\r\n"
731 + << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
732 << L" -h,--hostname Container host name\r\n"
733 << L" -i,--interactive Attach to stdin and keep it open\r\n"
734 << L" -l,--label Set metadata on an object\r\n"
test/windows/wslc/e2e/WSLCE2EContainerRunTests.cpp
+1
@@ -724,6 +724,7 @@ private:
724 << L" --entrypoint Specifies the container init process executable\r\n"
725 << L" -e,--env Key=Value pairs for environment variables\r\n"
726 << L" --env-file File containing key=value pairs of env variables\r\n"
727 + << L" --gpus Add GPU devices to the container ('all' to pass all GPUs)\r\n"
728 << L" -h,--hostname Container host name\r\n"
729 << L" -i,--interactive Attach to stdin and keep it open\r\n"
730 << L" -l,--label Set metadata on an object\r\n"