File size: 7,950 Bytes
49589ef
3343c67
49589ef
 
3343c67
 
b180e83
49589ef
 
3343c67
49589ef
 
 
3343c67
 
49589ef
 
 
3343c67
 
 
 
 
 
 
 
 
 
 
 
 
49589ef
3343c67
 
 
 
 
49589ef
3343c67
 
 
 
 
 
 
49589ef
 
3343c67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbfe93e
 
3343c67
 
b4b1f9c
3343c67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6e1af37
3343c67
 
 
 
 
 
16381b6
 
5b1ac6a
 
d6978c9
 
 
 
 
3343c67
 
 
 
6fc7b7d
 
 
 
 
3343c67
 
 
 
 
 
 
 
 
 
 
 
 
55a50b2
3343c67
 
6fc7b7d
3343c67
 
 
 
 
 
 
 
 
26e8ba2
 
b180e83
 
26e8ba2
3343c67
 
 
6fc7b7d
 
 
 
 
3343c67
 
 
 
 
 
 
6e1af37
3343c67
 
 
 
 
 
16381b6
 
5b1ac6a
 
d6978c9
 
 
 
 
3343c67
 
 
 
 
 
 
 
 
b180e83
 
3343c67
 
 
 
 
49589ef
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
"""
Launch a PC-SHO-DLM training pod on RunPod using the current REST API.

Usage:
    export RUNPOD_API_KEY=...
    python3 deploy_runpod.py --list
    python3 deploy_runpod.py --create --gpu "<exact RunPod GPU type ID>" --steps 10
"""

import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request


RUNPOD_API = "https://rest.runpod.io/v1"
DEFAULT_IMAGE = "runpod/pytorch:2.1.0-py3.10-cuda11.8.0-devel-ubuntu22.04"
DEFAULT_VOLUME_MOUNT = "/workspace"
DEFAULT_HF_CODE_REPO = "https://huggingface.co/zotowata/pc-sho-dlm-code"


def get_runpod_key() -> str:
    key = os.environ.get("RUNPOD_API_KEY") or os.environ.get("RUNPOD_API_TOKEN")
    if not key:
        raise SystemExit(
            "Missing RunPod API key. Export RUNPOD_API_KEY before using this script."
        )
    return key


