Instructions to use safffrron/25M2111-Week02-Track2-40-Submission01 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use safffrron/25M2111-Week02-Track2-40-Submission01 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="safffrron/25M2111-Week02-Track2-40-Submission01")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("safffrron/25M2111-Week02-Track2-40-Submission01", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use safffrron/25M2111-Week02-Track2-40-Submission01 with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "safffrron/25M2111-Week02-Track2-40-Submission01" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "safffrron/25M2111-Week02-Track2-40-Submission01", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/safffrron/25M2111-Week02-Track2-40-Submission01
- SGLang
How to use safffrron/25M2111-Week02-Track2-40-Submission01 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 "safffrron/25M2111-Week02-Track2-40-Submission01" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "safffrron/25M2111-Week02-Track2-40-Submission01", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'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 "safffrron/25M2111-Week02-Track2-40-Submission01" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "safffrron/25M2111-Week02-Track2-40-Submission01", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use safffrron/25M2111-Week02-Track2-40-Submission01 with Docker Model Runner:
docker model run hf.co/safffrron/25M2111-Week02-Track2-40-Submission01
| """Loading, inspecting and slimming the Qwen3.5-4B checkpoint. | |
| Qwen3.5-4B is a hybrid multimodal model — 24 Gated DeltaNet (linear attention) | |
| layers interleaved with 8 full-attention layers, plus a 24-block vision tower | |
| and a multi-token-prediction head. For a text-only math benchmark the vision | |
| tower and MTP head are never executed, so they are ~455M parameters (9.7% of the | |
| checkpoint) of pure dead weight. | |
| Exact class names and module paths for this architecture vary across | |
| transformers releases, so everything here introspects the loaded module tree | |
| rather than hardcoding paths. ``describe_model`` exists to dump ground truth | |
| before we commit to any of it. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| import shutil | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Any | |
| import torch | |
| DEFAULT_MODEL = "Qwen/Qwen3.5-4B" | |
| # First matching pattern wins, so order matters: `visual`/`mtp` must precede the | |
| # generic `mlp`/`norm` patterns since those submodules contain MLPs too. | |
| BUDGET_GROUPS: tuple[tuple[str, str], ...] = ( | |
| ("vision", r"visual|vision"), | |
| ("mtp", r"(^|\.)mtp\."), | |
| ("embed", r"embed_tokens|lm_head"), | |
| ("linear_attn", r"linear_attn"), | |
| ("full_attn", r"self_attn"), | |
| ("mlp", r"\.mlp\."), | |
| ("norm", r"norm"), | |
| ) | |
| # Tiny but precision-critical: the SSM recurrence dynamics. Keeping every one of | |
| # these in fp32 costs ~3 MB, and quantizing them is what makes low-bit SSM | |
| # quantization collapse. | |
| SSM_SENSITIVE = r"A_log|dt_bias|conv1d|in_proj_a|in_proj_b" | |
| class GroupStats: | |
| name: str | |
| num_tensors: int = 0 | |
| num_params: int = 0 # unique storage only | |
| num_bytes: int = 0 # unique storage only | |
| raw_params: int = 0 # counting tied aliases twice | |
| num_aliases: int = 0 | |
| examples: list[str] = field(default_factory=list) | |
| def add(self, name: str, tensor: torch.Tensor, *, counted: bool) -> None: | |
| self.num_tensors += 1 | |
| self.raw_params += tensor.numel() | |
| if counted: | |
| self.num_params += tensor.numel() | |
| self.num_bytes += tensor.numel() * tensor.element_size() | |
| else: | |
| self.num_aliases += 1 | |
| if len(self.examples) < 3: | |
| self.examples.append(name + (" (tied alias)" if not counted else "")) | |
| def _group_for(name: str) -> str: | |
| for group, pattern in BUDGET_GROUPS: | |
| if re.search(pattern, name): | |
| return group | |
| return "other" | |
| def parameter_budget(state_dict: dict[str, torch.Tensor]) -> dict[str, Any]: | |
| """Group every tensor by component and report params / bytes per group. | |
| Tied weights (Qwen3.5 ties ``lm_head`` to ``embed_tokens``) appear twice in a | |
| ``state_dict`` while sharing one allocation. Counting both would overstate | |
| the checkpoint by 636M params / 1.27 GB here — and checkpoint bytes is the | |
| metric we are graded on — so aliases are detected by storage pointer and | |
| counted once. | |
| """ | |
| groups: dict[str, GroupStats] = {} | |
| seen_storage: dict[int, str] = {} | |
| aliases: list[dict[str, str]] = [] | |
| for name, tensor in state_dict.items(): | |
| if not isinstance(tensor, torch.Tensor): | |
| continue | |
| pointer = tensor.data_ptr() | |
| is_alias = pointer != 0 and pointer in seen_storage | |
| if is_alias: | |
| aliases.append({"name": name, "aliases": seen_storage[pointer]}) | |
| else: | |
| seen_storage[pointer] = name | |
| groups.setdefault(_group_for(name), GroupStats(_group_for(name))).add( | |
| name, tensor, counted=not is_alias | |
| ) | |
| total_params = sum(g.num_params for g in groups.values()) | |
| total_bytes = sum(g.num_bytes for g in groups.values()) | |
| return { | |
| "total_params": total_params, | |
| "total_bytes": total_bytes, | |
| "total_gb": total_bytes / 1e9, | |
| "raw_params_with_aliases": sum(g.raw_params for g in groups.values()), | |
| "tied_aliases": aliases, | |
| "groups": { | |
| name: { | |
| "num_tensors": g.num_tensors, | |
| "num_params": g.num_params, | |
| "num_bytes": g.num_bytes, | |
| "gb": g.num_bytes / 1e9, | |
| "num_aliases": g.num_aliases, | |
| "pct_params": 100 * g.num_params / total_params if total_params else 0.0, | |
| "examples": g.examples, | |
| } | |
| for name, g in sorted(groups.items(), key=lambda kv: -kv[1].num_params) | |
| }, | |
| } | |
| def checkpoint_size_bytes(state_dict: dict[str, torch.Tensor]) -> int: | |
| """Unique bytes in a state_dict — the number the leaderboard scores.""" | |
| return parameter_budget(state_dict)["total_bytes"] | |
| def format_budget(budget: dict[str, Any]) -> str: | |
| lines = [ | |
| f"{'component':<16}{'params':>16}{'% params':>11}{'GB':>9}{'tensors':>9}", | |
| "-" * 61, | |
| ] | |
| for name, stats in budget["groups"].items(): | |
| suffix = f" ({stats['num_aliases']} tied)" if stats["num_aliases"] else "" | |
| lines.append( | |
| f"{name:<16}{stats['num_params']:>16,}{stats['pct_params']:>10.1f}%" | |
| f"{stats['gb']:>9.3f}{stats['num_tensors']:>9}{suffix}" | |
| ) | |
| lines.append("-" * 61) | |
| lines.append( | |
| f"{'TOTAL':<16}{budget['total_params']:>16,}{100.0:>10.1f}%" | |
| f"{budget['total_gb']:>9.3f}" | |
| ) | |
| if budget.get("tied_aliases"): | |
| excess = budget["raw_params_with_aliases"] - budget["total_params"] | |
| lines.append( | |
| f" ({len(budget['tied_aliases'])} tied alias tensor(s) counted once; " | |
| f"naive state_dict sum would overstate by {excess:,} params)" | |
| ) | |
| return "\n".join(lines) | |
| def load_model( | |
| model_id: str = DEFAULT_MODEL, | |
| dtype: str = "bfloat16", | |
| device_map: str | None = "auto", | |
| cache_dir: str | None = None, | |
| device: str | None = None, | |
| multimodal: bool = False, | |
| ): | |
| """Load the model, trying each plausible auto-class for this architecture. | |
| On transformers >= 5, ``AutoModelForCausalLM`` resolves Qwen3.5 to | |
| ``Qwen3_5ForCausalLM`` and materializes only the language model — the vision | |
| tower and MTP head are never allocated, so the 455M-param text-only saving | |
| happens for free at load time. | |
| Pass ``device`` to pin the whole model to one GPU. At 8.4 GB it fits on any | |
| of ours, and ``device_map="auto"`` across several GPUs would pipeline-shard | |
| it instead, adding cross-device hops on every layer for no benefit. | |
| """ | |
| import transformers | |
| torch_dtype = { | |
| "bfloat16": torch.bfloat16, | |
| "float16": torch.float16, | |
| "float32": torch.float32, | |
| }[dtype] | |
| kwargs: dict[str, Any] = { | |
| "dtype": torch_dtype, | |
| "trust_remote_code": True, | |
| } | |
| if device is not None: | |
| kwargs["device_map"] = {"": device} | |
| elif device_map is not None: | |
| kwargs["device_map"] = device_map | |
| if cache_dir: | |
| kwargs["cache_dir"] = cache_dir | |
| candidates = [ | |
| "AutoModelForCausalLM", | |
| "AutoModelForImageTextToText", | |
| "AutoModelForVision2Seq", | |
| "AutoModel", | |
| ] | |
| if multimodal: | |
| # vLLM only registers Qwen3_5ForConditionalGeneration and rejects a | |
| # text-only Qwen3_5TextConfig (vllm#39231). Any checkpoint we intend to | |
| # evaluate with vLLM must therefore keep the multimodal wrapper, even | |
| # though the vision tower is never executed for a math prompt. | |
| candidates = [ | |
| "Qwen3_5ForConditionalGeneration", | |
| "AutoModelForImageTextToText", | |
| "AutoModelForVision2Seq", | |
| "AutoModelForCausalLM", | |
| ] | |
| errors: list[str] = [] | |
| for class_name in candidates: | |
| auto_class = getattr(transformers, class_name, None) | |
| if auto_class is None: | |
| continue | |
| try: | |
| model = auto_class.from_pretrained(model_id, **kwargs) | |
| model.eval() | |
| print(f"[model] loaded {model_id} via {class_name}") | |
| return model | |
| except Exception as exc: # noqa: BLE001 - we want the full error list | |
| errors.append(f" {class_name}: {type(exc).__name__}: {exc}") | |
| raise RuntimeError( | |
| f"Could not load {model_id} with any auto-class.\n" + "\n".join(errors) | |
| ) | |
| def load_tokenizer(model_id: str = DEFAULT_MODEL, cache_dir: str | None = None): | |
| from transformers import AutoTokenizer | |
| kwargs: dict[str, Any] = {"trust_remote_code": True} | |
| if cache_dir: | |
| kwargs["cache_dir"] = cache_dir | |
| tokenizer = AutoTokenizer.from_pretrained(model_id, **kwargs) | |
| if tokenizer.pad_token is None: | |
| tokenizer.pad_token = tokenizer.eos_token | |
| tokenizer.padding_side = "left" # required for correct batched generation | |
| return tokenizer | |
| def estimate_memory( | |
| model: torch.nn.Module, batch_size: int, seq_len: int | |
| ) -> dict[str, float]: | |
| """Predict peak VRAM for a generation run, so OOM is caught before it costs an hour. | |
| Only the 8 full-attention layers hold a growing KV cache; the 24 Gated | |
| DeltaNet layers keep a fixed-size recurrent state regardless of length, | |
| which is why this model's cache is far cheaper than a dense transformer's. | |
| """ | |
| config = getattr(model, "config", None) | |
| config = getattr(config, "text_config", config) | |
| layer_types = getattr(config, "layer_types", None) | |
| num_layers = getattr(config, "num_hidden_layers", 32) | |
| if layer_types: | |
| num_full = sum(1 for t in layer_types if "full" in str(t)) | |
| else: | |
| interval = getattr(config, "full_attention_interval", 4) | |
| num_full = num_layers // interval | |
| kv_heads = getattr(config, "num_key_value_heads", 4) | |
| head_dim = getattr(config, "head_dim", 256) | |
| dtype_size = 2 | |
| kv_bytes_per_token = num_full * kv_heads * head_dim * 2 * dtype_size | |
| kv_bytes = kv_bytes_per_token * seq_len * batch_size | |
| # DynamicCache grows by torch.cat on every decode step: allocate (n+1), | |
| # copy, free n. Peak is therefore ~2x the steady-state cache, which is | |
| # exactly the "ladder" you see climbing in nvtop until it OOMs. | |
| # expandable_segments lets the allocator grow a segment in place, cutting | |
| # the transient sharply -- but we still budget for it. | |
| growth_factor = 1.4 if "expandable_segments" in os.environ.get( | |
| "PYTORCH_CUDA_ALLOC_CONF", "" | |
| ) else 2.0 | |
| kv_peak_bytes = kv_bytes * growth_factor | |
| # Gated DeltaNet recurrent state: [v_heads, k_dim, v_dim] per layer, length-independent. | |
| v_heads = getattr(config, "linear_num_value_heads", 32) | |
| k_dim = getattr(config, "linear_key_head_dim", 128) | |
| v_dim = getattr(config, "linear_value_head_dim", 128) | |
| state_bytes = (num_layers - num_full) * v_heads * k_dim * v_dim * 4 * batch_size | |
| weight_bytes = sum(p.numel() * p.element_size() for p in model.parameters()) | |
| return { | |
| "weights_gb": weight_bytes / 1e9, | |
| "kv_cache_gb": kv_bytes / 1e9, | |
| "kv_peak_gb": kv_peak_bytes / 1e9, | |
| "ssm_state_gb": state_bytes / 1e9, | |
| "steady_gb": (weight_bytes + kv_bytes + state_bytes) / 1e9, | |
| # What the preflight must check against: peak, not steady state. | |
| "total_gb": (weight_bytes + kv_peak_bytes + state_bytes) / 1e9, | |
| "kv_bytes_per_token": kv_bytes_per_token, | |
| "growth_factor": growth_factor, | |
| } | |
| def free_vram_gb(device: str | torch.device) -> tuple[float, float]: | |
| """(free, total) GB on ``device`` — reflects other users' jobs on a shared box.""" | |
| free, total = torch.cuda.mem_get_info(torch.device(device)) | |
| return free / 1e9, total / 1e9 | |
| def describe_model(model: torch.nn.Module, max_depth: int = 3) -> dict[str, Any]: | |
| """Summarize the module tree — the ground truth we build stripping on.""" | |
| tree: list[dict[str, Any]] = [] | |
| for name, module in model.named_modules(): | |
| depth = name.count(".") | |
| if name and depth <= max_depth: | |
| n_params = sum(p.numel() for p in module.parameters(recurse=True)) | |
| tree.append( | |
| { | |
| "path": name, | |
| "class": type(module).__name__, | |
| "depth": depth, | |
| "num_params": n_params, | |
| } | |
| ) | |
| state_dict = model.state_dict() | |
| sensitive = [n for n in state_dict if re.search(SSM_SENSITIVE, n)] | |
| return { | |
| "model_class": type(model).__name__, | |
| "num_parameters": sum(p.numel() for p in model.parameters()), | |
| "module_tree": tree, | |
| "budget": parameter_budget(state_dict), | |
| "ssm_sensitive_tensors": { | |
| "count": len(sensitive), | |
| "num_params": sum(state_dict[n].numel() for n in sensitive), | |
| "names": sensitive[:20], | |
| }, | |
| "state_dict_prefixes": sorted( | |
| {".".join(n.split(".")[:2]) for n in state_dict} | |
| ), | |
| } | |
| def find_submodule(model: torch.nn.Module, pattern: str) -> list[str]: | |
| """Paths of modules whose name matches ``pattern`` and whose parent does not.""" | |
| regex = re.compile(pattern) | |
| hits = [name for name, _ in model.named_modules() if name and regex.search(name)] | |
| # Keep only the shallowest match on each branch. | |
| return [h for h in hits if not any(h.startswith(o + ".") for o in hits)] | |
| def strip_unused_modules( | |
| model: torch.nn.Module, | |
| drop_vision: bool = True, | |
| drop_mtp: bool = True, | |
| ) -> dict[str, Any]: | |
| """Delete text-irrelevant submodules in place; return what was removed. | |
| The vision tower and MTP head together are ~455M params. A text-only math | |
| eval never executes either: the ViT has no image inputs, and the MTP head is | |
| a speculative-decoding draft head that HF ``generate()`` does not call. | |
| """ | |
| removed: list[dict[str, Any]] = [] | |
| targets: list[str] = [] | |
| if drop_vision: | |
| targets.append(r"(^|\.)(visual|vision_tower)$") | |
| if drop_mtp: | |
| targets.append(r"(^|\.)mtp$") | |
| for pattern in targets: | |
| for path in find_submodule(model, pattern): | |
| parent = model | |
| parts = path.split(".") | |
| for part in parts[:-1]: | |
| parent = getattr(parent, part) | |
| child = getattr(parent, parts[-1], None) | |
| if child is None: | |
| continue | |
| n_params = sum(p.numel() for p in child.parameters(recurse=True)) | |
| setattr(parent, parts[-1], None) | |
| removed.append({"path": path, "num_params": n_params}) | |
| # transformers >= 5 loads Qwen3.5 through Qwen3_5ForCausalLM, which never | |
| # allocates the vision tower or MTP head. Finding nothing to remove is the | |
| # expected outcome there, not a failure. | |
| already_text_only = not removed and not find_submodule(model, r"visual|vision_tower|mtp") | |
| return { | |
| "removed": removed, | |
| "num_params_removed": sum(r["num_params"] for r in removed), | |
| "bytes_removed_bf16": 2 * sum(r["num_params"] for r in removed), | |
| "already_text_only": already_text_only, | |
| } | |
| # Files the model repo carries that `save_pretrained` does not reproduce. | |
| # vLLM's multimodal path builds an image processor even for a text-only prompt, | |
| # so a checkpoint missing `preprocessor_config.json` fails to load outright. | |
| AUXILIARY_FILES = ( | |
| "preprocessor_config.json", | |
| "video_preprocessor_config.json", | |
| "chat_template.jinja", | |
| ) | |
| def save_checkpoint( | |
| model: torch.nn.Module, | |
| out_dir: str | Path, | |
| source_model: str = DEFAULT_MODEL, | |
| cache_dir: str | None = None, | |
| ) -> list[str]: | |
| """Write a checkpoint that vLLM and transformers can both load. | |
| ``model.save_pretrained`` emits weights + config, and the tokenizer covers | |
| vocab/merges, but the processor and chat-template files come from the source | |
| repo and must be copied across explicitly. | |
| """ | |
| out = Path(out_dir) | |
| out.mkdir(parents=True, exist_ok=True) | |
| model.save_pretrained(out) | |
| load_tokenizer(source_model, cache_dir=cache_dir).save_pretrained(out) | |
| copied: list[str] = [] | |
| source_dir = Path(source_model) | |
| for filename in AUXILIARY_FILES: | |
| target = out / filename | |
| if target.exists(): | |
| copied.append(filename) | |
| continue | |
| try: | |
| if source_dir.is_dir(): | |
| candidate = source_dir / filename | |
| if not candidate.is_file(): | |
| continue | |
| shutil.copy(candidate, target) | |
| else: | |
| from huggingface_hub import hf_hub_download | |
| downloaded = hf_hub_download( | |
| source_model, filename, cache_dir=cache_dir | |
| ) | |
| shutil.copy(downloaded, target) | |
| copied.append(filename) | |
| except Exception as exc: # noqa: BLE001 - optional files; report and continue | |
| print(f"[save_checkpoint] could not copy {filename}: {exc}") | |
| return copied | |