File size: 4,726 Bytes
626bca9
 
 
9d7b292
 
 
 
626bca9
 
 
9d7b292
 
626bca9
9d7b292
 
626bca9
 
9d7b292
626bca9
 
 
 
 
 
 
 
 
9d7b292
 
 
626bca9
 
 
 
 
 
 
 
 
 
9bf246c
 
 
626bca9
 
 
 
 
 
8481d22
626bca9
 
 
 
9d7b292
626bca9
 
359f2c4
626bca9
ed0dcee
 
9d7b292
ed0dcee
 
 
626bca9
9d7b292
626bca9
 
 
 
359f2c4
 
 
 
 
 
 
 
 
9d7b292
020a3e3
 
 
 
 
 
 
 
 
9d7b292
 
626bca9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9bf246c
 
626bca9
 
 
 
 
 
9d7b292
 
 
 
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
"""
clankerDiffusion — Modal L4 training.

Builds a CUDA image, pulls the latest code from the HF code repo at runtime,
keeps data + checkpoints on a persistent Modal Volume (/vol), injects HF_TOKEN
from .env, trains a big hybrid model on an L4 (24 GB), and pushes every
checkpoint to the HF repo:

    coderofpears/clankerDiffusion-checkpoints

Data is generated in-container by prep.py (fast HF egress), scaled by --scale,
so no large file transfer is needed.

Launch (after `modal token new` on this machine):
    modal run modal_train.py::train_on_l4 --hours 20 --size large --scale 3
"""
import os
from modal import App, Image, Volume, Secret

HERE = os.path.dirname(os.path.abspath(__file__))
CODE_REPO = "coderofpears/clankerDiffusion-base"
DATA_REPO = "coderofpears/clankerDiffusion-data"
CKPT_REPO = "coderofpears/clankerDiffusion-checkpoints"
DOTENV = os.path.join(HERE, ".env")

image = (
    Image.debian_slim()
    .pip_install("torch==2.11.0", index_url="https://download.pytorch.org/whl/cu128")
    .pip_install("numpy", "tokenizers", "datasets", "safetensors",
                 "huggingface_hub", "hf-transfer")
    .env({"HF_HUB_ENABLE_HF_TRANSFER": "1", "TOKENIZERS_PARALLELISM": "false"})
)

app = App("clanker-diffusion", image=image)
volume = Volume.from_name("clanker-vol", create_if_missing=True)

SIZES = {
    "base":  dict(d_model=768,  n_layers=12, n_heads=12, d_ff=2048, batch=24),
    "large": dict(d_model=2048, n_layers=24, n_heads=16, d_ff=5504, batch=8),
    "xl":    dict(d_model=2560, n_layers=28, n_heads=20, d_ff=6912, batch=4),
    # ~0.8B: fits L4 comfortably, more throughput than the 1.35B "large"
    "08b":   dict(d_model=1536, n_layers=20, n_heads=16, d_ff=4096, batch=12,
                  rope_scale=16.0),
}


@app.function(
    image=image,
    gpu="L4",
    timeout=82800,  # 23h, under Modal's 24h cap
    volumes={"/vol": volume},
    secrets=[Secret.from_dotenv(DOTENV)],
)
def train_on_l4(hours: float = 20.0, ckpt_every: int = 250,
                size: str = "large", batch: int = None, scale: float = 3.0):
    import subprocess
    import sys
    import shutil

    # ---- pull latest code from HF (use huggingface_hub, not the `hf` CLI) ----
    from huggingface_hub import snapshot_download
    os.makedirs("/root/clanker", exist_ok=True)
    snapshot_download(repo_id=CODE_REPO, repo_type="model",
                      local_dir="/root/clanker",
                      token=os.environ.get("HF_TOKEN"))
    os.chdir("/root/clanker")

    data_dir, ckpt_dir = "/vol/data", "/vol/checkpoints"
    os.makedirs(data_dir, exist_ok=True)
    os.makedirs(ckpt_dir, exist_ok=True)

    # reuse the canonical tokenizer / RAG corpus already fetched with the code
    for name in ("tokenizer.json", "meta.json", "rag_corpus.txt"):
        src = os.path.join("/root/clanker/data", name)
        if os.path.exists(src):
            shutil.copy(src, os.path.join(data_dir, name))

    os.makedirs(data_dir, exist_ok=True)
    os.makedirs(ckpt_dir, exist_ok=True)

    # ---- ensure data (generate in-container; fast HF egress) ----
    # Only treat the dataset as complete if meta.json exists (a partial
    # train.bin from a killed run must be regenerated).
    meta_path = os.path.join(data_dir, "meta.json")
    if not os.path.exists(meta_path):
        stale = os.path.join(data_dir, "train.bin")
        if os.path.exists(stale):
            print("[modal] found partial train.bin without meta.json; discarding.")
            os.remove(stale)
        print(f"[modal] generating data locally (scale={scale}) ...")
        subprocess.run([sys.executable, "prep.py", "--out-dir", data_dir,
                        "--scale", str(scale)], check=True)

    spec = dict(SIZES.get(size, SIZES["large"]))
    if batch:
        spec["batch"] = batch

    cmd = [
        sys.executable, "train.py",
        "--hours", str(hours),
        "--batch", str(spec["batch"]),
        "--ckpt-every", str(ckpt_every),
        "--data-dir", data_dir,
        "--ckpt-dir", ckpt_dir,
        "--hf-repo", CKPT_REPO,
        "--d-model", str(spec["d_model"]),
        "--n-layers", str(spec["n_layers"]),
        "--n-heads", str(spec["n_heads"]),
        "--d-ff", str(spec["d_ff"]),
    ]
    if "rope_scale" in spec:
        cmd += ["--rope-scale", str(spec["rope_scale"])]
    print("[modal] launching:", " ".join(cmd), flush=True)
    subprocess.run(cmd, check=True)
    print("[modal] training finished", flush=True)


@app.local_entrypoint()
def main(hours: float = 20.0, ckpt_every: int = 250, size: str = "large",
         batch: int = None, scale: float = 3.0):
    train_on_l4.remote(hours=hours, ckpt_every=ckpt_every, size=size,
                        batch=batch, scale=scale)