Text Generation
Transformers
Safetensors
English
lfm2_moe
text-generation-inference
unsloth
conversational
Instructions to use ItachiSan/LiquidCode-2.5-24B-A2B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ItachiSan/LiquidCode-2.5-24B-A2B with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="ItachiSan/LiquidCode-2.5-24B-A2B") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("ItachiSan/LiquidCode-2.5-24B-A2B") model = AutoModelForCausalLM.from_pretrained("ItachiSan/LiquidCode-2.5-24B-A2B", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use ItachiSan/LiquidCode-2.5-24B-A2B with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "ItachiSan/LiquidCode-2.5-24B-A2B" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ItachiSan/LiquidCode-2.5-24B-A2B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/ItachiSan/LiquidCode-2.5-24B-A2B
- SGLang
How to use ItachiSan/LiquidCode-2.5-24B-A2B with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "ItachiSan/LiquidCode-2.5-24B-A2B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ItachiSan/LiquidCode-2.5-24B-A2B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "ItachiSan/LiquidCode-2.5-24B-A2B" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ItachiSan/LiquidCode-2.5-24B-A2B", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Unsloth Desktop
- Docker Model Runner
How to use ItachiSan/LiquidCode-2.5-24B-A2B with Docker Model Runner:
docker model run hf.co/ItachiSan/LiquidCode-2.5-24B-A2B
Download notebook.py from ItachiSan/LiquidCode-2.5-24B-A2B: direct link, hf CLI and curl.
- Browser
- Download file 35.6 kB
-
https://huggingface.co/ItachiSan/LiquidCode-2.5-24B-A2B/resolve/main/notebook.py
- Command line
-
hf download hf://ItachiSan/LiquidCode-2.5-24B-A2B/notebook.py
-
curl -L -o notebook.py https://huggingface.co/ItachiSan/LiquidCode-2.5-24B-A2B/resolve/main/notebook.py
35.6 kB
| # /// script | |
| # requires-python = ">=3.13" | |
| # dependencies = [ | |
| # "datasets==4.8.5", | |
| # "huggingface-hub==1.33.0", | |
| # "torch==2.14.1", | |
| # "transformers==5.17.0", | |
| # "trl==1.15.0", | |
| # "unsloth==2026.11.2", | |
| # "unsloth-zoo==2026.11.3", | |
| # ] | |
| # /// | |
| import marimo | |
| __generated_with = "0.25.2" | |
| app = marimo.App(width="medium", auto_download=["html"]) | |
| def _(): | |
| # Let's start! | |
| import marimo as mo | |
| mo.md("# Unsloth Finetuning notebook!") | |
| return (mo,) | |
| def _(mo): | |
| # Definition of mo_exec and mo_exec stream | |
| from subprocess import ( | |
| run as _run, | |
| Popen as _Popen, | |
| PIPE as _PIPE, | |
| STDOUT as _STDOUT, | |
| ) | |
| def mo_exec(*process_args, cwd=None): | |
| cmdline = " ".join(process_args).split(" ") | |
| result = _run( | |
| cmdline, | |
| capture_output=True, | |
| text=True, | |
| cwd=cwd, | |
| ) | |
| result_text = f""" | |
| **Command**: _{" ".join(process_args)}_ | |
| **Exit code**: {result.returncode} | |
| """ | |
| if result.stdout: | |
| result_text += f""" | |
| **stdout** | |
| ``` | |
| {result.stdout} | |
| ``` | |
| """ | |
| if result.stderr: | |
| result_text += f""" | |
| **stderr** | |
| ``` | |
| {result.stderr} | |
| ``` | |
| """ | |
| return mo.md(result_text) | |
| def mo_stream(*process_args, cwd=None): | |
| print(f"π Running: {' '.join(process_args)} ...") | |
| # Run process with real-time output piping | |
| cmdline = " ".join(process_args).split(" ") | |
| process = _Popen( | |
| cmdline, | |
| stdout=_PIPE, | |
| stderr=_STDOUT, | |
| text=True, | |
| cwd=cwd, | |
| bufsize=1, | |
| errors="replace", # Replace invalid bytes | |
| ) | |
| # Print lines as they come in so you see live progress | |
| for line in process.stdout: | |
| print(line, end="") | |
| process.wait() | |
| if process.returncode != 0: | |
| raise RuntimeError( | |
| f"β Command failed with exit code {process.returncode}" | |
| ) | |
| return mo_exec, mo_stream | |
| def _(mo_stream): | |
| # Important setup! | |
| import os | |
| # Avoid Unsloth custom modules compilation. Make sure we can run properly | |
| os.environ["UNSLOTH_FULLGRAPH"] = "0" | |
| os.environ["UNSLOTH_NO_COMPILE"] = "1" | |
| os.environ["UNSLOTH_DISABLE_MOE_GROUPED_MM"] = "1" | |
| # Extra debug messages in case of errors | |
| os.environ.setdefault("TORCHDYNAMO_VERBOSE", "1") | |
| os.environ.setdefault("TORCH_LOGS", "+dynamo") | |
| # Hide HuggingFace download messages | |
| os.environ.setdefault("HF_XET_HIGH_PERFORMANCE", "1") | |
| # Set prebuilt llama.cpp location | |
| llama_cpp_dir = "/marimo/unsloth/llama.cpp" | |
| os.environ.setdefault("UNSLOTH_LLAMA_CPP_PATH", llama_cpp_dir) | |
| # Do the ugly. Install Unsloth stuff via CLI | |
| mo_stream("unsloth install-kernels") | |
| from huggingface_hub import HfApi | |
| hf = HfApi() # HF_TOKEN is in .env | |
| print("Connected to HuggingFace!") | |
| return hf, llama_cpp_dir, os | |
| def _(mo_stream): | |
| # Show required wheels | |
| mo_stream("unsloth install-kernels --dry-run") | |
| print(f""" | |
| These are the packages to be installed via the cell below or the *Packages* section. | |
| If needed, install these dependencies (found by hand; better the one from the cell above): | |
| - causal-conv1d: https://github.com/protopia-ai/causal-conv1d/releases/download/v1.6.2.post7/causal_conv1d-1.6.2.post7+cu130torch2.14cxx11abiTRUE-cp313-cp313-linux_x86_64.whl | |
| - xformers: https://download.pytorch.org/whl/cu130/xformers-0.0.35-py39-none-manylinux_2_28_x86_64.whl | |
| - flash-attn: https://github.com/mjun0812/flash-attention-prebuild-wheels/releases/download/v0.10.0/flash_attn-2.8.3%2Bcu130torch2.14-cp313-cp313-linux_x86_64.whl | |
| """) | |
| return | |
| def _(mo_stream): | |
| # Check Unsloth status | |
| mo_stream("unsloth doctor") | |
| return | |
| def _(mo_exec): | |
| mo_exec("env") | |
| return | |
| def _(mo, os): | |
| # Show system status | |
| import sys | |
| import torch | |
| # Silly dependency, but ensures we run the cell with the os import which has all the important stuff. | |
| _unsloth_patch = True if os.getenv("UNSLOTH_PATCHED") == "1" else False | |
| fa_avail, xf_avail, cc_avail = False, False, False | |
| try: | |
| import flash_attn as _fa | |
| fa_avail = _fa.__version__ | |
| except ImportError: | |
| pass | |
| try: | |
| import xformers as _xf | |
| xf_avail = _xf.__version__ | |
| except ImportError: | |
| pass | |
| try: | |
| import causal_conv1d as _cc | |
| cc_avail = _cc.__version__ | |
| except ImportError: | |
| pass | |
| mo.md(f""" | |
| ## System status | |
| |**Metric** |**Value** | | |
| |----------------|-------------------------------| | |
| |Python version |{sys.version} | | |
| |PyTorch version |{torch.__version__} | | |
| |CUDA available |{torch.cuda.is_available()} | | |
| |CUDA version |{torch.version.cuda} | | |
| |GPU |{torch.cuda.get_device_name(0)}| | |
| |Flash Attention |{fa_avail} | | |
| |XFormers |{xf_avail} | | |
| |causal-conv1d |{cc_avail} | | |
| |Unsloth applied |{_unsloth_patch} | | |
| """) | |
| return (torch,) | |
| def _(): | |
| # Define models and datasets | |
| # Here are all the models and datasets we want to use for our finetuning | |
| MODELS = [ | |
| "LiquidAI/LFM2.5-24B-A2B", | |
| "LiquidAI/LFM2.5-8B-A1B", | |
| "LiquidAI/LFM2.5-2.6B", | |
| "LiquidAI/LFM3-1.7B", | |
| ] | |
| DATASETS = [ | |
| "nvidia/Nemotron-SFT-SWE-v3", | |
| "empero-ai/gpt-5.6-luna-sft-900x", | |
| ] | |
| return DATASETS, MODELS | |
| def _(DATASETS, MODELS, mo): | |
| # Nice dropdown for selection. | |
| model_selection = mo.ui.dropdown( | |
| options=MODELS, value=MODELS[0], label="Model to finetune" | |
| ) | |
| dataset_selection = mo.ui.dropdown( | |
| options=DATASETS, | |
| value=DATASETS[0], | |
| label="Dataset to use for finetuning", | |
| ) | |
| mo.vstack([model_selection, dataset_selection]) | |
| return dataset_selection, model_selection | |
| def _(dataset_selection, mo, model_selection): | |
| # Fill the required variables for all next sections! | |
| base_model_name, dataset_name = ( | |
| model_selection.value, | |
| dataset_selection.value, | |
| ) | |
| # Definition of the place where the model ends! | |
| model_base_name = "-".join(base_model_name.split("-")[1:]) | |
| model_name = f"LiquidCode-2.5-{model_base_name}" | |
| model_repo = f"ItachiSan/{model_name}" | |
| model_gguf_repo = model_repo + "-GGUF" | |
| mo.md(f""" | |
| Selected combination: | |
| - Base model: {base_model_name} | |
| - Dataset: {dataset_name} | |
| - Model name: {model_name} | |
| - Model repository: {model_repo} | |
| """) | |
| return ( | |
| base_model_name, | |
| dataset_name, | |
| model_base_name, | |
| model_gguf_repo, | |
| model_name, | |
| model_repo, | |
| ) | |
| def _(mo): | |
| mo.md(""" | |
| # Setting up crucial parameters! | |
| """) | |
| return | |
| # Function to calculate the training config. | |
| def calculate_training_config( | |
| vram_gb: float, | |
| model_params_b: float, | |
| num_gpus: int = 1, | |
| target_gbs: int = 128, | |
| max_seq_length: int = 2048, | |
| use_4bit_qlora: bool = True, | |
| ): | |
| """ | |
| Calculates safe per-device batch size and gradient accumulation steps. | |
| """ | |
| print(f"--- Training Configuration Calculator ---") | |
| print(f"Hardware: {vram_gb} GB VRAM | GPUs: {num_gpus}") | |
| print( | |
| f"Model Size: {model_params_b}B parameters | Max Seq Length: {max_seq_length}" | |
| ) | |
| print(f"Target Global Batch Size (GBS): {target_gbs}\n") | |
| # 1. Estimate Base Model VRAM Footprint | |
| if use_4bit_qlora: | |
| base_model_vram = ( | |
| model_params_b * 0.55 | |
| ) # ~0.55 GB per billion params in 4-bit | |
| else: | |
| base_model_vram = ( | |
| model_params_b * 2.0 | |
| ) # ~2 GB per billion params in 16-bit | |
| overhead_vram = 4.0 # CUDA context, system overhead, LoRA adapters | |
| available_vram_for_activations = ( | |
| vram_gb - base_model_vram - overhead_vram | |
| ) | |
| if available_vram_for_activations <= 0: | |
| return "β Error: Model is too large to fit in this VRAM size even with quantization!" | |
| # 2. Approximate VRAM consumed per sequence at given sequence length | |
| # (Empirical scaling factor for 4k/8k context transformers) | |
| vram_per_sequence = ( | |
| (max_seq_length / 4096) * (model_params_b / 24) * 2.5 | |
| ) | |
| # 3. Calculate safe max per-device batch size (leaving a 15% safety buffer) | |
| safe_vram_pool = available_vram_for_activations * 0.85 | |
| estimated_bs = int(safe_vram_pool / vram_per_sequence) | |
| # Bound batch size between 1 and 32, rounding to clean numbers if possible | |
| per_device_bs = max(1, min(estimated_bs, 32)) | |
| # 4. Calculate Gradient Accumulation Steps to match Target GBS | |
| total_effective_bs_per_step = per_device_bs * num_gpus | |
| if target_gbs % total_effective_bs_per_step != 0: | |
| # Round up or find closest clean division | |
| grad_accum_steps = max( | |
| 1, round(target_gbs / total_effective_bs_per_step) | |
| ) | |
| actual_gbs = per_device_bs * grad_accum_steps * num_gpus | |
| else: | |
| grad_accum_steps = target_gbs // total_effective_bs_per_step | |
| actual_gbs = target_gbs | |
| print(f"β Recommended Settings:") | |
| print(f" per_device_train_batch_size = {per_device_bs}") | |
| print(f" gradient_accumulation_steps = {grad_accum_steps}") | |
| print( | |
| f" Resulting Global Batch Size = {actual_gbs} (Target was {target_gbs})" | |
| ) | |
| print( | |
| f" Estimated VRAM utilization: ~{int(base_model_vram + (per_device_bs * vram_per_sequence) + overhead_vram)} GB / {vram_gb} GB\n" | |
| ) | |
| return per_device_bs, grad_accum_steps | |
| def _(mo_exec): | |
| # GPU check | |
| mo_exec("nvidia-smi") | |
| return | |
| def _(mo, model_base_name): | |
| mo.md(f""" | |
| Planned model: {model_base_name} | |
| GPU available: see above. | |
| Use the data for the next calculation. | |
| """) | |
| return | |
| def _(model_base_name): | |
| # --- Example: Your exact setup (96GB VRAM, 24B Model, 4096 Seq Length) --- | |
| max_seq_length = 2048 # Adjust as needed (e.g., 4096) | |
| _target_gbs = 128 # Not sure how to get this, got it from Gemini | |
| _gpu_size_gb = 96.0 # GPU VRAM GB, get it from nvidia-smi block above | |
| # Detecting model size from the name | |
| _model_params_b = model_base_name.rstrip("B") | |
| if "-" in _model_params_b: | |
| _model_params_b = _model_params_b.split("-")[0].rstrip("B") | |
| _model_params_b = float(_model_params_b) | |
| batch_size, gradient_steps = calculate_training_config( | |
| vram_gb=_gpu_size_gb, | |
| model_params_b=_model_params_b, | |
| num_gpus=1, | |
| target_gbs=_target_gbs, | |
| max_seq_length=max_seq_length, | |
| use_4bit_qlora=True, | |
| ) | |
| # Nice info print | |
| print(f"""\ | |
| Configured parameters: | |
| - Total model size: {_model_params_b} | |
| - Available VRAM: {_gpu_size_gb} | |
| - Sequence length: {max_seq_length} | |
| - Batch size: {batch_size} | |
| - Gradient steps: {gradient_steps} | |
| """) | |
| return batch_size, gradient_steps, max_seq_length | |
| def _(mo): | |
| mo.md(""" | |
| # Training the model! | |
| """) | |
| return | |
| def _(base_model_name, max_seq_length): | |
| # Load the model | |
| from unsloth import FastLanguageModel | |
| dtype = None # None for auto-detection | |
| load_in_4bit = True # Use 4-bit quantization to save memory | |
| # 1. Load base model and tokenizer | |
| base_model, tokenizer = FastLanguageModel.from_pretrained( | |
| model_name=base_model_name, # Or your specific base model path | |
| max_seq_length=max_seq_length, | |
| dtype=dtype, | |
| load_in_4bit=load_in_4bit, | |
| ) | |
| base_model, tokenizer | |
| return FastLanguageModel, base_model, tokenizer | |
| def _(base_model): | |
| # Try detecting modules automatically | |
| detected_modules = set() | |
| # Simple map of modules to skip. Their bool value is to print the item once. | |
| _modules_to_skip = { | |
| # Modules to skip for LFM2 MoE | |
| "q_layernorm": False, | |
| "k_layernorm": False, | |
| "experts": False, | |
| "gate": False, | |
| } | |
| for _layer in base_model.model.layers: | |
| for _name, _ in _layer.named_modules(): | |
| # The modules are in form | |
| # sublayer(?) . module | |
| # If no dot is there, we have nothing to do here. | |
| if "." not in _name: | |
| continue | |
| _, _module_name = _name.split(".", maxsplit=1) | |
| # Handle cleanly re-detection | |
| if "." in _module_name: | |
| _module_name = _module_name.split(".", maxsplit=1)[0] | |
| if _module_name in _modules_to_skip: | |
| if _modules_to_skip[_module_name] is False: | |
| print("Skip:", _module_name) | |
| _modules_to_skip[_module_name] = True | |
| continue | |
| if _module_name not in detected_modules: | |
| print("Found:", _module_name) | |
| detected_modules.add(_module_name) | |
| detected_modules | |
| return (detected_modules,) | |
| def _(FastLanguageModel, base_model, detected_modules, tokenizer): | |
| # 2. Configure the LoRA model and get the adapters | |
| # There are hard-coded values found by inspection and chat with AI | |
| _target_modules = [ | |
| ## Default values for Unsloth, with extra comments from Gemini | |
| # 1. Standard Attention Projections | |
| "q_proj", | |
| "k_proj", | |
| "v_proj", | |
| "o_proj", | |
| # Unused in Lfm2Moe, but keep for safety | |
| "gate_proj", | |
| "up_proj", | |
| "down_proj", | |
| ## Added extra modules that should properly target MoE | |
| # 2. Hybrid Short Convolution Layers (Local temporal mixing) | |
| "in_proj", | |
| "out_proj", | |
| "conv", | |
| # 3. MoE Expert Projections & Router (Layer naming for LFM MoE) | |
| "ffn_gate_exps", | |
| "ffn_up_exps", | |
| "ffn_down_exps", | |
| "ffn_gate_inp", | |
| # 4. Dense FFN layers (Present in non-expert blocks) | |
| # "ffn_gate", "ffn_up", "ffn_down", | |
| "w1", | |
| "w2", | |
| "w3", # Fixed after checking the values below | |
| ] | |
| # Stick also out own modules for safety. | |
| _target_modules = sorted( | |
| list(set(_target_modules + list(detected_modules))) | |
| ) | |
| print("Training following modules:", _target_modules) | |
| # Now, get the trainable model | |
| model = FastLanguageModel.get_peft_model( | |
| base_model, | |
| target_modules=_target_modules, | |
| r=32, # Choose any number > 0 ! Suggested 8, 16, 32, 64, 128 | |
| lora_alpha=64, | |
| lora_dropout=0, # Optimized to 0 by default | |
| bias="none", # Supports "none", "all" | |
| use_gradient_checkpointing="unsloth", # Saves VRAM | |
| random_state=3407, | |
| use_rslora=False, # Rank stabilized LoRA support | |
| loftq_config=None, # LoftQ | |
| ) | |
| model, tokenizer | |
| return (model,) | |
| def _(model): | |
| _trainable_params = 0 | |
| _total_params = 0 | |
| for _name, _param in model.named_parameters(): | |
| _total_params += _param.numel() | |
| if _param.requires_grad: | |
| _trainable_params += _param.numel() | |
| print(f"Total parameters: {_total_params:,}") | |
| print( | |
| f"Trainable parameters: {_trainable_params:,} ({100 * _trainable_params / _total_params:.2f}%)" | |
| ) | |
| return | |
| def _(tokenizer): | |
| print("Tokenizer template:") | |
| print(tokenizer.get_chat_template()) | |
| return | |
| def _(): | |
| # Shared preprocessing functions | |
| import json | |
| def rjl(value): | |
| """ | |
| Hand-made Recursive JSON Loads. | |
| This is because AI sucks at writing this. | |
| JSON allows for | |
| - iterables: dict, list | |
| - values: bool, null, int, float, string | |
| Some datasets have JSON nested in strings and the default json.loads does | |
| not handle those. This function does. | |
| Args: | |
| value: The value to convert. Does nothing unless it is a JSON string. | |
| Returns: | |
| The parsed Python object, or the original value if not a JSON string. | |
| """ | |
| # Handle iterables first. We want to be sure to loop when needed. | |
| if isinstance(value, list): | |
| return [rjl(item) for item in value] | |
| if isinstance(value, dict): | |
| return {key: rjl(val) for key, val in value.items()} | |
| # Other simple case: if not string, no need to json.loads | |
| if not isinstance(value, str): | |
| return value | |
| # Here we have a string, let's see if it is a JSON | |
| try: | |
| decoded = json.loads(value) | |
| except (json.JSONDecodeError, TypeError): | |
| # The string is not a JSON, so return it. | |
| return value | |
| # Now we decoded something, but we don't know what. | |
| # So, let's recurse! This way we handle all cases | |
| return rjl(decoded) | |
| return (rjl,) | |
| def _(c, rjl): | |
| # Nemotron specific preprocessing | |
| def _fix_nemotron_chat(chat): | |
| fixed_chat = [] | |
| for message in chat: | |
| fixed_message = message.copy() | |
| # Ensure tool calls are data and not JSON strings | |
| fixed_message["tool_calls"] = rjl(message["tool_calls"]) | |
| # If we are an assistant, ensure the tool calls is a list otherwise the template will fail | |
| if fixed_message["role"] == "assistant": | |
| if fixed_message["tool_calls"] is None: | |
| fixed_message["tool_calls"] = [] | |
| fixed_chat.append(fixed_message) | |
| return fixed_chat | |
| def preprocess_nemotron_sft(examples): | |
| # First, get the proper column. | |
| chats = examples["messages"] | |
| # Check if we are batching or not and return the result accordingly | |
| batched = not isinstance(chats[0], dict) | |
| return ( | |
| [_fix_nemotron_chat(c) for c in chats] | |
| if batched | |
| else _fix_nemotron_chat(c) | |
| ) | |
| return (preprocess_nemotron_sft,) | |
| def _(tokenizer): | |
| # Process the dataset if needed | |
| def generate_prompts_text( | |
| examples, | |
| dataset_name: str | None = None, | |
| dataset_functions: dict[str, any] | None = None, | |
| ): | |
| # If we need to do any extra processing, here we do: | |
| if dataset_name and dataset_functions: | |
| examples = dataset_functions[dataset_name](examples) | |
| return { | |
| "text": tokenizer.apply_chat_template( | |
| examples, | |
| # We tokenize later with Unsloth | |
| tokenize=False, | |
| # Not sure why. Perhaps check? | |
| add_generation_prompt=False, | |
| ) | |
| } | |
| return (generate_prompts_text,) | |
| def _(model_name): | |
| dataset_repo = f"ItachiSan/{model_name}-SFT-chats" | |
| print(f"Dataset repo: {dataset_repo}") | |
| return (dataset_repo,) | |
| def _( | |
| dataset_name, | |
| dataset_repo, | |
| generate_prompts_text, | |
| model_name, | |
| os, | |
| preprocess_nemotron_sft, | |
| ): | |
| # v2: do evaluation | |
| from datasets import load_dataset | |
| # Fetch the dataset online. Disable only the first time we process the dataset. | |
| _fetch_online = True | |
| # Whether to push the dataset for refetching. Requires the repo to exist. | |
| _push_online = True | |
| # Choose how much to use for evaluation | |
| eval_split_size = 0.01 # 1% | |
| # Good number to detect how many processes we can run | |
| num_proc = int(os.cpu_count() * 0.75) + 1 | |
| if not _fetch_online: | |
| # Load the dataset, efficiently! | |
| dataset = load_dataset(dataset_name, split="train", num_proc=num_proc) | |
| # Format conversations using the model's chat template. | |
| # Adjust the column name (e.g., "conversations" or "messages") | |
| # to match your dataset's schema. | |
| # IMPORTANT CHANGES! | |
| # We need to drop the other columns, because... we suck | |
| # The main issue is that, in our patching of the Nemotron dataset, we convert data. | |
| # Then, pyarrow complains the JSON data we parse to build the text has different shape (duh...) | |
| # We bypass the problem by dropping all the other columns, because yes. | |
| chats = dataset.map( | |
| generate_prompts_text, | |
| fn_kwargs=dict( | |
| dataset_name=dataset_name, | |
| dataset_functions={ | |
| ## Add special handlers connections in here if needed. | |
| # Select proper column and decode JSON strings | |
| "nvidia/Nemotron-SFT-SWE-v3": preprocess_nemotron_sft, | |
| }, | |
| ), | |
| batched=True, | |
| remove_columns=dataset.column_names, | |
| num_proc=num_proc, | |
| ) | |
| chats_split = chats.train_test_split( | |
| test_size=0.01, # 1% for test size can also be an integer for # of rows | |
| shuffle=True, # Should always set to True! | |
| seed=3407, | |
| ) | |
| if _push_online: | |
| chats_split.push_to_hub( | |
| f"ItachiSan/{model_name}-SFT-chats", | |
| private=True, | |
| num_proc=num_proc, | |
| ) | |
| else: | |
| chats_split = load_dataset(dataset_repo, num_proc=num_proc) | |
| # Split the dataset | |
| train_dataset = chats_split["train"] # Dataset for training | |
| eval_dataset = chats_split["test"] # Dataset for evaluation | |
| train_dataset, eval_dataset | |
| return eval_dataset, num_proc, train_dataset | |
| def _(train_dataset): | |
| # Peek into the chats! | |
| from random import randint as _randint | |
| _chat_n = _randint(0, train_dataset.num_rows) | |
| print(f"Example chat (row {_chat_n}):\n{train_dataset[_chat_n]['text']}") | |
| return | |
| def _( | |
| batch_size, | |
| eval_dataset, | |
| gradient_steps, | |
| max_seq_length, | |
| model, | |
| model_repo, | |
| num_proc, | |
| tokenizer, | |
| torch, | |
| train_dataset, | |
| ): | |
| # Configure the SFTTrainer | |
| from trl import SFTTrainer, SFTConfig | |
| # Using the formula in the tool section, we can calculate the following. | |
| training_steps = int( | |
| round((train_dataset.num_rows / (batch_size * gradient_steps)) / 10) | |
| * 10 | |
| ) | |
| warmup_steps = training_steps * 0.05 | |
| print( | |
| f"Training {training_steps} steps with batch: {batch_size}, gradient: {gradient_steps}" | |
| ) | |
| _sft_args = SFTConfig( | |
| # Gemini recommended params | |
| # Numbers in comments are earlier ones, now they are calculated via function above. | |
| # Math is easy: 16x8 is based on GPU performance | |
| # Then, n_steps are dataset_size / (16x8) | |
| # Then warmup steps is 5% of the buffer | |
| per_device_train_batch_size=batch_size, | |
| gradient_accumulation_steps=gradient_steps, | |
| max_steps=training_steps, | |
| warmup_steps=warmup_steps, | |
| # Log not too frequently (original: 1) | |
| logging_steps=5, | |
| # Better on long trainings (original: linear) | |
| lr_scheduler_type="cosine", | |
| # Same as before | |
| learning_rate=2e-4, | |
| fp16=not torch.cuda.is_bf16_supported(), | |
| bf16=torch.cuda.is_bf16_supported(), | |
| optim="adamw_8bit", | |
| weight_decay=0.01, | |
| seed=3407, | |
| output_dir="outputs", | |
| report_to="none", | |
| # We do long training. Ensure we can resume. | |
| save_strategy="steps", | |
| save_steps=50, | |
| save_total_limit=5, | |
| # Store checkpoints on Hugging Face | |
| push_to_hub=True, | |
| hub_model_id=model_repo, | |
| hub_strategy="checkpoint", | |
| # v2: do eval | |
| eval_strategy="steps", # Runs eval every few steps or epochs. | |
| eval_steps=0.05, # Evaluate every 5% | |
| per_device_eval_batch_size=batch_size, # Ensure we eval quickly | |
| eval_accumulation_steps=gradient_steps, # You can increase this include of batch_size | |
| # TODO: add early stopping: https://unsloth.ai/docs/basics/finetuning-from-last-checkpoint#how-do-i-do-early-stopping | |
| ) | |
| trainer = SFTTrainer( | |
| model=model, | |
| tokenizer=tokenizer, | |
| dataset_text_field="text", # Here we point at our single column | |
| max_seq_length=max_seq_length, | |
| train_dataset=train_dataset, | |
| eval_dataset=eval_dataset, | |
| dataset_num_proc=num_proc, | |
| packing=False, # TODO: check wheter packing caused some issues or not | |
| args=_sft_args, | |
| ) | |
| trainer | |
| return (trainer,) | |
| def _(mo, model_repo, os): | |
| # Reload checkpoints if any | |
| from huggingface_hub import snapshot_download | |
| repo_id = model_repo | |
| local_output_dir = "outputs" | |
| # Automatically pull existing checkpoints from your HF Hub repo back into the sandbox | |
| try: | |
| print( | |
| f"π₯ Checking Hugging Face Hub for existing checkpoints in {repo_id}..." | |
| ) | |
| snapshot_download( | |
| repo_id=repo_id, | |
| local_dir=local_output_dir, | |
| allow_patterns=[ | |
| "checkpoint-*/**", | |
| "last-checkpoint/**", | |
| "*.json", | |
| "*.safetensors", | |
| ], | |
| ) | |
| print("β Checkpoints synced successfully from Hub.") | |
| except Exception as e: | |
| print(f"β οΈ No remote checkpoints found or repository is empty yet: {e}") | |
| # Detect the checkpoint if any | |
| existing_checkpoint = False | |
| if os.path.isdir(local_output_dir): | |
| existing_checkpoints = [ | |
| os.path.join(local_output_dir, d) | |
| for d in os.listdir(local_output_dir) | |
| if d.startswith("checkpoint-") or d == "last-checkpoint" | |
| ] | |
| existing_checkpoints.sort( | |
| key=lambda x: ( | |
| int(x.split("-")[-1]) | |
| if x.startswith("checkpoint") # covers local checkpoints | |
| else 42069 | |
| ) # for 'last-checkpoint' we use a very huge number | |
| ) | |
| if existing_checkpoints: # pythonic len(list) > 0 | |
| existing_checkpoint = existing_checkpoints[-1] | |
| mo.md(f"Checkpoint: {existing_checkpoint}") | |
| return (existing_checkpoint,) | |
| def _(mo): | |
| mo.md(""" | |
| # DO IT! | |
| """) | |
| return | |
| def _(os): | |
| # Print env vars if needed to check if some things need to change | |
| for _k, _v in sorted(os.environ.items(), key=lambda k: k[0]): | |
| print(f"{_k}={_v}") | |
| return | |
| def _(existing_checkpoint, model, tokenizer, trainer): | |
| # Finetune the model | |
| _force_training = False | |
| _starting_point = False | |
| if not _force_training: | |
| _starting_point = existing_checkpoint | |
| # Do the thing! | |
| trainer_stats = trainer.train(resume_from_checkpoint=_starting_point) | |
| lora_output_dir = "/marimo/model/lora" | |
| # Save the LoRA adapters and tokenizer | |
| model.save_pretrained(lora_output_dir) | |
| tokenizer.save_pretrained(lora_output_dir) | |
| print("Model saved to", lora_output_dir) | |
| trainer_stats | |
| return | |
| def _(model, model_repo, tokenizer): | |
| # Publish all the things! | |
| model.push_to_hub(model_repo) | |
| tokenizer.push_to_hub(model_repo) | |
| model.push_to_hub_merged( | |
| model_repo, | |
| tokenizer=tokenizer, | |
| save_method="merged_16bit", | |
| ) | |
| return | |
| def _(mo): | |
| mo.md(""" | |
| # Testing the model! | |
| """) | |
| return | |
| def _(os, torch): | |
| # If you get TorchDynamo errors like: | |
| # File "/tmp/torchinductor_root/r7/cr75u36gyrpc7ppml5xne6rruogr56gjefh5axla5phsc2ix5gjp.py" | |
| #, line 101, in call | |
| # buf2 = torch.ops.unsloth_zoo.routed_nf4_gate_up.default(many_args...) | |
| #NameError: name 's78' is not defined. Did you mean: 's68'? | |
| # Run this cell. It clears the corrupted TorchInductor cache and reset dynamo graphs | |
| import shutil | |
| shutil.rmtree('/tmp/torchinductor_root', ignore_errors=True) | |
| torch._dynamo.reset() | |
| # Also, disable all compilation and Dynamo tracing for inference to avoid shape generation bugs | |
| os.environ["UNSLOTH_NO_COMPILE"] = "1" | |
| os.environ["UNSLOTH_FULLGRAPH"] = "0" | |
| # If truly needed, run also the next line | |
| #os.environ["TORCH_COMPILE"] = "0" | |
| return | |
| def _(model_repo, os): | |
| # If available, load the LoRA directly. Otherwise, we fetch from the repo. | |
| # Used both for chat and export | |
| loadable_model_path = model_repo | |
| _lora_model_path = "/marimo/model/lora" | |
| if os.path.isfile(_lora_model_path + os.sep + "config.json"): | |
| loadable_model_path = _lora_model_path | |
| print("Model to load:", loadable_model_path) | |
| return (loadable_model_path,) | |
| def _(FastLanguageModel, loadable_model_path): | |
| # Load base model + adapters directly | |
| test_model, test_tokenizer = FastLanguageModel.from_pretrained( | |
| model_name=loadable_model_path, | |
| max_seq_length=2048, | |
| dtype=None, | |
| load_in_4bit=True, | |
| ) | |
| # Enable fast inference mode | |
| FastLanguageModel.for_inference(test_model) | |
| test_model, test_tokenizer | |
| return test_model, test_tokenizer | |
| def _(mo, test_model, test_tokenizer): | |
| # Hook our model in a chat box! | |
| def ui_chat_with_our_model(messages: list[mo.ai.ChatMessage], config: mo.ai.ChatModelConfig): | |
| # Match the custom messages objects to things we can work with: | |
| chat_history = [ | |
| {"role": message.role, "content": message.content} | |
| for message in messages | |
| ] | |
| # 1. Get those inputs matched | |
| inputs = test_tokenizer.apply_chat_template( | |
| chat_history, | |
| tokenize=True, | |
| add_generation_prompt=True, | |
| return_tensors="pt", | |
| ).to("cuda") | |
| # 3. Generate response | |
| return test_model.generate( | |
| input_ids=inputs, | |
| use_cache=True, | |
| max_new_tokens=config.max_tokens, | |
| temperature=config.temperature, | |
| top_p=config.top_p, | |
| top_k=config.top_k, | |
| #frequency_penalty=config.frequency_penalty, | |
| #presence_penalty=config.presence_penalty, | |
| ) | |
| mo.ui.chat( | |
| ui_chat_with_our_model, | |
| show_configuration_controls=True, | |
| ) | |
| return | |
| def _(mo): | |
| mo.md(""" | |
| # Export time! | |
| """) | |
| return | |
| def _(FastLanguageModel, loadable_model_path): | |
| # Wire the model for export | |
| print("π Loading uploaded model weights for export to GGUF...") | |
| export_model, export_tokenizer = FastLanguageModel.from_pretrained( | |
| model_name=loadable_model_path, | |
| max_seq_length=2048, | |
| dtype=None, | |
| load_in_4bit=True, | |
| ) | |
| print( | |
| "β Model loaded! Variables are `export_model` and `export_tokenizer`." | |
| ) | |
| export_model, export_tokenizer | |
| return export_model, export_tokenizer | |
| def _(mo, os): | |
| # Define change_dir to allow Unsloth to run properly. | |
| from contextlib import contextmanager | |
| def change_dir(path): | |
| prev_dir = os.getcwd() | |
| if not os.path.isdir(path): | |
| os.mkdir() | |
| os.chdir(path) | |
| try: | |
| yield | |
| finally: | |
| os.chdir(prev_dir) | |
| mo.md(""" | |
| `change_dir` is now defined. | |
| Remember to use it for Unsloth GGUF operations as such: | |
| ```python | |
| with change_dir("/home/marimo/.unsloth/llama.cpp"): | |
| do_the_thing. | |
| ``` | |
| """) | |
| return | |
| def _(manual_model, manual_tokenizer): | |
| print("π Merging adapters to 16-bit safetensors...") | |
| # Save the merged model as standard HF format first (bypasses GGUF conversion script) | |
| manual_model.save_pretrained_merged( | |
| "merged_model", | |
| manual_tokenizer, | |
| save_method="merged_16bit", # Merges LoRA into base weights | |
| ) | |
| print("β Merged model successfully saved to ./merged_model_16bit!") | |
| return | |
| def _(llama_cpp_dir): | |
| # Retrieve Unsloth llama.cpp | |
| # Original referece here: | |
| # https://github.com/unslothai/unsloth-zoo/blob/main/unsloth_zoo/llama_cpp.py | |
| print("π Fetching Unsloth llama.cpp ...") | |
| from unsloth_zoo.llama_cpp import install_llama_cpp | |
| install_llama_cpp( | |
| llama_cpp_folder=llama_cpp_dir, gpu_support=True, print_output=True | |
| ) | |
| print(f"β llama.cpp is now available at {llama_cpp_dir}") | |
| return | |
| def _(export_model, export_tokenizer): | |
| # First, export the model as merged bits | |
| model_merged_dir = "/marimo/model/merged" | |
| export_model.save_pretrained_merged( | |
| model_merged_dir, export_tokenizer, save_method="merged_16bit" | |
| ) | |
| return (model_merged_dir,) | |
| def _(llama_cpp_dir, mo_stream, model_merged_dir, model_name, os): | |
| # Second, convert to F16 GGUF as first step | |
| model_gguf_dir = "/marimo/model/gguf" | |
| os.makedirs(model_gguf_dir, exist_ok=True) | |
| def model_gguf_file(quant): | |
| return f"{model_gguf_dir}/{model_name}-{quant}.gguf" | |
| os.makedirs(model_gguf_dir, exist_ok=True) | |
| _f16_model_path = model_gguf_file("F16") | |
| if not os.path.isfile(_f16_model_path): | |
| mo_stream( | |
| "python", | |
| f"{llama_cpp_dir}/convert_hf_to_gguf.py", | |
| model_merged_dir, | |
| "--outfile", | |
| model_gguf_file("F16"), | |
| "--outtype", | |
| "f16", | |
| "--split-max-size", | |
| "50G", | |
| cwd=llama_cpp_dir, | |
| ) | |
| else: | |
| print("Conversion to F16 GGUF already done!") | |
| return model_gguf_dir, model_gguf_file | |
| def _(llama_cpp_dir, mo_stream, model_gguf_file, os): | |
| # Now quantize all the files! | |
| quantize_quants = [ | |
| "Q2_K", | |
| "Q3_K_M", | |
| "Q4_K_M", | |
| "Q5_K_M", | |
| "Q6_K", | |
| "Q8_0", | |
| "TQ1_0", | |
| "TQ2_0", | |
| ] | |
| for _quant in quantize_quants: | |
| # Handle special quant file naming | |
| _model_gguf_file = model_gguf_file(_quant) | |
| if _quant == "Q2_K": | |
| _model_gguf_file = _model_gguf_file.replace(_quant, "Q2_K_M") | |
| elif _quant == "Q6_K": | |
| _model_gguf_file = _model_gguf_file.replace(_quant, "Q6_K_M") | |
| elif _quant == "Q8_0": | |
| _model_gguf_file = _model_gguf_file.replace(_quant, "Q8_K_M") | |
| # Avoid doing the work again. | |
| if os.path.isfile(_model_gguf_file): | |
| print( | |
| f"β File for quant {_quant} already exists: {_model_gguf_file}" | |
| ) | |
| continue | |
| print(f"β¨ Working on quant: {_quant}") | |
| mo_stream( | |
| f"{llama_cpp_dir}/llama-quantize", | |
| model_gguf_file("F16"), | |
| _model_gguf_file, | |
| _quant, # quant type | |
| "16", # number of threads | |
| ) | |
| print(f"β File for quant {_quant} now at: {_model_gguf_file}") | |
| return | |
| def _(hf, model_gguf_dir, model_gguf_repo): | |
| # Finally push the stuff! | |
| print("Starting the upload...") | |
| hf.upload_folder( | |
| folder_path=model_gguf_dir, | |
| repo_id=model_gguf_repo, | |
| repo_type="model", | |
| allow_patterns=["*.gguf", "*.json", "*.md", "*.txt"], | |
| ) | |
| print("All GGUF files uploaded successfully!") | |
| return | |
| def _(mo): | |
| mo.md(""" | |
| # Experiments zone! | |
| """) | |
| return | |
| def _(mo): | |
| mo.md(r""" | |
| Nothing here for now... | |
| """) | |
| return | |
| if __name__ == "__main__": | |
| app.run() | |