def request(method: str, path: str, payload: dict | None = None) -> dict | list:
    key = get_runpod_key()
    url = f"{RUNPOD_API}{path}"
    body = None if payload is None else json.dumps(payload).encode()
    req = urllib.request.Request(
        url,
        data=body,
        method=method,
        headers={
            "Authorization": f"Bearer {key}",
            "Content-Type": "application/json",
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            data = resp.read().decode()
            return json.loads(data) if data else {}
    except urllib.error.HTTPError as e:
        detail = e.read().decode(errors="ignore")
        raise RuntimeError(f"RunPod API {e.code}: {detail}") from e


def list_pods() -> list[dict]:
    data = request("GET", "/pods")
    if not isinstance(data, list):
        raise RuntimeError(f"Unexpected pods response: {data}")
    return data


def list_gpu_types() -> list[dict]:
    # Legacy GraphQL gpuTypes is not used; current pod list exposes live machine data only
    # via pod creation filters. This launcher accepts a preferred GPU display name and lets
    # RunPod place the pod on the first matching inventory.
    return []


def make_remote_bootstrap(args: argparse.Namespace) -> str:
    lines = [
        "set -euo pipefail",
        "cd /workspace",
        "if ! command -v git >/dev/null 2>&1 || ! command -v git-lfs >/dev/null 2>&1; then apt-get update && apt-get install -y --no-install-recommends git git-lfs; fi",
        "export GIT_LFS_SKIP_SMUDGE=1",
        "git lfs install",
    ]
    if os.environ.get("HF_TOKEN"):
        lines.append('git config --global credential.helper store')
        lines.append('printf "https://user:%s@huggingface.co\\n" "$HF_TOKEN" > ~/.git-credentials')
    lines += [
        f"if [ ! -d pc-sho-dlm ]; then git clone {sh_quote(args.code_repo)} pc-sho-dlm; fi",
        "cd pc-sho-dlm",
        "git fetch --all --tags || true",
    ]
    if args.code_revision:
        lines.append(f"git checkout {sh_quote(args.code_revision)}")
    lines += [
        "chmod +x ./train_runpod.sh",
        "bash ./train_runpod.sh",
    ]
    return " && ".join(lines)


def sh_quote(value: str) -> str:
    return "'" + value.replace("'", "'\"'\"'") + "'"


def create_pod(args: argparse.Namespace) -> dict:
    env = {
        "HF_TOKEN": os.environ.get("HF_TOKEN", ""),
        "MODEL_PRESET": args.model_preset,
        "MODE": args.mode,
        "TOKENIZER": args.tokenizer,
        "DATA": args.data,
        "STEPS": str(args.steps),
        "BATCH_SIZE": str(args.batch_size),
        "LR": str(args.lr),
        "LOG_INTERVAL": str(args.log_interval),
        "SAVE_INTERVAL": str(args.save_interval),
        "TENSORBOARD_DIR": args.tensorboard_dir,
        "TENSORBOARD_PORT": str(args.tensorboard_port),
        "PRETRAIN_SHUFFLE_BUFFER": str(args.pretrain_shuffle_buffer),
        "PRETRAIN_SHUFFLE_SEED": str(args.pretrain_shuffle_seed),
        "SEQ_LEN_OVERRIDE": str(args.seq_len_override or ""),
        "EVAL_INTERVAL": str(args.eval_interval),
        "EVAL_BATCHES": str(args.eval_batches),
        "EVAL_BATCH_SIZE": str(args.eval_batch_size),
        "EVAL_RECORD_SKIP": str(args.eval_record_skip),
        "HF_REPO_ID": args.hf_repo_id,
        "CODE_REPO_URL": args.code_repo,
        "CODE_REPO_REVISION": args.code_revision or "",
    }
    docker_start_cmd = (
        ["bash", "-lc", "sleep infinity"]
        if args.idle_start
        else ["bash", "-lc", make_remote_bootstrap(args)]
    )
    payload = {
        "name": args.name,
        "cloudType": args.cloud_type,
        "computeType": "GPU",
        "gpuCount": args.gpu_count,
        "gpuTypeIds": [args.gpu],
        "gpuTypePriority": "availability",
        "containerDiskInGb": args.container_disk_gb,
        "volumeInGb": args.volume_gb,
        "volumeMountPath": DEFAULT_VOLUME_MOUNT,
        "imageName": args.image,
        "ports": ["22/tcp", "8888/http"],
        "supportPublicIp": True,
        "globalNetworking": True,
        "interruptible": args.interruptible,
        "env": env,
        "dockerStartCmd": docker_start_cmd,
    }
    return request("POST", "/pods", payload)


def main() -> None:
    parser = argparse.ArgumentParser(description="RunPod launcher for PC-SHO-DLM 2B")
    parser.add_argument("--list", action="store_true", help="List existing pods")
    parser.add_argument("--create", action="store_true", help="Create a new pod")
    parser.add_argument("--name", default="pc-sho-dlm-2b")
    parser.add_argument(
        "--gpu",
        default=None,
        help="Exact RunPod GPU type ID, e.g. one of the values shown in the RunPod console/API",
    )
    parser.add_argument("--gpu-count", type=int, default=1)
    parser.add_argument("--cloud-type", choices=["SECURE", "COMMUNITY"], default="COMMUNITY")
    parser.add_argument("--interruptible", action="store_true")
    parser.add_argument(
        "--idle-start",
        action="store_true",
        help="Start container with 'sleep infinity' instead of auto-running bootstrap",
    )
    parser.add_argument("--image", default=DEFAULT_IMAGE)
    parser.add_argument("--volume-gb", type=int, default=150)
    parser.add_argument("--container-disk-gb", type=int, default=100)
    parser.add_argument("--code-repo", default=DEFAULT_HF_CODE_REPO)
    parser.add_argument("--code-revision", default=None)
    parser.add_argument("--model-preset", default="2b")
    parser.add_argument("--mode", default="unified")
    parser.add_argument("--tokenizer", default="HuggingFaceTB/SmolLM2-1.7B")
    parser.add_argument("--data", default="HuggingFaceFW/fineweb-edu")
    parser.add_argument("--steps", type=int, default=50000)
    parser.add_argument("--batch-size", type=int, default=1)
    parser.add_argument("--lr", type=float, default=1e-4)
    parser.add_argument("--log-interval", type=int, default=10)
    parser.add_argument("--save-interval", type=int, default=500)
    parser.add_argument("--tensorboard-dir", default="checkpoints/tensorboard")
    parser.add_argument("--tensorboard-port", type=int, default=8888)
    parser.add_argument("--pretrain-shuffle-buffer", type=int, default=4096)
    parser.add_argument("--pretrain-shuffle-seed", type=int, default=17)
    parser.add_argument("--seq-len-override", type=int, default=None)
    parser.add_argument("--eval-interval", type=int, default=500)
    parser.add_argument("--eval-batches", type=int, default=8)
    parser.add_argument("--eval-batch-size", type=int, default=1)
    parser.add_argument("--eval-record-skip", type=int, default=200000)
    parser.add_argument("--hf-repo-id", default="zotowata/pc-sho-dlm-2b-pretrain")
    args = parser.parse_args()

    if args.list:
        pods = list_pods()
        print(json.dumps(pods, indent=2))
        return

    if args.create:
        if not args.gpu:
            raise SystemExit("--gpu is required with --create")
        pod = create_pod(args)
        print(json.dumps(pod, indent=2))
        return

    parser.print_help()


if __name__ == "__main__":
    main()