mtr-cli 0.2.0__tar.gz → 0.3.0__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.
Files changed (31) hide show
  1. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/PKG-INFO +16 -6
  2. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/README.md +15 -5
  3. mtr_cli-0.3.0/docs/intro-to-mtr.md +180 -0
  4. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/cli.py +53 -6
  5. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/config.py +14 -0
  6. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/sync.py +157 -25
  7. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/pyproject.toml +1 -1
  8. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_config.py +56 -0
  9. mtr_cli-0.3.0/tests/unit/test_sync_rsync.py +368 -0
  10. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_sync_sftp.py +28 -0
  11. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/uv.lock +1 -1
  12. mtr_cli-0.2.0/tests/unit/test_sync_rsync.py +0 -142
  13. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/.gitignore +0 -0
  14. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/.pre-commit-config.yaml +0 -0
  15. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/.python-version +0 -0
  16. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/AGENTS.md +0 -0
  17. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/LICENSE +0 -0
  18. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/examples/config.yaml +0 -0
  19. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/__init__.py +0 -0
  20. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/logger.py +0 -0
  21. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/mtr/ssh.py +0 -0
  22. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/__init__.py +0 -0
  23. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/conftest.py +0 -0
  24. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/integration/__init__.py +0 -0
  25. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/integration/test_cli_flow.py +0 -0
  26. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/integration/test_cli_phase1.py +0 -0
  27. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/__init__.py +0 -0
  28. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_logger.py +0 -0
  29. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_ssh.py +0 -0
  30. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_ssh_interactive.py +0 -0
  31. {mtr_cli-0.2.0 → mtr_cli-0.3.0}/tests/unit/test_ssh_pre_cmd.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtr-cli
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A CLI tool for seamless local development and remote execution on GPU servers.
5
5
  Project-URL: Homepage, https://github.com/lecoan/mtremote
6
6
  Project-URL: Repository, https://github.com/lecoan/mtremote
@@ -57,11 +57,21 @@ pip install mtr-cli
57
57
 
58
58
  MTRemote 需要以下系统命令:
59
59
 
60
- | 命令 | 用途 | 安装方式 |
61
- |------|------|----------|
62
- | `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` |
63
- | `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 |
64
- | `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) |
60
+ | 命令 | 用途 | 安装方式 | 版本要求 |
61
+ |------|------|----------|----------|
62
+ | `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` | - |
63
+ | `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 | **≥ 3.1.0** (TTY 进度显示需要) |
64
+ | `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) | - |
65
+
66
+ **注意**:macOS 自带的 rsync 版本较旧(2.6.9),不支持 TTY 模式下的进度显示。建议通过 Homebrew 安装新版:
67
+
68
+ ```bash
69
+ # macOS 用户建议升级 rsync
70
+ brew install rsync
71
+
72
+ # 验证版本
73
+ rsync --version # 应显示 3.1.0 或更高版本
74
+ ```
65
75
 
66
76
  **注意**:交互式 Shell 功能(如 `mtr bash`, `mtr ipython`)**必须**安装 `ssh`。密码认证**必须**安装 `sshpass`。
67
77
 
@@ -30,11 +30,21 @@ pip install mtr-cli
30
30
 
31
31
  MTRemote 需要以下系统命令:
32
32
 
33
- | 命令 | 用途 | 安装方式 |
34
- |------|------|----------|
35
- | `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` |
36
- | `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 |
37
- | `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) |
33
+ | 命令 | 用途 | 安装方式 | 版本要求 |
34
+ |------|------|----------|----------|
35
+ | `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` | - |
36
+ | `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 | **≥ 3.1.0** (TTY 进度显示需要) |
37
+ | `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) | - |
38
+
39
+ **注意**:macOS 自带的 rsync 版本较旧(2.6.9),不支持 TTY 模式下的进度显示。建议通过 Homebrew 安装新版:
40
+
41
+ ```bash
42
+ # macOS 用户建议升级 rsync
43
+ brew install rsync
44
+
45
+ # 验证版本
46
+ rsync --version # 应显示 3.1.0 或更高版本
47
+ ```
38
48
 
39
49
  **注意**:交互式 Shell 功能(如 `mtr bash`, `mtr ipython`)**必须**安装 `ssh`。密码认证**必须**安装 `sshpass`。
40
50
 
