Source code for rapidtools.models.llama

# Copyright (c) 2025 The University of Washington
#
# This file is part of rapidtools.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors
# may be used to endorse or promote products derived from this software without
# specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# You should have received a copy of the BSD 3-Clause License along with
# rapidtools. If not, see <http://www.opensource.org/licenses/>.
#
# Contributors:
# Barbaros Cetiner
#
# Last updated:
# 09-22-2026

"""
Meta Llama vision models (Llama 4 and Llama 3.2 Vision) via Transformers.

Llama 4 Scout/Maverick are natively multimodal mixture-of-experts models
(``Llama4ForConditionalGeneration``); Llama 3.2 Vision uses the cross-attention
``Mllama`` architecture. Both are loaded through the Transformers multimodal
auto class so the same wrapper serves every generation. Downloading the
weights requires accepting Meta's licence on Hugging Face.

Example:
    >>> from rapidtools.models import LlamaVisionInference
    >>> model = LlamaVisionInference(load_in_4bit=True)
    >>> out = model.run_inference('roof.jpg', 'Describe the roof damage.')
    >>> print(out.text)
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import Any

import torch
from transformers import AutoProcessor

from .base import GenerationConfig, ModelOutput
from .catalogs import (
    LLAMA_DEFAULT_MODEL,
    LLAMA_MODEL_CATALOG,
    PROVIDERS,
)
from .hf_vision import _load_multimodal_model
from .local_base import BaseLocalInferenceModel

logger = logging.getLogger(__name__)


[docs] class LlamaVisionInference(BaseLocalInferenceModel): """ Local inference with Meta's Llama 4 and Llama 3.2 Vision models. Example: >>> from rapidtools.models import LlamaVisionInference >>> LlamaVisionInference.list_available_models()[0] 'meta-llama/Llama-4-Scout-17B-16E-Instruct' >>> model = LlamaVisionInference( ... 'meta-llama/Llama-3.2-11B-Vision-Instruct', load_in_4bit=True ... ) >>> out = model.run_inference( ... ['front.jpg', 'side.jpg'], 'Return JSON with a damage_level key.', ... json_mode=True, ... ) >>> out.text '{"damage_level": "severe"}' """ INFO = PROVIDERS['llama'] MODEL_CATALOG: list[dict[str, str]] = LLAMA_MODEL_CATALOG def __init__( self, model_id: str = LLAMA_DEFAULT_MODEL, device: str = 'auto', load_in_4bit: bool = False, temperature: float = 0.4, max_tokens: int = 2048, ): """ Load the processor and weights into GPU/CPU memory. Args: model_id: Hugging Face repo ID (Llama 4 or Llama 3.2 Vision). device: ``'cuda'``, ``'cpu'``, or ``'auto'`` (spread across GPUs). load_in_4bit: Quantize with bitsandbytes so large models fit consumer GPUs (e.g. the 11B model on 24 GB). temperature: Sampling temperature (0 disables sampling). max_tokens: Maximum new tokens generated per call. """ super().__init__(device=device, temperature=temperature, max_tokens=max_tokens) self.model_id = model_id self.load_in_4bit = load_in_4bit logger.info( f'Loading processor and weights for {self.model_id}. ' 'This may take a moment...' ) # 1. Load the processor (tokenization + image resizing/normalization) self.processor = AutoProcessor.from_pretrained(self.model_id) # 2. Configure model loading and VRAM management model_kwargs: dict[str, Any] = {'device_map': self.device} if load_in_4bit: from transformers import BitsAndBytesConfig model_kwargs['quantization_config'] = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_compute_dtype=torch.float16 ) else: # Half precision halves memory relative to float32. model_kwargs['dtype'] = torch.float16 # 3. Load through the multimodal auto class (Mllama or Llama4). self.model = _load_multimodal_model(self.model_id, **model_kwargs) self.model.eval() logger.info(f'Llama model {self.model_id} loaded successfully.')
[docs] @classmethod def list_available_models(cls, auth_key: str | None = None) -> list[str]: """ Return the curated Llama vision checkpoints. Downloading them from the Hub requires accepting Meta's licence. Args: auth_key: Unused; present for signature parity with API wrappers. Returns: list[str]: Repository IDs, newest first. Example: >>> from rapidtools.models import LlamaVisionInference >>> len(LlamaVisionInference.list_available_models()) 6 """ return [entry['model_id'] for entry in cls.MODEL_CATALOG]
[docs] def run_inference( self, image_inputs: str | Path | list[str | Path], prompt: str | Path, json_mode: bool = False, temperature: float | None = None, max_tokens: int | None = None, config: GenerationConfig | None = None, **kwargs: Any, ) -> ModelOutput | None: """ Execute a forward pass and return the generated text. Args: image_inputs: One path/URL or a list of them. prompt: Prompt text or path to a prompt file. json_mode: Append a JSON-only instruction to the prompt (open models have no native JSON mode). temperature: Per-call temperature override. max_tokens: Per-call output-token limit override. config: A :class:`~rapidtools.models.GenerationConfig`; explicit keyword arguments take precedence over it. **kwargs: Ignored; accepted for interface compatibility. Returns: ModelOutput | None: Generated text plus raw token IDs, or ``None`` when no image loads or generation fails (e.g. out of memory). Example: >>> from rapidtools.models import LlamaVisionInference >>> model = LlamaVisionInference(load_in_4bit=True) >>> model.run_inference('house.jpg', 'Is the roof intact?').text 'No, part of the roof is missing.' """ prompt_str = self._resolve_prompt(prompt) log_ctx = f"[Prompt snippet: '{prompt_str[:30]}...']" if not isinstance(image_inputs, list): image_inputs = [image_inputs] loaded_images = [] for img_input in image_inputs: pil_img = self._load_image_as_pil(img_input) if pil_img: loaded_images.append(pil_img) if not loaded_images: logger.error(f'{log_ctx} No valid images could be loaded into PIL.') return None gen = self._resolve_generation( config, temperature=temperature, max_tokens=max_tokens, json_mode=json_mode ) if gen.json_mode: prompt_str += ( '\n\nYou must respond with only valid JSON and no other ' 'conversational text.' ) content_block: list[dict[str, Any]] = [{'type': 'image'} for _ in loaded_images] if prompt_str: content_block.append({'type': 'text', 'text': prompt_str}) messages = [{'role': 'user', 'content': content_block}] try: text_prompt = self.processor.apply_chat_template( messages, add_generation_prompt=True ) inputs = self.processor( text=text_prompt, images=loaded_images, return_tensors='pt' ) inputs = inputs.to(self.model.device) except Exception as e: # noqa: BLE001 - surfaced as a failed asset logger.error(f'{log_ctx} Failed to process Llama tensors: {e}') return None final_temp = gen.temperature final_tokens = gen.max_tokens try: with torch.no_grad(): output_ids = self.model.generate( **inputs, max_new_tokens=final_tokens, temperature=final_temp, do_sample=(final_temp is not None and final_temp > 0.0), ) # Keep only the newly generated tokens. generated_ids = output_ids[0][inputs['input_ids'].shape[1] :] extracted_text = self.processor.decode( generated_ids, skip_special_tokens=True ).strip() return ModelOutput( text=extracted_text, raw_response={'output_ids': output_ids.cpu().tolist()}, ) except torch.OutOfMemoryError: logger.error( f'{log_ctx} GPU OUT OF MEMORY ERROR. Try smaller images or ' 'setting load_in_4bit=True.' ) return None except Exception as e: # noqa: BLE001 - surfaced as a failed asset logger.error(f'{log_ctx} Unexpected tensor generation error: {e}') return None