Files
Model-Optimizer/tools/debugger/README.md
T
Chenjie Luo 7c8557158d Add job cancellation support to the debugger command relay (#1262)
## Summary

- Add a `cancel` subcommand to the client that terminates the currently
running command on the server
- Server now runs commands in the background with PID tracking, enabling
cancellation mid-execution
- Client-side timeouts automatically cancel the running command on the
server (previously the server process was left running)
- Hardened against race conditions through 4 rounds of adversarial
review (15 fixes total)

### Key changes

**server.sh:**
- Commands run in background with PID tracked in `$RELAY_DIR/running`
(atomic tmp+mv write)
- Cancel detection loop checks for `$RELAY_DIR/cancel` file with cmd_id
verification
- SIGTERM with 5s grace period, then SIGKILL escalation for stuck
processes
- `.exit` file written before `running` marker removed (ordering
guarantee)
- `set -e`-safe: `wait` uses `|| exit_code=$?` pattern; cleanup trap
fully guarded
- Stale cancel files cleared at command start; mismatched/empty signals
rejected
- Command file read into memory and removed before execution (eliminates
TOCTOU with client timeout)

**client.sh:**
- New `cancel` subcommand: writes target cmd_id to cancel file, waits
for server acknowledgment (30s timeout)
- `run` timeout now sends targeted cancel signal (verifies cmd_id match
to avoid killing wrong command)
- `run` timeout cleans up orphaned result files
- `status` shows currently running command
- `flush` rejects if a command is currently running (prevents state
corruption)
- Exit code validated as numeric before use

### Protocol additions

```
.relay/
├── running    # server writes cmd_id:pid while executing (atomic)
├── cancel     # client writes target cmd_id to request cancellation
```

## Test plan

- [ ] Start server in Docker, handshake from host
- [ ] Run a command (`client.sh run "sleep 30"`), cancel it (`client.sh
cancel`), verify exit code 130
- [ ] Run a command with short timeout (`--timeout 5 run "sleep 30"`),
verify auto-cancel
- [ ] Run a command that exits non-zero, verify server stays alive
- [ ] Run `status` during execution, verify it shows the running command
- [ ] Attempt `flush` during execution, verify it is rejected
- [ ] Cancel when nothing is running, verify clean message

🤖 Generated with [Claude Code](https://claude.com/claude-code)

### Before your PR is "*Ready for review*"

Make sure you read and follow [Contributor
guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md)
and your commits are signed (`git commit -s -S`).

Make sure you read and follow the [Security Best
Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors)
(e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(...,
weights_only=False)`, `pickle`, etc.).

- Is this change backward compatible?: ✅
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: N/A (bash scripts for dev
tooling, tested manually)
- Did you update
[Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
N/A (internal tooling)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Added a comprehensive debug skill guide and protocol reference with
quick CLI examples and a new “Cancelling Commands” section.

* **New Features**
* Client-side `cancel` command to terminate the currently running remote
command.
  * Status now reports active command (or `(idle)`).

* **Improvements**
* Stronger startup/validation guidance, safer shutdown/cleanup,
deterministic cancel exit semantics (130), and auto-cancel on
client-side timeout.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Chenjie Luo <chenjiel@nvidia.com>
2026-04-15 16:49:42 +00:00

4.4 KiB

File-Based Command Relay (Debugger)

A lightweight client/server system for running commands inside a Docker container from the host, using only a shared filesystem — no networking required.

Overview

Host (Claude Code)                      Docker Container
┌─────────────┐                         ┌─────────────────┐
│ client.sh   │  writes cmd file        │ server.sh       │
│   run "X"   │ ───────────────────►    │   detects cmd   │
│             │                         │   executes X    │
│  reads      │  writes result file     │   writes result │
│  result     │ ◄───────────────────    │                 │
└─────────────┘                         └─────────────────┘
          └──── shared filesystem (.relay/) ────┘

Assumptions

  • The ModelOpt repo is accessible from both host and container (e.g., bind-mounted)
  • HuggingFace models are mounted at /hf-local
  • The server auto-detects the repo root from the location of server.sh

Quick Start

1. Start the server (inside Docker)

# The server auto-detects the repo root (two levels up from tools/debugger/)
bash /path/to/modelopt/tools/debugger/server.sh

The server automatically sets the working directory to the repo root. You can override with --workdir.

2. Connect from the host

bash tools/debugger/client.sh handshake

3. Run commands

# Run a simple command
bash tools/debugger/client.sh run "echo hello"

# Run a test script
bash tools/debugger/client.sh run "bash llm_ptq/scripts/huggingface_example.sh"

# Run with a long timeout (default is 600s)
bash tools/debugger/client.sh --timeout 1800 run "python my_long_test.py"

# Cancel a running command
bash tools/debugger/client.sh cancel

# Check status
bash tools/debugger/client.sh status

Protocol

The relay uses a directory at tools/debugger/.relay/ with this structure:

.relay/
├── server.ready      # Written by server on startup
├── client.ready      # Written by client during handshake
├── handshake.done    # Written by server to confirm handshake
├── running           # Written by server while a command is executing (cmd_id:pid)
├── cancel            # Written by client to request cancellation of the running command
├── cmd/              # Client writes command .sh files here
│   └── <id>.sh       # Command to execute
└── result/           # Server writes results here
    ├── <id>.log      # stdout + stderr
    └── <id>.exit     # Exit code

Handshake

  1. Server starts, creates .relay/server.ready
  2. Client writes .relay/client.ready
  3. Server detects it, writes .relay/handshake.done
  4. Both sides are now connected

Command Execution

  1. Client writes a command to .relay/cmd/<id>.sh
  2. Server detects the file, reads the command content, and removes the .sh file
  3. Server runs bash -c <content> in a new process group, writes .relay/running
  4. Server writes .relay/result/<id>.exit and .relay/result/<id>.log, then removes .relay/running
  5. Client reads results and cleans up

Cancellation

  1. Client writes the target cmd_id to .relay/cancel
  2. Server verifies the cmd_id matches, then kills the command's process group
  3. Server writes exit code 130 and removes .relay/running and .relay/cancel
  4. Client-side timeout also triggers cancellation automatically

Options

Server

Flag Default Description
--relay-dir <script_dir>/.relay Relay directory path
--workdir Auto-detected repo root Working directory for commands

Client

Flag Default Description
--relay-dir <script_dir>/.relay Relay directory path
--timeout 600 Seconds to wait for command result

Notes

  • The .relay/ directory is in .gitignore — it is not checked in.
  • Only one server should run at a time (startup clears the relay directory).
  • Commands run sequentially in the order the server discovers them.
  • A running command can be cancelled via client.sh cancel. Cancelled commands exit with code 130.
  • Client-side timeouts automatically cancel the running command on the server.