@@ -0,0 +1,180 @@
1
+ # mtr-cli:远程训练框架开发的工作流优化
2
+
3
+ ## 背景与问题
4
+
5
+ 在训练框架开发过程中,我们面临一个结构性矛盾:代码开发在本地进行,但功能验证必须在远程 GPU/NPU 集群上完成。这种"本地-远程"的割裂带来了三个核心问题。
6
+
7
+ ### 网络与环境隔离
8
+
9
+ 训练集群通常部署在内网或隔离环境中,无法直接访问外部网络。这导致依赖安装困难,而训练框架的依赖链本身就很复杂(PyTorch、DeepSpeed、Megatron 等)。环境配置耗时较长,一旦集群环境需要重建,成本很高。
10
+
11
+ ### 多集群切换成本高
12
+
13
+ 框架开发需要在 GPU 集群和 NPU 集群之间频繁切换验证。每次切换涉及代码同步、路径检查、环境确认等重复性操作。虽然启动方式统一使用 torchrun,但不同集群的节点地址、Python 环境路径等配置存在差异,需要反复适配。
14
+
15
+ ### AI Agent 集成受限
16
+
17
+ 训练任务在远程集群执行时,代码报错和异常堆栈输出在远程终端。AI Agent 无法直接访问这些信息,开发者需要手动复制粘贴错误内容,打断开发流程。Agent 无法实时感知执行状态,失去了即时辅助调试的能力。
18
+
19
+ ## 解决方案:mtr-cli
20
+
21
+ mtr-cli 采用"本地开发,远程执行"的架构,在保持本地开发体验的同时,实现与远程集群的无缝集成。
22
+
23
+ ### 核心功能
24
+
25
+ **智能代码同步**
26
+
27
+ 支持 rsync(增量同步,速度快)和 SFTP(兼容性好)两种引擎。自动过滤 .git、__pycache__ 等无需同步的文件,避免大数据集误传。
28
+
29
+ **多集群配置管理**
30
+
31
+ 通过配置文件集中管理多个训练集群:
32
+
33
+ ```yaml
34
+ servers:
35
+ gpu-cluster:
36
+ host: "gpu-node-01"
37
+ user: "dev"
38
+ remote_dir: "/data/train-project"
39
+
40
+ npu-cluster:
41
+ host: "npu-node-01"
42
+ user: "dev"
43
+ remote_dir: "/home/dev/project"
44
+ ```
45
+
46
+ 集群切换通过命令行参数完成:
47
+
48
+ ```bash
49
+ # GPU 集群执行
50
+ mtr -s gpu-cluster torchrun --nproc_per_node=8 train.py
51
+
52
+ # NPU 集群执行
53
+ mtr -s npu-cluster torchrun --nproc_per_node=8 train.py
54
+ ```
55
+
56
+ **实时流式输出**
57
+
58
+ 远程训练日志实时回显至本地终端,包括 loss 曲线、吞吐量、显存占用等关键指标。代码异常时,堆栈信息直接显示在本地,AI Agent 可即时获取并分析。支持交互式命令(vim、ipython 等)。
59
+
60
+ ## 应用场景
61
+
62
+ **分布式训练框架开发**
63
+
64
+ 在本地完成数据并行或模型并行逻辑的实现后,需要上多卡环境验证。传统流程涉及打包代码、scp 传输、ssh 登录、路径定位、执行运行等多个步骤。使用 mtr-cli 可简化为:
65
+
66
+ ```bash
67
+ mtr torchrun --nproc_per_node=8 train.py
68
+ ```
69
+
70
+ 代码自动同步,远程执行,日志实时回传。
71
+
72
+ **跨硬件平台验证**
73
+
74
+ 框架在 GPU 环境验证通过后,需要在 NPU 环境测试兼容性。传统方式需要重复环境配置和代码同步流程。使用 mtr-cli 只需切换服务器参数:
75
+
76
+ ```bash
77
+ mtr -s npu-cluster torchrun --nproc_per_node=8 train.py
78
+ ```
79
+
80
+ **即时调试**
81
+
82
+ 训练脚本执行过程中发生异常,报错信息和堆栈直接输出在本地终端。AI Agent 实时获取错误内容,可立即进行分析和建议,无需手动复制粘贴。
83
+
84
+ ## 收益分析
85
+
86
+ - **时间效率**:消除手动 scp/rsync 和反复 ssh 登录的操作 overhead
87
+ - **认知负担**:本地维护单一的代码库和配置,远程仅作为计算资源
88
+ - **AI 辅助能力**:Agent 可获取完整执行日志,恢复实时调试辅助
89
+ - **流程一致性**:屏蔽 GPU/NPU 后端差异,提供统一的开发体验
90
+
91
+ ## 快速开始
92
+
93
+ ### 安装
94
+
95
+ 推荐使用 uv 安装(更快、更可靠):
96
+
97
+ ```bash
98
+ uv pip install mtr-cli
99
+ ```
100
+
101
+ 或使用 pip:
102
+
103
+ ```bash
104
+ pip install mtr-cli
105
+ ```
106
+
107
+ ### 初始化配置
108
+
109
+ 在项目目录下运行初始化命令,生成默认配置文件:
110
+
111
+ ```bash
112
+ mtr --init
113
+ ```
114
+
115
+ 这会创建 `.mtr/config.yaml` 文件,包含默认配置模板。根据你的集群信息编辑该文件:
116
+
117
+ ```yaml
118
+ servers:
119
+ dev-gpu:
120
+ host: "192.168.1.10"
121
+ user: "dev"
122
+ key_filename: "~/.ssh/id_rsa"
123
+ remote_dir: "/data/project"
124
+ sync: "rsync"
125
+ ```
126
+
127
+ ### 执行命令
128
+
129
+ 配置完成后,在项目根目录执行:
130
+
131
+ ```bash
132
+ # 使用默认服务器
133
+ mtr python train.py
134
+
135
+ # 指定服务器
136
+ mtr -s dev-gpu torchrun --nproc_per_node=8 train.py
137
+ ```
138
+
139
+ 完整 workflow:
140
+
141
+ ```bash
142
+ # 1. 安装
143
+ uv pip install mtr-cli
144
+
145
+ # 2. 进入项目目录
146
+ cd my-project
147
+
148
+ # 3. 初始化配置
149
+ mtr --init
150
+ # 编辑 .mtr/config.yaml 添加服务器信息
151
+
152
+ # 4. 运行
153
+ mtr python train.py
154
+ ```
155
+
156
+ ### 常用参数
157
+
158
+ ```bash
159
+ # 指定服务器
160
+ mtr -s gpu-cluster python train.py
161
+
162
+ # 跳过代码同步(仅执行命令)
163
+ mtr --no-sync python train.py
164
+
165
+ # 预览将要执行的命令(不实际运行)
166
+ mtr --dry-run python train.py
167
+
168
+ # 强制禁用 TTY(用于日志记录场景)
169
+ mtr --no-tty python train.py
170
+
171
+ # 启用日志记录
172
+ mtr --enable-log python train.py
173
+
174
+ # 从远程下载文件
175
+ mtr --get /remote/path/to/file.txt --to ./local/file.txt
176
+ ```
177
+
178
+ ---
179
+
180
+ mtr-cli 的目标是将远程训练的日常操作自动化,使开发者能够专注于框架本身,而非环境切换的繁琐流程。
@@ -15,6 +15,11 @@ defaults:
15
15
  # 选项: "rsync" (推荐), "sftp"
