# Two-machine LAN inference test

This guide proves that distributed inference works across two physical machines: a Linux rig (tracker + first shard) and a Windows 11 / WSL2 rig (second shard). A test script sends real inference requests and validates the output.

## Network topology

```
[Linux machine — 192.168.1.10]
  meshnet-tracker  :8080
  meshnet-node A   :8001   shard 0–19  (tracker-mode, entry point)

[Windows 11 / WSL2 — 192.168.1.20]
  meshnet-node B   :8001   shard 20–39

[Client — either machine]
  scripts/test_lan_inference.py --tracker http://192.168.1.10:8080
```

Adjust the IPs and shard ranges to match your hardware. Use a model that fits (sharded) in both GPUs combined. The example uses `microsoft/Phi-3-medium-128k-instruct` (40 layers, BF16 ~15 GB each shard ~7.5 GB).

---

## Prerequisites

**Both machines:**
- Python 3.11+ with `meshnet-node` installed (see `docs/INSTALL_WINDOWS.md` for Windows)
- Model weights already downloaded (pre-fetch prevents timeout on first startup)
- LAN connectivity verified: `ping 192.168.1.10` from Windows, `ping 192.168.1.20` from Linux

**Linux machine ports open:**

```bash
# ufw (skip if firewall is off)
sudo ufw allow 8080/tcp   # tracker
sudo ufw allow 8001/tcp   # node A
```

**Windows machine port forwarded (WSL2 only):**

```powershell
# Run in PowerShell as Administrator — redo after every WSL restart
$wsl = (wsl hostname -I).Trim()
netsh interface portproxy add v4tov4 listenport=8001 listenaddress=0.0.0.0 connectport=8001 connectaddress=$wsl
New-NetFirewallRule -DisplayName "meshnet-node" -Direction Inbound -Protocol TCP -LocalPort 8001 -Action Allow
```

---

## Start sequence

**Always start in this order: tracker → node A → node B → test.**

### Terminal 1 — Linux: tracker

```bash
meshnet-tracker --port 8080
```

Expected:

```
[tracker] listening on 0.0.0.0:8080
```

### Terminal 2 — Linux: node A (shard 0–19, tracker-mode)

```bash
meshnet-node \
  --model microsoft/Phi-3-medium-128k-instruct \
  --quantization bf16 \
  --shard-start 0 --shard-end 19 \
  --tracker http://localhost:8080 \
  --port 8001 \
  --host 0.0.0.0
```

`shard_start=0` auto-sets `tracker_mode=True` — this node accepts inference requests.

Wait until you see `meshnet-node ready` before continuing.

### Terminal 3 — Windows WSL2: node B (shard 20–39)

```bash
meshnet-node \
  --model microsoft/Phi-3-medium-128k-instruct \
  --quantization bf16 \
  --shard-start 20 --shard-end 39 \
  --tracker http://192.168.1.10:8080 \
  --port 8001 \
  --host 0.0.0.0 \
  --advertise-host 192.168.1.20
```

`--advertise-host` must be the Windows **LAN IP** (not the WSL2 internal 172.x.x.x IP) so the Linux node can reach it.

---

## Verify nodes are registered

From any machine with `curl`:

```bash
# List all registered nodes
curl http://192.168.1.10:8080/v1/nodes

# Check route for the model — should list both node endpoints in order
curl "http://192.168.1.10:8080/v1/route?model=microsoft/Phi-3-medium-128k-instruct"
```

Expected route response:

```json
{
  "route": [
    "http://192.168.1.10:8001",
    "http://192.168.1.20:8001"
  ]
}
```

If only one endpoint appears, node B hasn't registered yet — wait a few seconds and retry.

---

## Run the test script

```bash
# From any machine that can reach the tracker
python3 scripts/test_lan_inference.py \
  --tracker http://192.168.1.10:8080 \
  --gateway http://192.168.1.10:8001
```

Expected output:

```
Inference endpoint: http://192.168.1.10:8001
Tracker: http://192.168.1.10:8080

Route: ['http://192.168.1.10:8001', 'http://192.168.1.20:8001']

[1] Q: What is 7 × 8? Answer in one word.
     A: 56
     3 tokens  2.41s  1.2 t/s

[2] Q: Name the capital of France in one word.
     A: Paris
     2 tokens  1.87s  1.1 t/s

[3] Q: Complete the sequence: 1, 1, 2, 3, 5, ___
     A: 8
     2 tokens  1.93s  1.0 t/s

All 3 requests completed successfully.
Exit code: 0
```

The script exits 0 if all 3 requests complete with valid OpenAI-format responses.

---

## Reading latency from node logs

The node logs show per-hop timing. On node A terminal look for:

```
[node] forwarding to downstream: http://192.168.1.20:8001  (took 1.23s)
```

Approximate breakdown:
- **client → node A (encode + first shard):** full request latency minus the downstream time
- **node A → node B (pipeline):** the `forwarding to downstream` duration
- **node B → node A (tail decode + token):** included in downstream duration

Full end-to-end latency = prompt encode + shard A forward + network transfer + shard B forward + decode.

With LAN latency < 1 ms, the network transfer is negligible. Bottleneck is GPU compute.

---

## Known Issues

**WSL2 IP changes after restart.**
The `netsh portproxy` forwarding rule uses a fixed WSL2 IP. If Windows or WSL2 restarts, the IP changes and the rule breaks. Redo the `netsh` and `New-NetFirewallRule` commands. To automate this, add a Task Scheduler job on WSL start.

**Node B registers with internal WSL2 IP (172.x.x.x) instead of LAN IP.**
Symptom: route response lists `172.x.x.x` and node A cannot reach it.
Fix: always pass `--advertise-host 192.168.1.20` (your Windows LAN IP) when starting node B.

**Model download times out node registration.**
If the model hasn't been pre-fetched, `transformers` downloads it during node startup, which can take 20+ minutes. The tracker heartbeat timeout (90s) will expire, and node A will deregister node B. Pre-download the model weights before starting the node (see `docs/INSTALL_WINDOWS.md` Step 6). Node B re-registers automatically via the heartbeat re-registration loop once it's up.

**`bf16` unsupported on older NVIDIA GPUs.**
GPUs before Ampere (RTX 30xx) have limited bfloat16 support. Use `--quantization int8` on RTX 20xx and earlier.

**Windows Defender blocks inbound connection on WSL2.**
Even with the firewall rule added, Windows Defender SmartScreen or a corporate security policy can block the connection. Verify by checking Windows Event Viewer → Security → Filtering Platform Connection for blocked connections on port 8001.

**Route returns only one node.**
If node B registers but the route only returns one endpoint, check that both nodes use the same `--model` string (full HuggingFace repo path). Route lookup matches on `hf_repo` — a short name vs. full path mismatch causes the node to be excluded.
