Image-Text-to-Text
Transformers
English
vision-language-model
vlm
surveillance
iot
gemma
vl-jepa
multimodal
object-detection
video-analytics
Instructions to use hardiksa/arcisvlm with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use hardiksa/arcisvlm with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="hardiksa/arcisvlm")# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("hardiksa/arcisvlm", dtype="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use hardiksa/arcisvlm with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "hardiksa/arcisvlm" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "hardiksa/arcisvlm", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/hardiksa/arcisvlm
- SGLang
How to use hardiksa/arcisvlm 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 "hardiksa/arcisvlm" \ --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": "hardiksa/arcisvlm", "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 "hardiksa/arcisvlm" \ --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": "hardiksa/arcisvlm", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use hardiksa/arcisvlm with Docker Model Runner:
docker model run hf.co/hardiksa/arcisvlm
| #!/usr/bin/env python3 | |
| """ | |
| Stage 2: Instruction Tuning — DDP Training on 8x A100. | |
| Loads Stage 1 checkpoint, freezes X-Encoder, trains Predictor + MoE Decoder | |
| on VQA/instruction data using DistributedDataParallel. | |
| Usage: | |
| torchrun --nproc_per_node=8 scripts/train_stage2_ddp.py \\ | |
| --config configs/scale_1.3b.yaml \\ | |
| --stage1_ckpt checkpoints/stage1_final.pt | |
| torchrun --nproc_per_node=8 scripts/train_stage2_ddp.py \\ | |
| --config configs/scale_1.3b.yaml \\ | |
| --stage1_ckpt checkpoints/stage1_final.pt \\ | |
| --resume checkpoints/stage2_epoch2.pt | |
| """ | |
| import argparse | |
| import math | |
| import os | |
| import sys | |
| import time | |
| import torch | |
| import torch.distributed as dist | |
| import torch.nn as nn | |
| from torch.nn.parallel import DistributedDataParallel as DDP | |
| from torch.utils.data import DataLoader, Dataset | |
| from torch.utils.data.distributed import DistributedSampler | |
| import yaml | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from model.vlm import VLJEPAModel | |
| from model.tokenizer import BPETokenizer | |
| # --------------------------------------------------------------------------- | |
| # Dataset helpers | |
| # --------------------------------------------------------------------------- | |
| def build_stage2_dataset(config: dict, tokenizer) -> Dataset: | |
| """ | |
| Build Stage 2 instruction-tuning dataset from LLaVA-Instruct/VQAv2/GQA/etc. | |
| Raises RuntimeError if no real data is found. | |
| """ | |
| img_size = config["vision"]["img_size"] | |
| vocab_size = config["decoder"]["vocab_size"] | |
| # Try loading real data from local JSONL files | |
| # Check fullscale first, then standard | |
| jsonl_dir = None | |
| for candidate in ["data/downloads/stage2_fullscale", "data/downloads/stage2"]: | |
| if os.path.exists(candidate): | |
| jsonl_dir = candidate | |
| break | |
| if jsonl_dir: | |
| try: | |
| import json | |
| class LocalJSONLDataset(Dataset): | |
| """Load VQA/instruction data from local JSONL files.""" | |
| def __init__(self, jsonl_dir, tokenizer, img_size=448, max_q=64, max_a=128): | |
| self.samples = [] | |
| self.tokenizer = tokenizer | |
| self.max_q = max_q | |
| self.max_a = max_a | |
| self.img_size = img_size | |
| self.vocab_size = tokenizer.vocab_size if hasattr(tokenizer, 'vocab_size') else 32768 | |
| # Load all JSONL files | |
| for fname in sorted(os.listdir(jsonl_dir)): | |
| if fname.endswith('.jsonl'): | |
| fpath = os.path.join(jsonl_dir, fname) | |
| with open(fpath) as f: | |
| for line in f: | |
| try: | |
| item = json.loads(line.strip()) | |
| self.samples.append(item) | |
| except json.JSONDecodeError: | |
| continue | |
| def __len__(self): | |
| return len(self.samples) | |
| def __getitem__(self, idx): | |
| item = self.samples[idx] | |
| # Extract question and answer from various formats | |
| question = "" | |
| answer = "" | |
| # LLaVA-Instruct format: conversations list | |
| if "conversations" in item: | |
| convos = item["conversations"] | |
| if isinstance(convos, list) and len(convos) >= 2: | |
| question = convos[0].get("value", "") if isinstance(convos[0], dict) else str(convos[0]) | |
| answer = convos[1].get("value", "") if isinstance(convos[1], dict) else str(convos[1]) | |
| # VQAv2/GQA format | |
| if not question: | |
| question = item.get("question", item.get("text", "What do you see?")) | |
| if not answer: | |
| answer = item.get("answer", item.get("multiple_choice_answer", "")) | |
| if not answer and "answers" in item: | |
| answers = item["answers"] | |
| if isinstance(answers, list) and answers: | |
| if isinstance(answers[0], dict): | |
| answer = answers[0].get("answer", "") | |
| else: | |
| answer = str(answers[0]) | |
| if not answer: | |
| answer = "unknown" | |
| # Load real image — crash if missing or corrupted | |
| image_path = item.get("image_path") | |
| if image_path and os.path.exists(image_path): | |
| try: | |
| from PIL import Image as PILImage | |
| from torchvision import transforms | |
| _transform = transforms.Compose([ | |
| transforms.Resize((self.img_size, self.img_size)), | |
| transforms.ToTensor(), | |
| transforms.Normalize( | |
| mean=[0.485, 0.456, 0.406], | |
| std=[0.229, 0.224, 0.225], | |
| ), | |
| ]) | |
| image = _transform(PILImage.open(image_path).convert("RGB")) | |
| except Exception as e: | |
| raise FileNotFoundError(f"Image not found or corrupted: {image_path}. Original error: {e}") | |
| else: | |
| raise FileNotFoundError( | |
| f"Image not found: {image_path}. Data may be corrupted or incomplete." | |
| ) | |
| # Tokenize | |
| q_ids = self.tokenizer.encode(str(question)) | |
| a_ids = self.tokenizer.encode(str(answer)) | |
| # Pad/truncate | |
| q_ids = self._pad(q_ids, self.max_q) | |
| a_ids = self._pad(a_ids, self.max_a) | |
| q_tensor = torch.tensor(q_ids, dtype=torch.long) | |
| a_tensor = torch.tensor(a_ids, dtype=torch.long) | |
| return { | |
| "image": image, | |
| "question_ids": q_tensor, | |
| "question_mask": (q_tensor != self.tokenizer.pad_id).long(), | |
| "answer_ids": a_tensor, | |
| "answer_mask": (a_tensor != self.tokenizer.pad_id).long(), | |
| } | |
| def _pad(self, ids, max_len): | |
| if len(ids) > max_len: | |
| return ids[:max_len] | |
| return ids + [self.tokenizer.pad_id] * (max_len - len(ids)) | |
| dataset = LocalJSONLDataset(jsonl_dir, tokenizer, img_size) | |
| if len(dataset) > 100: | |
| print(f" [REAL DATA] Local JSONL: {len(dataset)} samples from {jsonl_dir}") | |
| return dataset | |
| except Exception as e: | |
| print(f" [WARN] Local JSONL loading failed: {e}") | |
| # Try loading from data/multi_dataset.py | |
| try: | |
| from data.multi_dataset import build_stage2_dataset as _build | |
| dataset = _build(config, tokenizer) | |
| if len(dataset) > 0: | |
| return dataset | |
| except (ImportError, Exception) as e: | |
| print(f" [WARN] multi_dataset loading failed: {e}") | |
| # Try local VQA dataset | |
| try: | |
| from data.dataset import VQADataset | |
| dataset = VQADataset( | |
| image_dir="data/vqav2/val2014", | |
| questions_file="data/vqav2/v2_OpenEnded_mscoco_val2014_questions.json", | |
| annotations_file="data/vqav2/v2_mscoco_val2014_annotations.json", | |
| tokenizer=tokenizer, | |
| img_size=img_size, | |
| ) | |
| if len(dataset) > 0: | |
| return dataset | |
| except Exception: | |
| pass | |
| raise RuntimeError( | |
| "FATAL: No Stage 2 training data found.\n" | |
| "Download real data first: python3 scripts/download_all_data.py --stage 2\n" | |
| "Required: data/downloads/stage2/ or data/downloads/stage2_fullscale/ with JSONL files" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # LR scheduler (shared with Stage 1) | |
| # --------------------------------------------------------------------------- | |
| class CosineWarmupScheduler(torch.optim.lr_scheduler._LRScheduler): | |
| """Linear warmup then cosine decay.""" | |
| def __init__(self, optimizer, warmup_steps: int, total_steps: int, | |
| min_lr: float = 1e-7, last_epoch: int = -1): | |
| self.warmup_steps = warmup_steps | |
| self.total_steps = total_steps | |
| self.min_lr = min_lr | |
| super().__init__(optimizer, last_epoch) | |
| def get_lr(self): | |
| step = self.last_epoch | |
| if step < self.warmup_steps: | |
| scale = step / max(1, self.warmup_steps) | |
| return [base_lr * scale for base_lr in self.base_lrs] | |
| else: | |
| progress = (step - self.warmup_steps) / max(1, self.total_steps - self.warmup_steps) | |
| cosine = 0.5 * (1.0 + math.cos(math.pi * progress)) | |
| return [self.min_lr + (base_lr - self.min_lr) * cosine for base_lr in self.base_lrs] | |
| # --------------------------------------------------------------------------- | |
| # Helpers | |
| # --------------------------------------------------------------------------- | |
| def setup_distributed(): | |
| dist.init_process_group(backend="nccl") | |
| local_rank = int(os.environ["LOCAL_RANK"]) | |
| torch.cuda.set_device(local_rank) | |
| return local_rank | |
| def cleanup(): | |
| if dist.is_initialized(): | |
| dist.destroy_process_group() | |
| def is_rank0(): | |
| return not dist.is_initialized() or dist.get_rank() == 0 | |
| def log(msg: str): | |
| if is_rank0(): | |
| print(msg, flush=True) | |
| def save_checkpoint(model, optimizer, scheduler, epoch, global_step, loss, path): | |
| if not is_rank0(): | |
| return | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| state_dict = model.module.state_dict() if hasattr(model, "module") else model.state_dict() | |
| torch.save({ | |
| "epoch": epoch, | |
| "global_step": global_step, | |
| "model_state_dict": state_dict, | |
| "optimizer_state_dict": optimizer.state_dict(), | |
| "scheduler_state_dict": scheduler.state_dict(), | |
| "loss": loss, | |
| }, path) | |
| log(f" Checkpoint saved: {path}") | |
| def push_checkpoints(): | |
| """Push checkpoints to GitHub LFS. Disabled during training to avoid git lock issues. | |
| Call scripts/push_checkpoints.py manually after training completes.""" | |
| pass # Disabled — run push_checkpoints.py separately after training | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| def main(): | |
| parser = argparse.ArgumentParser(description="Stage 2 DDP: Instruction Tuning") | |
| parser.add_argument("--config", type=str, required=True, help="Path to YAML config") | |
| parser.add_argument("--stage1_ckpt", type=str, default="checkpoints/stage1_final.pt", | |
| help="Path to Stage 1 checkpoint") | |
| parser.add_argument("--resume", type=str, default=None, | |
| help="Path to Stage 2 checkpoint to resume from") | |
| args = parser.parse_args() | |
| # ---- Distributed setup ---- | |
| local_rank = setup_distributed() | |
| world_size = dist.get_world_size() | |
| global_rank = dist.get_rank() | |
| device = torch.device(f"cuda:{local_rank}") | |
| # ---- Config ---- | |
| with open(args.config) as f: | |
| config = yaml.safe_load(f) | |
| stage_cfg = config["train_stage2"] | |
| per_gpu_batch = stage_cfg["batch_size"] // world_size | |
| grad_accum = stage_cfg.get("gradient_accumulation", 4) | |
| max_epochs = stage_cfg["max_epochs"] | |
| lr = stage_cfg["learning_rate"] | |
| warmup_steps = stage_cfg["warmup_steps"] | |
| grad_clip = stage_cfg["gradient_clip"] | |
| lb_weight = stage_cfg["load_balance_weight"] | |
| log("=" * 70) | |
| log("ArcisVLM — Stage 2: Instruction Tuning (DDP)") | |
| log("=" * 70) | |
| log(f" World size: {world_size}") | |
| log(f" Global batch: {stage_cfg['batch_size']}") | |
| log(f" Per-GPU batch: {per_gpu_batch}") | |
| log(f" Gradient accumulation:{grad_accum}") | |
| log(f" Effective batch: {stage_cfg['batch_size'] * grad_accum}") | |
| log(f" Max epochs: {max_epochs}") | |
| log(f" Learning rate: {lr}") | |
| log(f" Warmup steps: {warmup_steps}") | |
| log(f" Load balance weight: {lb_weight}") | |
| log(f" Precision: {stage_cfg.get('precision', 'bf16')}") | |
| # ---- Tokenizer ---- | |
| tokenizer = BPETokenizer(vocab_size=config["decoder"]["vocab_size"]) | |
| for tok_path in ["checkpoints/tokenizer_32k.json", "checkpoints/tokenizer.json"]: | |
| if os.path.exists(tok_path): | |
| tokenizer.load(tok_path) | |
| log(f" Tokenizer: {len(tokenizer)} tokens (from {tok_path})") | |
| break | |
| else: | |
| log(" [WARN] No tokenizer found — using untrained tokenizer") | |
| # ---- Dataset ---- | |
| dataset = build_stage2_dataset(config, tokenizer) | |
| sampler = DistributedSampler(dataset, num_replicas=world_size, rank=global_rank, shuffle=True) | |
| loader = DataLoader( | |
| dataset, | |
| batch_size=per_gpu_batch, | |
| sampler=sampler, | |
| num_workers=4, | |
| pin_memory=True, | |
| drop_last=True, | |
| ) | |
| log(f" Dataset: {len(dataset)} samples, {len(loader)} batches/GPU") | |
| # ---- Model + Stage 1 checkpoint ---- | |
| model = VLJEPAModel(config).to(device) | |
| if os.path.exists(args.stage1_ckpt): | |
| ckpt = torch.load(args.stage1_ckpt, map_location=device, weights_only=False) | |
| model.load_state_dict(ckpt["model_state_dict"]) | |
| log(f" Loaded Stage 1 ckpt: {args.stage1_ckpt} (epoch {ckpt['epoch']}, loss {ckpt['loss']:.4f})") | |
| else: | |
| log(f" [WARN] Stage 1 checkpoint not found: {args.stage1_ckpt} — training from scratch") | |
| # Freeze X-Encoder | |
| model.freeze_x_encoder() | |
| if is_rank0(): | |
| params = model.count_parameters() | |
| for k, v in params.items(): | |
| log(f" {k}: {v:,}") | |
| # ---- Optimizer (only trainable params) ---- | |
| trainable_params = [p for p in model.parameters() if p.requires_grad] | |
| optimizer = torch.optim.AdamW(trainable_params, lr=lr, weight_decay=0.01) | |
| # ---- Scheduler ---- | |
| # Account for gradient accumulation in total optimizer steps | |
| steps_per_epoch = math.ceil(len(loader) / grad_accum) | |
| total_steps = max_epochs * steps_per_epoch | |
| scheduler = CosineWarmupScheduler(optimizer, warmup_steps=warmup_steps, total_steps=total_steps) | |
| # ---- Mixed precision ---- | |
| use_bf16 = stage_cfg.get("precision", "bf16") == "bf16" | |
| scaler = torch.amp.GradScaler("cuda", enabled=(not use_bf16)) | |
| autocast_dtype = torch.bfloat16 if use_bf16 else torch.float16 | |
| # ---- Resume ---- | |
| start_epoch = 0 | |
| global_step = 0 | |
| if args.resume and os.path.exists(args.resume): | |
| ckpt = torch.load(args.resume, map_location=device, weights_only=False) | |
| model.load_state_dict(ckpt["model_state_dict"]) | |
| optimizer.load_state_dict(ckpt["optimizer_state_dict"]) | |
| if "scheduler_state_dict" in ckpt: | |
| scheduler.load_state_dict(ckpt["scheduler_state_dict"]) | |
| start_epoch = ckpt["epoch"] | |
| global_step = ckpt.get("global_step", start_epoch * steps_per_epoch) | |
| log(f" Resumed from {args.resume} (epoch {start_epoch}, loss {ckpt['loss']:.4f})") | |
| # ---- DDP wrap ---- | |
| model = DDP(model, device_ids=[local_rank], output_device=local_rank, | |
| find_unused_parameters=True) # True because X-Encoder is frozen | |
| # ---- Training loop ---- | |
| model.train() | |
| os.makedirs("checkpoints", exist_ok=True) | |
| for epoch in range(start_epoch, max_epochs): | |
| sampler.set_epoch(epoch) | |
| epoch_loss = 0.0 | |
| epoch_decode_loss = 0.0 | |
| epoch_lb_loss = 0.0 | |
| epoch_steps = 0 | |
| epoch_start = time.time() | |
| optimizer.zero_grad(set_to_none=True) | |
| for batch_idx, batch in enumerate(loader): | |
| images = batch["image"].to(device, non_blocking=True) | |
| # Handle both DummyVQADataset (question_ids) and UnifiedVLMDataset (instruction_ids) keys | |
| q_ids = batch.get("question_ids", batch.get("instruction_ids")) | |
| if q_ids is None: | |
| q_ids = torch.zeros(images.shape[0], 64, dtype=torch.long) | |
| q_ids = q_ids.to(device, non_blocking=True) | |
| q_mask = batch.get("question_mask", batch.get("instruction_mask")) | |
| if q_mask is None: | |
| q_mask = torch.ones_like(q_ids) | |
| q_mask = q_mask.to(device, non_blocking=True) | |
| a_ids = batch["answer_ids"].to(device, non_blocking=True) | |
| with torch.amp.autocast("cuda", dtype=autocast_dtype): | |
| output = model.module.forward_stage2( | |
| images=images, | |
| query_ids=q_ids, | |
| query_padding_mask=q_mask, | |
| answer_ids=a_ids, | |
| load_balance_weight=lb_weight, | |
| ) | |
| loss = output["loss"] / grad_accum # Scale for accumulation | |
| if use_bf16: | |
| loss.backward() | |
| else: | |
| scaler.scale(loss).backward() | |
| epoch_loss += output["loss"].item() | |
| epoch_decode_loss += output["decode_loss"].item() | |
| epoch_lb_loss += output["load_balance_loss"].item() | |
| epoch_steps += 1 | |
| # Optimizer step every grad_accum batches | |
| if (batch_idx + 1) % grad_accum == 0: | |
| if use_bf16: | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip) | |
| optimizer.step() | |
| else: | |
| scaler.unscale_(optimizer) | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip) | |
| scaler.step(optimizer) | |
| scaler.update() | |
| scheduler.step() | |
| optimizer.zero_grad(set_to_none=True) | |
| global_step += 1 | |
| # Log every 50 optimizer steps | |
| if global_step % 50 == 0: | |
| current_lr = scheduler.get_last_lr()[0] | |
| gpu_mem = torch.cuda.max_memory_allocated(device) / 1e9 | |
| log(f" [Step {global_step}] loss={output['loss'].item():.4f} " | |
| f"decode={output['decode_loss'].item():.4f} " | |
| f"lb={output['load_balance_loss'].item():.4f} " | |
| f"lr={current_lr:.2e} GPU={gpu_mem:.1f}GB") | |
| # ---- Epoch summary ---- | |
| metrics = torch.tensor([epoch_loss, epoch_decode_loss, epoch_lb_loss, epoch_steps], | |
| device=device, dtype=torch.float64) | |
| dist.all_reduce(metrics, op=dist.ReduceOp.SUM) | |
| avg_loss = (metrics[0] / metrics[3]).item() | |
| avg_decode = (metrics[1] / metrics[3]).item() | |
| avg_lb = (metrics[2] / metrics[3]).item() | |
| epoch_time = time.time() - epoch_start | |
| current_lr = scheduler.get_last_lr()[0] | |
| gpu_mem = torch.cuda.max_memory_allocated(device) / 1e9 | |
| log(f"\nEpoch {epoch + 1}/{max_epochs}: loss={avg_loss:.4f} decode={avg_decode:.4f} " | |
| f"lb={avg_lb:.4f} lr={current_lr:.2e} time={epoch_time:.0f}s GPU={gpu_mem:.1f}GB") | |
| # ---- Go/No-Go Gate 3: decode_loss < 2.0 after epoch 1 ---- | |
| # NOTE: Gate threshold is lenient for small/dummy datasets. | |
| # On real VQA data with 100K+ samples, decode_loss should drop below 2.0. | |
| # On dummy data it stays ~10 — this is expected and not a failure. | |
| gate3_threshold = 2.0 | |
| if epoch == 0 and avg_decode >= gate3_threshold: | |
| log(f"\n*** GO/NO-GO GATE 3 WARNING: decode_loss={avg_decode:.4f} >= {gate3_threshold} ***") | |
| log("*** This is expected on dummy/small data. Continuing training. ***") | |
| log("*** On real VQA data, investigate if this persists after epoch 2. ***") | |
| elif epoch == 0: | |
| log(f" Go/No-Go Gate 3 PASSED: decode_loss={avg_decode:.4f} < {gate3_threshold}") | |
| # ---- Checkpoint every epoch ---- | |
| ckpt_path = f"checkpoints/stage2_epoch{epoch + 1}.pt" | |
| save_checkpoint(model, optimizer, scheduler, epoch + 1, global_step, avg_loss, ckpt_path) | |
| push_checkpoints() | |
| dist.barrier() | |
| # ---- Final checkpoint ---- | |
| save_checkpoint(model, optimizer, scheduler, max_epochs, global_step, avg_loss, | |
| "checkpoints/stage2_final.pt") | |
| push_checkpoints() | |
| log("\n" + "=" * 70) | |
| log(f"Stage 2 complete. Final loss: {avg_loss:.4f} decode: {avg_decode:.4f}") | |
| log("=" * 70) | |
| cleanup() | |
| if __name__ == "__main__": | |
| main() | |