import os

import modal
import modal.experimental

cuda_version = "12.6.0"  # should be no greater than host CUDA version
flavor = "devel"  #  includes full CUDA toolkit
operating_sys = "ubuntu22.04"
tag = f"{cuda_version}-{flavor}-{operating_sys}"

LOCAL_CODE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REMOTE_CODE_DIR = "/root/"
REMOTE_TRAIN_SCRIPT_PATH = "train_audios.py"
GPU_TYPE = "H100"

image = (
    modal.Image.from_registry(f"nvidia/cuda:{tag}", add_python="3.12")
    # Note that libibverbs-dev and libibverbs1 are required for RDMA.
    .apt_install("git", "libibverbs-dev", "libibverbs1")
    .pip_install(
        "torch==2.6.0",
        "wandb==0.19.11",
        "tqdm==4.67.1",
        "numpy==1.26.4",
        "torcheval==0.0.7",
        "funcy==2.0",
        "joblib==1.5.1",
        "ffmpeg-python>=0.2.0",
        "sox>=1.4.1",
        "tinytag>=1.8.1",
        "sentencepiece>=0.1.97",
        "transformers",
        "torchaudio",
        "tokenizers",
    )
    .apt_install("sox", "ffmpeg", "libsox-fmt-mp3")
    .workdir("/root/scripts")
    .add_local_dir("../../suno_utils", remote_path="/root/suno_utils")
    .add_local_dir(LOCAL_CODE_DIR, remote_path=REMOTE_CODE_DIR)
)

app = modal.App("suno-hoot-training", image=image)

model_volume = modal.Volume.from_name("suno-hoot-training")
data_volume = modal.Volume.from_name("suno-hoot-data")

# The number of containers (i.e. nodes) in the cluster. This can be between 1 and 8.
n_nodes = 2
# Typically this matches the number of GPUs per container.
n_proc_per_node = 8


def _train_multi_node(profile: bool = False) -> None:
    from torch.distributed.run import parse_args, run

    cluster_info = modal.experimental.get_cluster_info()
    # which container am I?
    container_rank: int = cluster_info.rank
    # what's the leader/master/main container's address?
    main_ip_addr: str = cluster_info.container_ips[0]
    container_id = os.environ["MODAL_TASK_ID"]

    print(f"hello from {container_id}, rank {container_rank} of {n_nodes}")
    if container_rank == 0:
        print(f"main container's address: {main_ip_addr}")

    args = [
        f"--nnodes={n_nodes}",
        f"--nproc-per-node={n_proc_per_node}",
        f"--node-rank={cluster_info.rank}",
        f"--master-addr={main_ip_addr}",
        REMOTE_TRAIN_SCRIPT_PATH,
        "--out_dir=/models/checkpoints",
        "--max_duration_s=280",
        "--max_iters=10000",
        "--warmup_iters=10",
        "--log_interval=1",
        "--eval_interval=5_000",
        "--eval_iters=50",
        "--learning_rate=1e-6",
        "--min_lr=1e-6",
        "--batch_size=2",
        "--decoder_type=v2",
        "--gradient_accumulation_steps=1",
        "--n_augment_freq_masks=4",
        "--preload_checkpoint=/models/hoot_v6_t2_300k.pt",
        "--preload_decoder=True",
        "--train_suno_input_path=/models/suno_hoot_cleaned.json",
        "--custom_seed_offset=8889",
        "--lyrics_key=text",
        "--wandb_run_name=hoot_v6_v2_sft_test",
    ]
    if profile:
        args.append("--profile=True")
    print(f"Running torchrun with args: {' '.join(args)}")
    run(parse_args(args))


@app.function(
    gpu=f"{GPU_TYPE}:{n_proc_per_node}",
    secrets=[
        # Required for connecting to Weights & Biases from within the Modal container.
        modal.Secret.from_name("wandb-secret"),
    ],
    volumes={
        "/models": model_volume,
        # This volume path matches the path in `suno_hoot_20250617_subset100k.json`. This should
        # be something like /data in a prod environment.
        "/app2/suno/data/dpo/audios/auk_t1": data_volume,
    },
    timeout=60 * 60 * 24,
)
@modal.experimental.clustered(n_nodes, rdma=True)
def train_multi_node(profile: bool = False):
    """
    Train the model on a multi-node cluster with N GPUs per node (typically 4).
    Good cluster scale performance should result in a ~linear speedup as the number of nodes
    is increased.
    """
    _train_multi_node(profile)
