wafer-cli 0.2.23__py3-none-any.whl → 0.2.24__py3-none-any.whl

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.py CHANGED
@@ -4975,6 +4975,55 @@ def workspaces_sync(
4975
4975
  raise typer.Exit(1) from None
4976
4976
 
4977
4977
 
4978
+ @workspaces_app.command("pull")
4979
+ def workspaces_pull(
4980
+ workspace: str = typer.Argument(..., help="Workspace name or ID"),
4981
+ remote_path: str = typer.Argument(..., help="Remote path in workspace (relative to /workspace or absolute)"),
4982
+ local_path: Path = typer.Argument(
4983
+ Path("."), help="Local destination path (default: current directory)"
4984
+ ),
4985
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Show [wafer] status messages"),
4986
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress [wafer] status messages"),
4987
+ ) -> None:
4988
+ """Pull files from workspace to local machine.
4989
+
4990
+ Uses rsync over SSH to download files from the workspace's /workspace directory.
4991
+
4992
+ Examples:
4993
+ wafer workspaces pull dev kernel.py ./ # Pull single file
4994
+ wafer workspaces pull dev kernel.py ./my_kernel.py # Pull and rename
4995
+ wafer workspaces pull dev /workspace/results ./ # Pull directory
4996
+ """
4997
+ from .global_config import get_preferences
4998
+ from .workspaces import pull_files
4999
+
5000
+ # Determine verbosity based on mode
5001
+ prefs = get_preferences()
5002
+ if quiet:
5003
+ show_status = False
5004
+ elif verbose:
5005
+ show_status = True
5006
+ else:
5007
+ show_status = prefs.mode == "explicit"
5008
+
5009
+ if show_status:
5010
+ typer.echo(f"[wafer] Pulling {remote_path} from workspace {workspace}...", err=True)
5011
+
5012
+ def on_progress(msg: str) -> None:
5013
+ if show_status:
5014
+ typer.echo(f"[wafer] {msg}", err=True)
5015
+
5016
+ try:
5017
+ file_count = pull_files(
5018
+ workspace, remote_path, local_path.resolve(), on_progress=on_progress
5019
+ )
5020
+ if show_status:
5021
+ typer.echo(f"[wafer] Pulled {file_count} files to {local_path}", err=True)
5022
+ except RuntimeError as e:
5023
+ typer.echo(f"Error: {e}", err=True)
5024
+ raise typer.Exit(1) from None
5025
+
5026
+
4978
5027
  # =============================================================================
4979
5028
  # Target operations commands (exec/ssh/sync)
4980
5029
  # =============================================================================
wafer/evaluate.py CHANGED
@@ -2801,15 +2801,6 @@ if torch.cuda.is_available():
2801
2801
  gc.collect()
2802
2802
  torch.cuda.empty_cache()
2803
2803
  torch.cuda.reset_peak_memory_stats()
2804
-
2805
- # Enable TF32 for fair benchmarking against reference kernels.
2806
- # PyTorch 1.12+ disables TF32 for matmul by default, which handicaps
2807
- # reference kernels using cuBLAS. We enable it so reference kernels
2808
- # run at their best performance (using tensor cores when applicable).
2809
- # This ensures speedup comparisons are against optimized baselines.
2810
- torch.backends.cuda.matmul.allow_tf32 = True
2811
- torch.backends.cudnn.allow_tf32 = True
2812
- print("[KernelBench] TF32 enabled for fair benchmarking")
2813
2804
 
2814
2805
 
2815
2806
  def _calculate_timing_stats(times: list[float]) -> dict:
wafer/wevin_cli.py CHANGED
@@ -205,10 +205,22 @@ def _get_session_preview(session: object) -> str:
205
205
  return ""
206
206
 
207
207
 
208
+ def _get_log_file_path() -> Path:
209
+ """Get user-specific log file path, creating directory if needed.
210
+
211
+ Uses ~/.wafer/logs/ to avoid permission issues with shared /tmp.
212
+ """
213
+ log_dir = Path.home() / ".wafer" / "logs"
214
+ log_dir.mkdir(parents=True, exist_ok=True)
215
+ return log_dir / "wevin_debug.log"
216
+
217
+
208
218
  def _setup_logging() -> None:
209
219
  """Configure logging to file only (no console spam)."""
210
220
  import logging.config
211
221
 
222
+ log_file = _get_log_file_path()
223
+
212
224
  logging.config.dictConfig({
213
225
  "version": 1,
214
226
  "disable_existing_loggers": False,
@@ -220,7 +232,7 @@ def _setup_logging() -> None:
220
232
  "handlers": {
221
233
  "file": {
222
234
  "class": "logging.handlers.RotatingFileHandler",
223
- "filename": "/tmp/wevin_debug.log",
235
+ "filename": str(log_file),
224
236
  "maxBytes": 10_000_000,
225
237
  "backupCount": 3,
226
238
  "formatter": "json",
wafer/workspaces.py CHANGED
@@ -542,6 +542,102 @@ def sync_files(
542
542
  return file_count, warning
543
543
 
544
544
 
545
+ def pull_files(
546
+ workspace_id: str,
547
+ remote_path: str,
548
+ local_path: Path,
549
+ on_progress: Callable[[str], None] | None = None,
550
+ ) -> int:
551
+ """Pull files from workspace to local via rsync over SSH.
552
+
553
+ Args:
554
+ workspace_id: Workspace ID or name
555
+ remote_path: Remote path in workspace (relative to /workspace or absolute)
556
+ local_path: Local destination path
557
+ on_progress: Optional callback for progress messages
558
+
559
+ Returns:
560
+ Number of files transferred
561
+
562
+ Raises:
563
+ RuntimeError: If rsync fails or workspace not accessible
564
+ """
565
+ import subprocess
566
+
567
+ def emit(msg: str) -> None:
568
+ if on_progress:
569
+ on_progress(msg)
570
+
571
+ assert workspace_id, "Workspace ID must be non-empty"
572
+
573
+ ws = get_workspace_raw(workspace_id)
574
+ workspace_status = ws.get("status")
575
+ assert workspace_status in VALID_STATUSES, (
576
+ f"Workspace {workspace_id} has invalid status '{workspace_status}'. "
577
+ f"Valid statuses: {VALID_STATUSES}"
578
+ )
579
+ if workspace_status == "error":
580
+ raise RuntimeError(
581
+ f"Workspace provisioning failed. Delete and recreate:\n"
582
+ f" wafer workspaces delete {workspace_id}\n"
583
+ f" wafer workspaces create {ws.get('name', workspace_id)} --wait"
584
+ )
585
+ if workspace_status != "running":
586
+ raise RuntimeError(
587
+ f"Workspace is {workspace_status}. Wait for it to be running before pulling files."
588
+ )
589
+ ssh_host = ws.get("ssh_host")
590
+ ssh_port = ws.get("ssh_port")
591
+ ssh_user = ws.get("ssh_user")
592
+ if not ssh_host or not ssh_port or not ssh_user:
593
+ raise RuntimeError(
594
+ f"Workspace is running but SSH not ready.\n"
595
+ f" Delete and recreate: wafer workspaces delete {workspace_id}\n"
596
+ f" Then: wafer workspaces create {ws.get('name', workspace_id)} --wait"
597
+ )
598
+ assert isinstance(ssh_port, int) and ssh_port > 0, "Workspace missing valid ssh_port"
599
+
600
+ # Normalize remote path - if not absolute, assume relative to /workspace
601
+ if not remote_path.startswith("/"):
602
+ remote_path = f"/workspace/{remote_path}"
603
+
604
+ # Build SSH command for rsync
605
+ ssh_opts = f"-p {ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
606
+
607
+ # Build rsync command (reverse of sync - from remote to local)
608
+ rsync_cmd = [
609
+ "rsync",
610
+ "-avz",
611
+ "-e",
612
+ f"ssh {ssh_opts}",
613
+ f"{ssh_user}@{ssh_host}:{remote_path}",
614
+ str(local_path),
615
+ ]
616
+
617
+ emit(f"Pulling {remote_path} from workspace...")
618
+
619
+ try:
620
+ result = subprocess.run(rsync_cmd, capture_output=True, text=True)
621
+ if result.returncode != 0:
622
+ raise RuntimeError(f"rsync failed: {result.stderr}")
623
+
624
+ # Count files from rsync output
625
+ lines = result.stdout.strip().split("\n")
626
+ file_count = sum(
627
+ 1
628
+ for line in lines
629
+ if line and not line.startswith((" ", "sent", "total", "receiving", "building"))
630
+ )
631
+
632
+ except FileNotFoundError:
633
+ raise RuntimeError("rsync not found. Install rsync to use pull feature.") from None
634
+ except subprocess.SubprocessError as e:
635
+ raise RuntimeError(f"Pull failed: {e}") from e
636
+
637
+ emit(f"Pulled {file_count} files")
638
+ return file_count
639
+
640
+
545
641
  def _init_sync_state(workspace_id: str) -> str | None:
546
642
  """Tell API to sync files from bare metal to Modal volume.
547
643
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wafer-cli
3
- Version: 0.2.23
3
+ Version: 0.2.24
4
4
  Summary: CLI tool for running commands on remote GPUs and GPU kernel optimization agent
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: typer>=0.12.0
@@ -5,10 +5,10 @@ wafer/api_client.py,sha256=i_Az2b2llC3DSW8yOL-BKqa7LSKuxOr8hSN40s-oQXY,6313
5
5
  wafer/auth.py,sha256=dwss_se5P-FFc9IN38q4kh_dBrA6k-CguDBkivgcdj0,14003
6
6
  wafer/autotuner.py,sha256=41WYP41pTDvMijv2h42vm89bcHtDMJXObDlWmn6xpFU,44416
7
7
  wafer/billing.py,sha256=jbLB2lI4_9f2KD8uEFDi_ixLlowe5hasC0TIZJyIXRg,7163
8
- wafer/cli.py,sha256=j4ODOVT_r-kyc21YOI8Yl8bkiZMGuqDpXRs7CvpNaek,261443
8
+ wafer/cli.py,sha256=tk7wnmwZiqlOT39E3EVVwgD-2Y0eAfLSAbif7IpLijY,263288
9
9
  wafer/config.py,sha256=h5Eo9_yfWqWGoPNdVQikI9GoZVUeysunSYiixf1mKcw,3411
10
10
  wafer/corpus.py,sha256=oQegXA43MuyRvYxOsWhmqeP5vMb5IKFHOvM-1RcahPA,22301
11
- wafer/evaluate.py,sha256=SxxhiPkO6aDdfktRzJXpbWMVmIGn_gw-o5C6Zwj2zRc,190930
11
+ wafer/evaluate.py,sha256=I0JwipvkkOIiiYaIyqe-HMSfYdTPkUtJXvs_1w_gJSQ,190413
12
12
  wafer/global_config.py,sha256=fhaR_RU3ufMksDmOohH1OLeQ0JT0SDW1hEip_zaP75k,11345
13
13
  wafer/gpu_run.py,sha256=TwqXy72T7f2I7e6n5WWod3xgxCPnDhU0BgLsB4CUoQY,9716
14
14
  wafer/inference.py,sha256=tZCO5i05FKY27ewis3CSBHFBeFbXY3xwj0DSjdoMY9s,4314
@@ -26,16 +26,16 @@ wafer/target_lock.py,sha256=SDKhNzv2N7gsphGflcNni9FE5YYuAMuEthngAJEo4Gs,7809
26
26
  wafer/targets.py,sha256=9r-iRWoKSH5cQl1LcamaX-T7cNVOg99ngIm_hlRk-qU,26922
27
27
  wafer/targets_ops.py,sha256=jN1oIBx0mutxRNE9xpIc7SaBxPkVmOyus2eqn0kEKNI,21475
28
28
  wafer/tracelens.py,sha256=g9ZIeFyNojZn4uTd3skPqIrRiL7aMJOz_-GOd3aiyy4,7998
29
- wafer/wevin_cli.py,sha256=Nuk7zTCiJrnpmYtdg5Hu0NbzONCqs54xtON6K7AVB9U,23189
30
- wafer/workspaces.py,sha256=iUdioK7kA3z_gOTMNVDn9Q87c6qpkdXF4bOhJWkUPg8,32375
29
+ wafer/wevin_cli.py,sha256=UwRibTYmIR99EjDupoyFm8gjhMs0vHXTkS4wQuiskho,23537
30
+ wafer/workspaces.py,sha256=k_iCZ-mOrG2KiTXqqcZ5_VifSIXsFGaZM4hjnxBnBmc,35666
31
31
  wafer/skills/wafer-guide/SKILL.md,sha256=KWetJw2TVTbz11_nzqazqOJWWRlbHRFShs4sOoreiWo,3255
32
32
  wafer/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  wafer/templates/ask_docs.py,sha256=Lxs-faz9v5m4Qa4NjF2X_lE8KwM9ES9MNJkxo7ep56o,2256
34
34
  wafer/templates/optimize_kernel.py,sha256=OvZgN5tm_OymO3lK8Dr0VO48e-5PfNVIIoACrPxpmqk,2446
35
35
  wafer/templates/optimize_kernelbench.py,sha256=aoOA13zWEl89r6QW03xF9NKxQ7j4mWe9rwua6-mlr4Y,4780
36
36
  wafer/templates/trace_analyze.py,sha256=XE1VqzVkIUsZbXF8EzQdDYgg-AZEYAOFpr6B_vnRELc,2880
37
- wafer_cli-0.2.23.dist-info/METADATA,sha256=LS8QcHjtver7DSzO75TAfJWH2H7gNswXzAp7Av4_OCU,560
38
- wafer_cli-0.2.23.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
39
- wafer_cli-0.2.23.dist-info/entry_points.txt,sha256=WqB7hB__WhtPY8y1cO2sZiUz7fCq6Ik-usAigpeFvWE,41
40
- wafer_cli-0.2.23.dist-info/top_level.txt,sha256=2MK1IVMWfpLL8BZCQ3E9aG6L6L666gSA_teYlwan4fs,6
41
- wafer_cli-0.2.23.dist-info/RECORD,,
37
+ wafer_cli-0.2.24.dist-info/METADATA,sha256=zoGLJsbN_xgLNwaGRnUll2HY545hFXoBYwYtzeGmlvo,560
38
+ wafer_cli-0.2.24.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
39
+ wafer_cli-0.2.24.dist-info/entry_points.txt,sha256=WqB7hB__WhtPY8y1cO2sZiUz7fCq6Ik-usAigpeFvWE,41
40
+ wafer_cli-0.2.24.dist-info/top_level.txt,sha256=2MK1IVMWfpLL8BZCQ3E9aG6L6L666gSA_teYlwan4fs,6
41
+ wafer_cli-0.2.24.dist-info/RECORD,,