wafer-cli 0.2.8__tar.gz → 0.2.10__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/PKG-INFO +1 -1
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/README.md +41 -2
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/pyproject.toml +2 -1
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_cli_coverage.py +125 -33
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_cli_parity_integration.py +1 -1
- wafer_cli-0.2.10/tests/test_kernel_scope_cli.py +620 -0
- wafer_cli-0.2.10/tests/test_nsys_analyze.py +293 -0
- wafer_cli-0.2.10/tests/test_nsys_profile.py +157 -0
- wafer_cli-0.2.10/tests/test_output.py +263 -0
- wafer_cli-0.2.10/tests/test_skill_commands.py +231 -0
- wafer_cli-0.2.10/tests/test_targets_ops.py +206 -0
- wafer_cli-0.2.10/tests/test_wevin_cli.py +660 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/GUIDE.md +18 -7
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/api_client.py +4 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/auth.py +85 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/cli.py +2339 -404
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/corpus.py +158 -32
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/evaluate.py +1232 -201
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/gpu_run.py +5 -1
- wafer_cli-0.2.10/wafer/kernel_scope.py +554 -0
- wafer_cli-0.2.10/wafer/nsys_analyze.py +1042 -0
- wafer_cli-0.2.10/wafer/nsys_profile.py +511 -0
- wafer_cli-0.2.10/wafer/output.py +241 -0
- wafer_cli-0.2.10/wafer/problems.py +357 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/skills/wafer-guide/SKILL.md +13 -0
- wafer_cli-0.2.10/wafer/ssh_keys.py +261 -0
- wafer_cli-0.2.10/wafer/target_lock.py +270 -0
- wafer_cli-0.2.10/wafer/targets.py +842 -0
- wafer_cli-0.2.10/wafer/targets_ops.py +718 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/wevin_cli.py +129 -18
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/workspaces.py +282 -182
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/PKG-INFO +1 -1
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/SOURCES.txt +13 -1
- wafer_cli-0.2.8/tests/test_isa_cli.py +0 -212
- wafer_cli-0.2.8/tests/test_wevin_cli.py +0 -68
- wafer_cli-0.2.8/wafer/nsys_analyze.py +0 -212
- wafer_cli-0.2.8/wafer/targets.py +0 -352
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/setup.cfg +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_analytics.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_billing.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_config_integration.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_file_operations_integration.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_rocprof_compute_integration.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_ssh_integration.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/tests/test_workflow_integration.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/__init__.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/analytics.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/autotuner.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/billing.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/config.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/global_config.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/inference.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/ncu_analyze.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/rocprof_compute.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/rocprof_sdk.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/rocprof_systems.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/templates/__init__.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/templates/ask_docs.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/templates/optimize_kernel.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/templates/trace_analyze.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer/tracelens.py +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/dependency_links.txt +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/entry_points.txt +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/requires.txt +0 -0
- {wafer_cli-0.2.8 → wafer_cli-0.2.10}/wafer_cli.egg-info/top_level.txt +0 -0
|
@@ -43,10 +43,16 @@ wafer remote-run --upload-dir ./my_code -- python3 train.py
|
|
|
43
43
|
|
|
44
44
|
Create and manage persistent GPU environments.
|
|
45
45
|
|
|
46
|
+
**Available GPUs:**
|
|
47
|
+
|
|
48
|
+
- `MI300X` - AMD Instinct MI300X (192GB HBM3, ROCm)
|
|
49
|
+
- `B200` - NVIDIA Blackwell B200 (180GB HBM3e, CUDA) - default
|
|
50
|
+
|
|
46
51
|
```bash
|
|
47
52
|
wafer workspaces list
|
|
48
|
-
wafer workspaces create my-workspace
|
|
49
|
-
wafer workspaces
|
|
53
|
+
wafer workspaces create my-workspace --gpu B200 --wait # NVIDIA B200
|
|
54
|
+
wafer workspaces create amd-dev --gpu MI300X # AMD MI300X
|
|
55
|
+
wafer workspaces ssh <workspace-id>
|
|
50
56
|
wafer workspaces delete <workspace-id>
|
|
51
57
|
```
|
|
52
58
|
|
|
@@ -197,6 +203,39 @@ Now you can tab-complete:
|
|
|
197
203
|
|
|
198
204
|
---
|
|
199
205
|
|
|
206
|
+
## AI Assistant Skills
|
|
207
|
+
|
|
208
|
+
Install the Wafer CLI skill to make wafer commands discoverable by your AI coding assistant:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Install for all supported tools (Claude Code, Codex CLI, Cursor)
|
|
212
|
+
wafer skill install
|
|
213
|
+
|
|
214
|
+
# Install for a specific tool
|
|
215
|
+
wafer skill install -t cursor # Cursor
|
|
216
|
+
wafer skill install -t claude # Claude Code
|
|
217
|
+
wafer skill install -t codex # Codex CLI
|
|
218
|
+
|
|
219
|
+
# Check installation status
|
|
220
|
+
wafer skill status
|
|
221
|
+
|
|
222
|
+
# Uninstall
|
|
223
|
+
wafer skill uninstall
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Installing from GitHub (Cursor)
|
|
227
|
+
|
|
228
|
+
You can also install the skill directly from GitHub in Cursor:
|
|
229
|
+
|
|
230
|
+
1. Open Cursor Settings (Cmd+Shift+J / Ctrl+Shift+J)
|
|
231
|
+
2. Navigate to **Rules** → **Add Rule** → **Remote Rule (Github)**
|
|
232
|
+
3. Enter: `https://github.com/wafer-ai/skills`
|
|
233
|
+
4. Cursor will automatically discover skills in `.cursor/skills/`
|
|
234
|
+
|
|
235
|
+
The skill provides comprehensive guidance for GPU kernel development, including documentation lookup, trace analysis, kernel evaluation, and optimization workflows.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
200
239
|
## Requirements
|
|
201
240
|
|
|
202
241
|
- Python 3.10+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "wafer-cli"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.10"
|
|
4
4
|
description = "CLI tool for running commands on remote GPUs and GPU kernel optimization agent"
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
dependencies = [
|
|
@@ -78,6 +78,7 @@ ignore = [
|
|
|
78
78
|
[tool.ruff.lint.per-file-ignores]
|
|
79
79
|
"tests/**/*.py" = ["ANN201"] # Don't require return type annotations in tests
|
|
80
80
|
"wafer/evaluate.py" = ["PLR0915", "PLR1702", "E402"] # complex deployment flows - TODO: refactor
|
|
81
|
+
"wafer/output.py" = ["ANN401"] # Output collector uses **kwargs for flexible event data
|
|
81
82
|
|
|
82
83
|
[tool.ruff.lint.pylint]
|
|
83
84
|
max-args = 7 # Max function arguments (Tiger Style: few parameters)
|
|
@@ -61,7 +61,7 @@ def _cleanup_test_workspaces() -> None:
|
|
|
61
61
|
return
|
|
62
62
|
for ws in workspaces:
|
|
63
63
|
if ws.get("name", "").startswith("test-"):
|
|
64
|
-
runner.invoke(app, ["workspaces", "delete", ws["id"]])
|
|
64
|
+
runner.invoke(app, ["workspaces", "delete", ws["id"], "-y"])
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
@pytest.fixture(scope="session", autouse=True)
|
|
@@ -213,7 +213,7 @@ class TestWorkspacesCommands:
|
|
|
213
213
|
finally:
|
|
214
214
|
# Delete
|
|
215
215
|
if ws_id:
|
|
216
|
-
result = runner.invoke(app, ["workspaces", "delete", ws_id])
|
|
216
|
+
result = runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
|
|
217
217
|
|
|
218
218
|
def test_workspaces_create_pretty(self) -> None:
|
|
219
219
|
"""Create workspace with pretty output."""
|
|
@@ -228,53 +228,43 @@ class TestWorkspacesCommands:
|
|
|
228
228
|
return
|
|
229
229
|
|
|
230
230
|
# Extract ID from output
|
|
231
|
-
# Output format: "
|
|
231
|
+
# Output format: "Creating workspace: <name> (<uuid>)"
|
|
232
232
|
match = re.search(r"\(([a-f0-9-]{36})\)", result.output)
|
|
233
233
|
if match:
|
|
234
234
|
ws_id = match.group(1)
|
|
235
|
+
assert "Creating workspace:" in result.output
|
|
235
236
|
assert ws_id is not None, f"Could not extract workspace ID from: {result.output}"
|
|
236
237
|
|
|
237
238
|
finally:
|
|
238
239
|
if ws_id:
|
|
239
|
-
runner.invoke(app, ["workspaces", "delete", ws_id])
|
|
240
|
+
runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
|
|
240
241
|
|
|
241
|
-
def
|
|
242
|
-
"""
|
|
243
|
-
ws_name = f"test-
|
|
242
|
+
def test_workspaces_create_wait_json(self) -> None:
|
|
243
|
+
"""Create workspace with --wait JSON output."""
|
|
244
|
+
ws_name = f"test-create-wait-{os.getpid()}"
|
|
244
245
|
ws_id = None
|
|
245
246
|
|
|
246
247
|
try:
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
result = runner.invoke(
|
|
249
|
+
app,
|
|
250
|
+
["workspaces", "create", ws_name, "--wait", "--json"],
|
|
251
|
+
)
|
|
249
252
|
if result.exit_code != 0:
|
|
250
253
|
if "auth" in result.output.lower() or "not authenticated" in result.output.lower():
|
|
251
254
|
pytest.skip("Not authenticated")
|
|
255
|
+
assert "error" in result.output.lower() or "no gpu" in result.output.lower()
|
|
252
256
|
return
|
|
253
257
|
|
|
254
258
|
data = json.loads(result.stdout)
|
|
255
|
-
ws_id = data.get("
|
|
259
|
+
ws_id = data.get("workspace_id")
|
|
256
260
|
assert ws_id is not None
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
if result.exit_code == 0:
|
|
261
|
-
attach_data = json.loads(result.stdout)
|
|
262
|
-
# Should have SSH connection info
|
|
263
|
-
assert "ssh_host" in attach_data
|
|
264
|
-
assert "ssh_port" in attach_data
|
|
265
|
-
assert "ssh_user" in attach_data
|
|
266
|
-
assert "private_key_pem" in attach_data
|
|
267
|
-
else:
|
|
268
|
-
# May fail with 503 if no GPU available - that's ok
|
|
269
|
-
assert (
|
|
270
|
-
"503" in result.output
|
|
271
|
-
or "No GPU" in result.output
|
|
272
|
-
or "error" in result.output.lower()
|
|
273
|
-
)
|
|
261
|
+
assert "ssh_host" in data
|
|
262
|
+
assert "ssh_port" in data
|
|
263
|
+
assert "ssh_user" in data
|
|
274
264
|
|
|
275
265
|
finally:
|
|
276
266
|
if ws_id:
|
|
277
|
-
runner.invoke(app, ["workspaces", "delete", ws_id])
|
|
267
|
+
runner.invoke(app, ["workspaces", "delete", ws_id, "-y"])
|
|
278
268
|
|
|
279
269
|
def test_workspaces_show_not_found(self) -> None:
|
|
280
270
|
"""Show workspace with invalid ID returns 404."""
|
|
@@ -288,10 +278,10 @@ class TestWorkspacesCommands:
|
|
|
288
278
|
or "auth" in result.output.lower()
|
|
289
279
|
)
|
|
290
280
|
|
|
291
|
-
def
|
|
292
|
-
"""
|
|
281
|
+
def test_workspaces_ssh_not_found(self) -> None:
|
|
282
|
+
"""SSH to workspace with invalid ID returns 404."""
|
|
293
283
|
fake_id = "00000000-0000-0000-0000-000000000000"
|
|
294
|
-
result = runner.invoke(app, ["workspaces", "
|
|
284
|
+
result = runner.invoke(app, ["workspaces", "ssh", fake_id])
|
|
295
285
|
if result.exit_code != 0:
|
|
296
286
|
# Should be 404 or auth error
|
|
297
287
|
assert (
|
|
@@ -300,6 +290,15 @@ class TestWorkspacesCommands:
|
|
|
300
290
|
or "auth" in result.output.lower()
|
|
301
291
|
)
|
|
302
292
|
|
|
293
|
+
def test_workspaces_exec_option_order(self) -> None:
|
|
294
|
+
"""Exec should reject options after workspace name."""
|
|
295
|
+
result = runner.invoke(
|
|
296
|
+
app,
|
|
297
|
+
["workspaces", "exec", "dev", "--timeout", "10", "--", "echo", "hi"],
|
|
298
|
+
)
|
|
299
|
+
assert result.exit_code != 0
|
|
300
|
+
assert "options must come before the workspace name" in result.output.lower()
|
|
301
|
+
|
|
303
302
|
|
|
304
303
|
class TestNsysAnalyzeCommand:
|
|
305
304
|
"""Test wafer nvidia nsys analyze command."""
|
|
@@ -333,7 +332,24 @@ class TestNsysAnalyzeCommand:
|
|
|
333
332
|
result = runner.invoke(
|
|
334
333
|
app, ["nvidia", "nsys", "analyze", str(real_nsys_file), "--remote", "--json"]
|
|
335
334
|
)
|
|
336
|
-
|
|
335
|
+
if result.exit_code != 0:
|
|
336
|
+
combined = result.output.lower()
|
|
337
|
+
if "auth" in combined or "login" in combined or "401" in combined or "403" in combined:
|
|
338
|
+
pytest.skip("Not authenticated")
|
|
339
|
+
if (
|
|
340
|
+
"no gpu" in combined
|
|
341
|
+
or "no targets" in combined
|
|
342
|
+
or "unavailable" in combined
|
|
343
|
+
or "timed out" in combined
|
|
344
|
+
or "timeout" in combined
|
|
345
|
+
or "connect" in combined
|
|
346
|
+
or "service" in combined
|
|
347
|
+
or "billing" in combined
|
|
348
|
+
or "spend limit" in combined
|
|
349
|
+
or "402" in combined
|
|
350
|
+
):
|
|
351
|
+
pytest.skip(f"Remote nsys unavailable: {result.output.strip()}")
|
|
352
|
+
assert result.exit_code == 0
|
|
337
353
|
data = json.loads(result.stdout)
|
|
338
354
|
assert "kernels" in data or "timeline" in data or "summary" in data
|
|
339
355
|
|
|
@@ -344,7 +360,24 @@ class TestNsysAnalyzeCommand:
|
|
|
344
360
|
pytest.skip("No real .nsys-rep fixture available")
|
|
345
361
|
|
|
346
362
|
result = runner.invoke(app, ["nvidia", "nsys", "analyze", str(real_nsys_file), "--remote"])
|
|
347
|
-
|
|
363
|
+
if result.exit_code != 0:
|
|
364
|
+
combined = result.output.lower()
|
|
365
|
+
if "auth" in combined or "login" in combined or "401" in combined or "403" in combined:
|
|
366
|
+
pytest.skip("Not authenticated")
|
|
367
|
+
if (
|
|
368
|
+
"no gpu" in combined
|
|
369
|
+
or "no targets" in combined
|
|
370
|
+
or "unavailable" in combined
|
|
371
|
+
or "timed out" in combined
|
|
372
|
+
or "timeout" in combined
|
|
373
|
+
or "connect" in combined
|
|
374
|
+
or "service" in combined
|
|
375
|
+
or "billing" in combined
|
|
376
|
+
or "spend limit" in combined
|
|
377
|
+
or "402" in combined
|
|
378
|
+
):
|
|
379
|
+
pytest.skip(f"Remote nsys unavailable: {result.output.strip()}")
|
|
380
|
+
assert result.exit_code == 0
|
|
348
381
|
assert "NSYS" in result.output or "Kernel" in result.output or "Profile" in result.output
|
|
349
382
|
|
|
350
383
|
def test_nsys_analyze_local(self, real_nsys_file: Path | None) -> None:
|
|
@@ -627,3 +660,62 @@ class TestEvaluateCommand:
|
|
|
627
660
|
or "error" in combined
|
|
628
661
|
or result.exit_code in (0, 1)
|
|
629
662
|
)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class TestWorkspacesExecFlagPassthrough:
|
|
666
|
+
"""Test that flags after -- are passed through to the command, not intercepted."""
|
|
667
|
+
|
|
668
|
+
def test_exec_passes_dash_i_flag(self) -> None:
|
|
669
|
+
"""The -i flag should pass through, not be intercepted as --image."""
|
|
670
|
+
result = runner.invoke(app, [
|
|
671
|
+
"workspaces", "exec", "test-ws", "--", "echo", "-i", "test"
|
|
672
|
+
])
|
|
673
|
+
# Should NOT fail with "no such option" or similar parse error
|
|
674
|
+
assert "no such option" not in result.output.lower()
|
|
675
|
+
assert "unrecognized" not in result.output.lower()
|
|
676
|
+
# May fail with workspace not found - that's fine, parsing succeeded
|
|
677
|
+
|
|
678
|
+
def test_exec_passes_dash_v_flag(self) -> None:
|
|
679
|
+
"""The -v flag should pass through, not be intercepted as --verbose."""
|
|
680
|
+
result = runner.invoke(app, [
|
|
681
|
+
"workspaces", "exec", "test-ws", "--", "python", "-v", "-c", "print(1)"
|
|
682
|
+
])
|
|
683
|
+
assert "no such option" not in result.output.lower()
|
|
684
|
+
|
|
685
|
+
def test_exec_passes_double_dash_help(self) -> None:
|
|
686
|
+
"""--help after -- should pass to command, not show exec help."""
|
|
687
|
+
result = runner.invoke(app, [
|
|
688
|
+
"workspaces", "exec", "test-ws", "--", "python", "--help"
|
|
689
|
+
])
|
|
690
|
+
# Should NOT show the exec command's help text
|
|
691
|
+
assert "Execute a command in workspace" not in result.output
|
|
692
|
+
|
|
693
|
+
def test_exec_passes_multiple_flags(self) -> None:
|
|
694
|
+
"""Multiple flags like -i -v -n should all pass through."""
|
|
695
|
+
result = runner.invoke(app, [
|
|
696
|
+
"workspaces", "exec", "test-ws", "--", "grep", "-i", "-v", "-n", "pattern"
|
|
697
|
+
])
|
|
698
|
+
assert "no such option" not in result.output.lower()
|
|
699
|
+
|
|
700
|
+
def test_exec_no_command_shows_error(self) -> None:
|
|
701
|
+
"""Missing command after -- should show helpful error."""
|
|
702
|
+
result = runner.invoke(app, [
|
|
703
|
+
"workspaces", "exec", "test-ws", "--"
|
|
704
|
+
])
|
|
705
|
+
assert result.exit_code != 0
|
|
706
|
+
assert "no command" in result.output.lower() or "error" in result.output.lower()
|
|
707
|
+
|
|
708
|
+
def test_exec_preserves_flag_order(self) -> None:
|
|
709
|
+
"""Flags should be preserved in order they were given."""
|
|
710
|
+
# This tests the shlex.join behavior
|
|
711
|
+
result = runner.invoke(app, [
|
|
712
|
+
"workspaces", "exec", "test-ws", "--", "cmd", "-a", "1", "-b", "2"
|
|
713
|
+
])
|
|
714
|
+
assert "no such option" not in result.output.lower()
|
|
715
|
+
|
|
716
|
+
def test_exec_with_equals_syntax(self) -> None:
|
|
717
|
+
"""Flags with --flag=value syntax should pass through."""
|
|
718
|
+
result = runner.invoke(app, [
|
|
719
|
+
"workspaces", "exec", "test-ws", "--", "cmd", "--output=/tmp/out"
|
|
720
|
+
])
|
|
721
|
+
assert "no such option" not in result.output.lower()
|