16
16
  sync: "rsync"
17
17
 
18
+ # 是否尊重 .gitignore 文件(仅 rsync 模式支持)
19
+ # 设置为 true 时,rsync 会自动读取项目根目录的 .gitignore 并排除匹配的文件
20
+ # SFTP 模式不支持此选项,如启用会报错
21
+ respect_gitignore: true
22
+
18
23
  exclude:
19
24
  - ".git/"
20
25
  - "__pycache__/"
@@ -186,6 +191,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
186
191
  # Resolve exclude
187
192
  exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
188
193
 
194
+ # Get respect_gitignore setting
195
+ respect_gitignore = config.get_respect_gitignore()
196
+
189
197
  # Determine engine
190
198
  engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
191
199
 
@@ -199,6 +207,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
199
207
  password=password,
200
208
  port=port,
201
209
  exclude=exclude,
210
+ respect_gitignore=respect_gitignore,
202
211
  )
203
212
  elif engine == "sftp":
204
213
  syncer = SftpSyncer(
@@ -210,6 +219,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
210
219
  password=password,
211
220
  port=port,
212
221
  exclude=exclude,
222
+ respect_gitignore=respect_gitignore,
213
223
  )
214
224
  else:
215
225
  click.secho(
@@ -225,11 +235,28 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
225
235
  logger.info(f"[DryRun] Would sync {local_dir} -> {remote_dir}", module="mtr.sync")
226
236
  else:
227
237
  if is_interactive and console:
228
- with console.status("[bold blue]Syncing code...", spinner="dots"):
229
- syncer.sync()
238
+ # TTY mode: single line real-time update using Rich Live
239
+ from rich.live import Live
240
+ from rich.text import Text
241
+
242
+ with Live(Text("Starting sync...", style="blue"), refresh_per_second=10) as live:
243
+
244
+ def show_sync_progress(filename):
245
+ # Get relative path for cleaner display
246
+ rel_path = os.path.relpath(filename, local_dir)
247
+ live.update(Text(f"Syncing: {rel_path}", style="blue"))
248
+
249
+ syncer.sync(show_progress=True, progress_callback=show_sync_progress)
250
+ live.update(Text("Sync completed!", style="green"))
230
251
  else:
252
+ # no_tty mode: print each file on new line
253
+ def show_sync_progress(filename):
254
+ rel_path = os.path.relpath(filename, local_dir)
255
+ click.echo(f"Syncing: {rel_path}")
256
+
231
257
  click.secho("Syncing code...", fg="blue")
232
- syncer.sync()
258
+ syncer.sync(show_progress=True, progress_callback=show_sync_progress)
259
+ click.secho("Sync completed!", fg="green")
233
260
  logger.info(f"Sync completed: {local_dir} -> {remote_dir}", module="mtr.sync")
234
261
  except SyncError as e:
235
262
  logger.error(f"Sync failed: {e}", module="mtr.sync")
@@ -260,6 +287,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
260
287
  # Resolve exclude
261
288
  exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
262
289
 
290
+ # Get respect_gitignore setting
291
+ respect_gitignore = config.get_respect_gitignore()
292
+
263
293
  # Determine engine
264
294
  engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
265
295
 
@@ -273,6 +303,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
273
303
  password=password,
274
304
  port=port,
275
305
  exclude=exclude,
306
+ respect_gitignore=respect_gitignore,
276
307
  )
