This project supports fine-tuning Large Language Models for two distinct tasks:
-
CodeLlama for Chess: Fine-tunes a CodeLlama LLM (e.g.,
codellama/CodeLlama-7b-hf) on a chess dataset (strategic_game_chess.jsonl) using PyTorch Distributed Data Parallel (DDP) or Fully Sharded Data Parallel (FSDP). -
Granite-3.3-2B-Instruct for Function Calling: Fine-tunes an IBM Granite model (e.g.,
ibm-granite/granite-3.3-2b-instruct) using a synthetically generated dataset for function calling, orchestrated with PyTorch DDP/FSDP. For demonstration and testing purposes, external datasets likeglaiveai/glaive-function-calling-v2orNousResearch/hermes-function-calling-v1are not currently used due to previous processing challenges.
Both fine-tuning processes are orchestrated using Slurm for distributed training on a Nebius Kubernetes cluster.
- Nebius Kubernetes Cluster with Slurm/Soperator: Functional cluster with Slurm/Soperator installed.
- Nebius Filestore (or equivalent shared storage): Mounted and accessible by Slurm worker nodes for storing model checkpoints and outputs.
- Docker Registry: Accessible registry (e.g., Nebius Container Registry) for the training image.
- Local Environment: Python and Docker installed for building the training image.
- Nebius CLI &
kubectl: Configured access (optional for debugging). - Slurm Client Tools:
sbatch,squeue, etc., on a login node.
llm-granite-ft/
├── Dockerfile.chess # Defines the container image for chess training
├── Dockerfile.function # Defines the container image for function calling training (example name)
├── chess-finetune.py # Python script for chess fine-tuning
├── fixed-scripts/
│ └── function-finetune-fixed.py # Python script for function calling fine-tuning
├── strategic_game_chess.jsonl # Chess dataset file (for chess-finetune.py)
├── submit_finetune_chess.sbatch # Slurm batch submission script for chess
├── submit_finetune_function_calling.sbatch # Slurm batch submission script for function calling
├── pyproject.toml # Project dependencies and metadata (used by uv)
└── README.md # This file
(Note: function-finetune.py and Dockerfile.osx are also present for local/OSX debugging but not detailed here for Slurm deployment.)
-
For Chess Fine-tuning: The
strategic_game_chess.jsonlfile should be present in thellm-granite-ftdirectory when building the Docker image, asDockerfile.chesscopies it into the image. -
For Function Calling Fine-tuning:
- The primary method for obtaining data for this task is now by using the synthetic data generator script:
llm-granite-ft/fixed-scripts/generate_granite_fc_examples.py. - This script creates a small, correctly formatted dataset with examples of function calls in the Granite format.
- Run this script first to generate the dataset. Example usage:
- The primary method for obtaining data for this task is now by using the synthetic data generator script:
python llm-granite-ft/fixed-scripts/generate_granite_fc_examples.py \
--output_path ./my_synthetic_fc_dataset \
--num_examples 25 \
--tokenizer_name_or_path "ibm-granite/granite-3.3-2b-instruct" \
--max_seq_length 512 - The
--output_path(e.g.,./my_synthetic_fc_dataset) will then be used as the--processed_dataset_pathargument for thefunction-finetune-fixed.pyscript via the Slurm sbatch file. - This synthetic dataset is primarily for demonstration and testing the fine-tuning pipeline.
You will need to build and push separate Docker images for each fine-tuning task.
a. Chess Fine-tuning Image:
-
Navigate to the
llm-granite-ftdirectory. -
Build the Docker image using
Dockerfile.chess:# Replace <tag> with your desired tag, e.g., latest docker build -f Dockerfile.chess -t <your_registry>/llm-chess-ft:<tag> .
(Example:
docker build -f Dockerfile.chess -t cr.eu-north1.nebius.cloud/e00xn9gpx27cp05wsr/llm-chess-ft:latest .) -
Push the image:
docker push <your_registry>/llm-chess-ft:<tag>
b. Function Calling Fine-tuning Image:
-
Navigate to the
llm-granite-ftdirectory. -
Build the Docker image using
Dockerfile.function(or your equivalent Dockerfile for this task):# Replace <tag> with your desired tag, e.g., latest # The image name should match what's in submit_finetune_function_calling.sbatch # Example: docker build -f Dockerfile.function -t cr.eu-north1.nebius.cloud/e00hdcpaq6azg81mmp/finetune-transformers:latest .
(Ensure
Dockerfile.functionis correctly set up to includefixed-scripts/function-finetune-fixed.pyand its dependencies likepeft,bitsandbytes,transformer_engineif using FP8, etc.) -
Push the image:
docker push cr.eu-north1.nebius.cloud/e00hdcpaq6azg81mmp/finetune-transformers:latest
(Use your actual image name and tag).
You will use different sbatch scripts for the different fine-tuning tasks.
Place this script on the Slurm login node or accessible shared storage.
Review and update these sections before submitting:
- Resource Requests (
#SBATCHdirectives): Adjust--nodes,--ntasks-per-node,--gres=gpu:...,--cpus-per-task,--mem,--timeas needed. srunoptions for Pyxis/Enroot:--container-image: Ensure this points to the chess image URI you pushed (e.g.,docker://cr.eu-north1.nebius.cloud/e00xn9gpx27cp05wsr/llm-granite-chess-ft:latest).--container-workdir: Should be/workspace.--container-mounts:- The script constructs
CONTAINER_MOUNTS_ARGto map a host path to/job_outputsinside the container. - The default host path is
/root/slurm_outputs/${SLURM_JOB_ID}. Ensure/root/slurm_outputsexists on the host file system accessible by workers and is writable by the job user. You might need to adjust this path based on your shared storage setup (e.g., map to your Nebius Filestore mount point).
- The script constructs
chess-finetune.pyArguments (FINETUNE_CLI_ARGS):--output_diris set automatically based on the mounted path.--data_pathpoints to the dataset copied into the image.- Uncomment and set other arguments like
--batch_size_per_device,--learning_rate,--gradient_accumulation_stepsif you need to override the defaults inchess-finetune.py.
torchrunparameters:--nproc_per_node: The script attempts to calculate this based on allocated GPUs ($CUDA_VISIBLE_DEVICES,$SLURM_GPUS_PER_TASK, etc.). Ensure your#SBATCH --gres=gpu:Nrequest aligns with how many processes you expect per node.
This script is used to launch fixed-scripts/function-finetune-fixed.py. Place it on the Slurm login node or accessible shared storage.
Key configurations in submit_finetune_function_calling.sbatch:
- Job Name: Set to
fc-qlora-h100(as per#SBATCH --job-name). - Output Log: Main Slurm log is directed to
/root/slurm_logs/fc_%j.log(as per#SBATCH --output). Node-specific logs may be created by the application within the job's shared directory. - Container Image: The sbatch script uses
IMAGE="cr.eu-north1.nebius.cloud/e00hdcpaq6azg81mmp/finetune-13:latest". Ensure this matches the image you built and pushed. - Shared Job Directory: Base directory for outputs, logs, and coordination files is
/slurm_jobs/${SLURM_JOB_ID}on the host, mounted into the container at/job_data. (Note: The sbatch script uses/mnt/jail/prefix forHOST_JOBDIRinternally, but the conceptual host path for user understanding is/slurm_jobs/...). - Python Script: Executes
function-finetune-fixed.py(located in the container's/workspace). - NCCL Environment Variables: The sbatch script sets
NCCL_DEBUG=INFO. Other NCCL variables likeNCCL_COLLNET_ENABLE,NCCL_IB_HCAare not explicitly set in the current version of the sbatch script. - Distributed Setup (using
torchrun):- The script now uses
torchrunto launchfunction-finetune-fixed.py. torchrunparameters include--nnodes,--nproc_per_node,--rdzv_backend=c10d,--rdzv_id, and--rdzv_endpoint(constructed fromMASTER_IPandMASTER_PORTderived from Slurm).- While the sbatch script sets
WORLD_SIZE,RANK, andLOCAL_RANK,torchruntypically manages these for the application. The Python scriptfunction-finetune-fixed.pyis designed forenv://initialization, whichtorchrunprovides.
- The script now uses
- Python Script Arguments (
FINETUNE_CLI_ARGS):--output_diris set to${CONT_JOBDIR}/checkpoints(whereCONT_JOBDIRis/job_datainside the container).--processed_dataset_pathshould be set to the path where the synthetic dataset was saved bygenerate_granite_fc_examples.py(e.g.,/path/on/shared/storage/my_synthetic_fc_datasetif generated outside the container, or a path within the container if copied during image build or mounted). The sbatch script example might use a placeholder like/job_data/synthetic_datasetwhich you would need to ensure is correctly populated or mounted.--use_qlorais passed by default, enabling QLoRA.--use_fp8is also passed by default in the sbatch script. If enabled:- LoRA layers are converted to Transformer Engine FP8 layers.
- The script internally sets the precision for non-TE components to
torch.float16. Standardtorch.autocastfor AMP will usetorch.float16for these parts.
- The
--disable_ampargument is not passed by default.- If
--use_fp8is active, non-TE parts run intorch.float16AMP. To run non-TE parts in FP32 while TE layers use FP8, you would add--disable_amptoFINETUNE_ARGS. - If
--use_fp8is not active, the script uses the--amp_precision_mode(defaulting tobf16) for AMP.
- If
- Other arguments for
function-finetune-fixed.py(e.g.,--batch_size_per_device,--learning_rate, other LoRA parameters) can be added toFINETUNE_ARGSin the sbatch script. - Important Considerations for
FINETUNE_ARGS(previouslyFINETUNE_CLI_ARGS):--batch_size_per_device: The scriptfunction-finetune-fixed.pydefaults this to 16. For large models like Granite 3.3B, especially on GPUs with ~80GB memory, even this might be too high. It's recommended to start with a smaller value (e.g., 4 or 8) and enable--gradient_checkpointingto prevent Out-Of-Memory errors. Adjust based on your specific GPU memory and model size.--lora_r: If using QLoRA (--use_qlora) in conjunction with FP8 (--use_fp8), the--lora_rvalue must be a multiple of 16. The scriptfunction-finetune-fixed.pydefaults--lora_rto 16 and includes a check to enforce this.--gradient_checkpointing: Strongly recommended to reduce memory usage, especially with large batch sizes or models. Ensure this flag is passed inFINETUNE_CLI_ARGSif needed.
If pulling llm-chess-ft:<tag> requires authentication:
- Create/edit
/root/.config/enroot/.credentials(or user equivalent) on the environment wheresrunexecutes (login node, potentially propagated to workers). - Add line:
machine <your_registry_host> login <KEY_ID> password <SECRET_KEY>- Use appropriate credentials (e.g., Nebius SA Static Access Key).
- Set permissions:
chmod 600 .../.credentials.
From the Slurm login node, submit the appropriate sbatch script for your desired task:
a. For Chess Fine-tuning:
sbatch /path/to/your/llm-granite-ft/submit_finetune_chess.sbatchb. For Function Calling Fine-tuning:
sbatch /path/to/your/llm-granite-ft/submit_finetune_function_calling.sbatchEnsure the paths to the sbatch scripts are correct.
- Slurm:
squeue -u $USER,scontrol show job <jobid>, check output log (e.g.,/root/chess_finetune_<jobid>.logas defined in the sbatch script). - Kubernetes (for Soperator debugging):
kubectl get pods -A -l slurm.nebius.ai/job-id=<jobid>,kubectl logs <pod_name> -n <namespace>.
- Uses standard PyTorch DDP (
torch.distributed.init_process_group,torch.nn.parallel.DistributedDataParallel). - Parses command-line arguments for hyperparameters (batch size, LR, etc.).
- Includes a basic
JsonlDatasetclass for the chess data (strategic_game_chess.jsonl). - Implements a standard training loop using
torch.optim.AdamW,GradScalerfor AMP (bf16), and a simple linear warmup LR schedule. - Typically launched via
torchrun(often implicitly handled bysrunwith correct arguments if the sbatch script is set up for it, though the examplesubmit_finetune_chess.sbatchmight need review for this specific launch method). - May contain optional FSDP logic.
- Current Version (as of last update): This script is significantly refactored for fine-tuning models like
ibm-granite/granite-3.3-2b-instructusing PyTorch FSDP with QLoRA. - Key Features:
- FSDP: Uses
torch.distributed.fsdp.FullyShardedDataParallelby default.- Utilizes
use_orig_params=Trueandignored_modules(especially for QLoRA compatibility) when initializing FSDP. It does not use a specific Hugging Facetransformer_auto_wrap_policy. - Includes logic to set
ignored_modulesfor FSDP, particularly for handling potentialint8parameters from quantization. - Uses
sync_module_states=TrueandStateDictType.FULL_STATE_DICTfor robust checkpointing.
- Utilizes
- QLoRA: Enabled via the
--use_qloraflag.- Configures
BitsAndBytesConfigfor 4-bit quantization (e.g.,nf4). Thebnb_4bit_quant_storageis aligned with the model'samp_dtypefor FSDP compatibility. - Uses
peft.prepare_model_for_kbit_trainingandpeft.get_peft_modelwithLoraConfig.
- Configures
- FP8 Support (Optional via Transformer Engine): Includes experimental support for NVIDIA Transformer Engine FP8 for LoRA adapters (
--use_fp8), iftransformer_engineis available.- Important Note on FP8 and AMP: When using
--use_fp8with Transformer Engine, standard PyTorch Automatic Mixed Precision (torch.autocast) should generally be disabled (--disable_amp). Transformer Engine'sfp8_autocastcontext manages its own precision for FP8 layers, and the surrounding non-TE operations will run in the precision determined by--disable_amp(FP32) or implicitly by the TE FP8 setup (often FP16 for non-TE parts if AMP is not explicitly disabled). The scriptfunction-finetune-fixed.pysetsamp_dtypetotorch.float16if--use_fp8is active and AMP is not disabled, which means non-TE parts run undertorch.autocast("cuda", dtype=torch.float16).
- Important Note on FP8 and AMP: When using
- Data Handling:
- Requires preprocessed data: The script loads data using
datasets.load_from_diskvia the mandatory--processed_dataset_pathargument. On-the-fly processing of raw datasets is no longer supported in this version. - Uses a custom
Splitclass (a wrapper around a Hugging FaceDatasetsplit).
- Requires preprocessed data: The script loads data using
- Mixed Precision (AMP):
- If
--use_fp8is not enabled, AMP is active by default, typically usingtorch.bfloat16(if supported) ortorch.float16. AGradScaler(standard or sharded for FSDP) is used withtorch.float16. - If
--use_fp8is enabled, non-TE parts of the model operate undertorch.autocastwithtorch.float16(unless--disable_ampis also passed, then they use FP32). - AMP can be fully disabled with
--disable_amp.
- If
- Optimizer & Scheduler: Uses
torch.optim.AdamWand a linear warmup LR schedule. - Gradient Checkpointing: Supported via
--gradient_checkpointing. - Logging & Checkpointing: Standard logging and checkpoint saving logic, compatible with FSDP.
- Distributed Launch: The
submit_finetune_function_calling.sbatchscript now usestorchrunto launch this Python script. The Python script itself is compatible withtorchrun'senv://initialization method for distributed training.
- FSDP: Uses
- Uses a base image like
nvcr.io/nvidia/pytorch:24.07-py3. - Installs minimal extra dependencies (e.g.,
transformers,datasets). - Copies the
chess-finetune.pyscript andstrategic_game_chess.jsonldataset into/workspace.
- This Dockerfile (e.g., named
Dockerfile.functionor similar, corresponding to the imagellm-granite-function-ft-fix) should be set up to:- Use a suitable PyTorch base image (e.g.,
nvcr.io/nvidia/pytorch:24.07-py3or newer, as used in recent examples). - Install dependencies like
transformers,datasets,peft,bitsandbytes,accelerate, and optionallytransformer_engine(for FP8). - Copy the
fixed-scripts/function-finetune-fixed.pyscript andfixed-scripts/generate_granite_fc_examples.py(if you intend to generate data within a job step, though typically it's a pre-step). - If using the synthetic data generator as a pre-step, the
Dockerfile.functiondoes not necessarily need to copy the dataset itself, as the path will be provided at runtime.
- Use a suitable PyTorch base image (e.g.,
- Slurmctld Down / PartitionConfig Errors: Check
sinfo,slurmctldlogs, andslurmdlogs on worker pods via cluster admin orkubectl. - Image Pull Errors (401 Unauthorized): Verify Enroot authentication credentials for your registry.
- Pyxis Errors (
couldn't start container): Check image pull success,--container-mountsvalidity (host path exists and has permissions), and basic container functionality. torchrunErrors / DDP Init Errors (forchess-finetune.pyor similartorchrun-based scripts): Ensuresrunis passing necessary Slurm environment variables (SLURM_PROCID,SLURM_NTASKS, etc.) correctly into the container fortorchrunauto-detection.torchrun/ DDP/FSDP Init Errors (forfunction-finetune-fixed.py): Iftorch.distributed.init_process_group(called internally by the script forenv://init) fails or hangs:- Verify
torchrunparameters in the sbatch script (--nnodes,--nproc_per_node, rendezvous endpoint). - Check that
MASTER_IPandMASTER_PORTare correctly determined and accessible between nodes. - Examine
NCCL_DEBUG=INFOoutput and any node-specific logs within the job directory for clues. - Ensure the network configuration (e.g., InfiniBand, Ethernet) is correctly utilized by NCCL.
- Verify
ModuleNotFoundError: Ensure the respective Dockerfile (Dockerfile.chessorDockerfile.function) installs all required Python packages. CheckPYTHONPATHif necessary, although direct script execution in/workspace(or/workspace/fixed-scripts/) should generally work if scripts and dependencies are correctly placed.- CUDA Errors /
nvidia-smifails inside container: Likely an issue with Pyxis/Enroot setup, host drivers, or Slurm GPU resource allocation (gres.conf, cgroups). Escalate to admin if basic checks fail. - Node Failure: As seen previously, check
slurmctld.logvia admin for hardware/daemon issues on the specific worker node.
The following notes are based on experimental fine-tuning runs with Granite-3.3-2B-Instruct using QLoRA, LoRA, FSDP, DDP, AMP, and Transformer Engine FP8 on H100 GPUs. These are observations and may vary with different models, datasets, or hardware. Default learning rate (lr) was 6e-5 and batch size per device (bs) was as noted.
Working Configurations:
- QLoRA + FSDP + AMP (BF16/FP16): Works well.
- Example:
lr: 6e-5, bs: 16
- Example:
- QLoRA + FSDP2 + AMP (BF16/FP16): Works well.
- Example:
lr: 6e-5, bs: 16
- Example:
- QLoRA + DDP + AMP (BF16/FP16): Works well.
- Example:
lr: 6e-5, bs: 16
- Example:
- QLoRA + FSDP + FP8 (Transformer Engine, AMP disabled): Works, but requires a smaller batch size.
- Example:
lr: 6e-5, bs: 2
- Example:
- QLoRA + DDP + FP8 (Transformer Engine, AMP disabled): Works, but requires a smaller batch size.
- Example:
lr: 6e-5, bs: 2
- Example:
- LoRA + FSDP + FP8 (Transformer Engine, AMP disabled): Works.
- Example:
lr: 6e-5, bs: 2
- Example:
- LoRA + DDP + FP8 (Transformer Engine, AMP disabled): Works.
- Example:
lr: 6e-5, bs: 2
- Example:
- LoRA + FSDP + AMP (BF16/FP16) + FP8 (Transformer Engine): Works. (Note: AMP here refers to the non-TE parts of the model; TE manages its own FP8 context).
- Example:
lr: 6e-5, bs: 2
- Example:
Configurations with Issues (Unstable/NaN Loss):
- QLoRA + FSDP + AMP (BF16/FP16) + FP8 (Transformer Engine): Unstable, NaN loss.
- Tested with:
lr: 6e-5, bs: 16 - Note: This suggests potential conflicts when standard AMP is active alongside QLoRA and TE FP8 under FSDP.
- Tested with:
- QLoRA + DDP + AMP (BF16/FP16) + FP8 (Transformer Engine): Unstable, NaN loss.
- Tested with:
lr: 6e-5, bs: 16 - Note: Similar instability as with FSDP under this combined AMP + QLoRA + TE FP8 setup.
- Tested with:
- LoRA + DDP + AMP (BF16/FP16) + FP8 (Transformer Engine): Unstable, NaN loss.
- Tested with:
lr: 6e-5, bs: 2 - Note: Instability even without QLoRA when combining DDP, standard AMP, and TE FP8.
- Tested with:
Key Takeaways from Experiments:
- Combining standard PyTorch AMP (
torch.autocast) with Transformer Engine's FP8 (fp8_autocast) seems to be a primary source of instability, especially with QLoRA. - When using Transformer Engine FP8, it's generally more stable to disable standard AMP (
--disable_amp) and let TE manage its precision context. The surrounding operations would then run in FP32 (if AMP is disabled) or FP16 (if--use_fp8implicitly sets non-TE AMP to FP16, as infunction-finetune-fixed.py). - QLoRA + FP8 (with AMP disabled) configurations required a significantly smaller batch size (2 vs. 16) to maintain stability compared to QLoRA + AMP (without FP8).
- FSDP generally appears more stable or manageable with complex configurations (like LoRA+FP8+AMP) than DDP in these specific tests, though DDP with LoRA+FP8 (AMP disabled) worked.