277
308
  elif engine == "sftp":
278
309
  syncer = SftpSyncer(
@@ -284,6 +315,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
284
315
  password=password,
285
316
  port=port,
286
317
  exclude=exclude,
318
+ respect_gitignore=respect_gitignore,
287
319
  )
288
320
  else:
289
321
  click.secho(
@@ -298,12 +330,27 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
298
330
  logger.info(f"[DryRun] Would download {remote_get_path} -> {local_dest}", module="mtr.sync")
299
331
  else:
300
332
  if is_interactive and console:
301
- with console.status(f"[bold blue]Downloading {remote_get_path}...", spinner="dots"):
302
- syncer.download(remote_get_path, local_dest)
333
+ # TTY mode: single line real-time update using Rich Live
334
+ from rich.live import Live
335
+ from rich.text import Text
336
+
337
+ with Live(Text("Starting download...", style="blue"), refresh_per_second=10) as live:
338
+
339
+ def show_download_progress(filename):
340
+ live.update(Text(f"Downloading: {filename}", style="blue"))
341
+
342
+ syncer.download(
343
+ remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress
344
+ )
345
+ live.update(Text("Download completed!", style="green"))
303
346
  console.print(f"✅ [green]Downloaded:[/green] {remote_get_path} -> {local_dest}")
304
347
  else:
348
+ # no_tty mode: print each file on new line
349
+ def show_download_progress(filename):
350
+ click.echo(f"Downloading: {filename}")
351
+
305
352
  click.secho(f"Downloading {remote_get_path}...", fg="blue")
306
- syncer.download(remote_get_path, local_dest)
353
+ syncer.download(remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress)
307
354
  click.secho(f"Download completed: {local_dest}", fg="green")
308
355
  logger.info(f"Download completed: {remote_get_path} -> {local_dest}", module="mtr.sync")
309
356
  except SyncError as e:
@@ -19,6 +19,20 @@ class Config:
19
19
  server_config: Dict[str, Any]
20
20
  global_defaults: Dict[str, Any]
21
21
 
22
+ def get_respect_gitignore(self) -> bool:
23
+ """Get respect_gitignore setting, default True.
24
+
25
+ Priority: server config > global defaults > True (default)
26
+ """
27
+ # Check server config first
28
+ if "respect_gitignore" in self.server_config:
29
+ return self.server_config["respect_gitignore"]
30
+ # Then check global defaults
31
+ if "respect_gitignore" in self.global_defaults:
32
+ return self.global_defaults["respect_gitignore"]
33
+ # Default to True
34
+ return True
35
+
22
36
 
23
37
  class ConfigLoader:
24
38
  def __init__(self, config_path: Optional[str] = None):