Files
2025-07-10 08:19:41 +02:00

195 lines
6.2 KiB
Python

import os
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
import folder_paths
import comfy.model_management as mm
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
log = logging.getLogger(__name__)
# Add SmolLM3 to model folders
if not "smollm3" in folder_paths.folder_names_and_paths:
folder_paths.add_model_folder_path("smollm3", os.path.join(folder_paths.models_dir, "smollm3"))
class SmolLM3ModelLoader:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model_name": (["HuggingFaceTB/SmolLM3-3B-Base", "HuggingFaceTB/SmolLM3-3B"],
{"default": "HuggingFaceTB/SmolLM3-3B"}),
"precision": (["fp16", "fp32", "bf16"], {"default": "fp16"}),
"device": (["cuda", "cpu"], {"default": "cuda"}),
},
}
RETURN_TYPES = ("SMOLLM3_MODEL", "SMOLLM3_TOKENIZER")
RETURN_NAMES = ("model", "tokenizer")
FUNCTION = "load_model"
CATEGORY = "SmolLM3"
def load_model(self, model_name, precision, device):
# Get device
if device == "cuda":
device = mm.get_torch_device()
# Set dtype
dtype_map = {
"fp16": torch.float16,
"fp32": torch.float32,
"bf16": torch.bfloat16
}
dtype = dtype_map[precision]
log.info(f"Loading SmolLM3 model: {model_name}")
# Load tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_name)
# Load model
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=dtype,
device_map=device if device == "cuda" else None
)
if device == "cuda":
model = model.to(device)
model.eval()
log.info(f"Model loaded successfully on {device} with {precision} precision")
return (model, tokenizer)
class SmolLM3Sampler:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("SMOLLM3_MODEL",),
"tokenizer": ("SMOLLM3_TOKENIZER",),
"prompt": ("STRING",
{"default": "Give me a brief explanation of gravity in simple terms.", "multiline": True}),
"max_new_tokens": ("INT", {"default": 512, "min": 1, "max": 32768, "step": 1}),
"temperature": ("FLOAT", {"default": 0.6, "min": 0.0, "max": 2.0, "step": 0.01}),
"top_p": ("FLOAT", {"default": 0.95, "min": 0.0, "max": 1.0, "step": 0.01}),
"seed": ("INT", {"default": -1, "min": -1, "max": 0xffffffffffffffff}),
"use_chat_template": ("BOOLEAN", {"default": True}),
},
"optional": {
"system_prompt": ("STRING", {"default": "", "multiline": True}),
}
}
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("generated_text", "full_output")
FUNCTION = "generate"
CATEGORY = "SmolLM3"
def generate(self, model, tokenizer, prompt, max_new_tokens, temperature, top_p, seed, use_chat_template,
system_prompt=""):
# Set seed if provided
if seed != -1:
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed(seed)
# Get device from model
device = next(model.parameters()).device
# Prepare input
if use_chat_template:
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": prompt})
text = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
else:
text = prompt
# Tokenize
model_inputs = tokenizer([text], return_tensors="pt").to(device)
# Generate
log.info(f"Generating with temperature={temperature}, top_p={top_p}, max_new_tokens={max_new_tokens}")
with torch.no_grad():
generated_ids = model.generate(
**model_inputs,
max_new_tokens=max_new_tokens,
temperature=temperature,
top_p=top_p,
do_sample=temperature > 0.0,
pad_token_id=tokenizer.eos_token_id
)
# Decode output
output_ids = generated_ids[0][len(model_inputs.input_ids[0]):]
generated_text = tokenizer.decode(output_ids, skip_special_tokens=True)
# Full output including input
full_output = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
log.info(f"Generated {len(output_ids)} tokens")
return (generated_text, full_output)
class SmolLM3SimpleGenerate:
"""Simple generation node without chat template"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("SMOLLM3_MODEL",),
"tokenizer": ("SMOLLM3_TOKENIZER",),
"prompt": ("STRING", {"default": "Gravity is", "multiline": True}),
"max_new_tokens": ("INT", {"default": 128, "min": 1, "max": 2048, "step": 1}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("generated_text",)
FUNCTION = "generate"
CATEGORY = "SmolLM3"
def generate(self, model, tokenizer, prompt, max_new_tokens):
# Get device from model
device = next(model.parameters()).device
# Tokenize
inputs = tokenizer.encode(prompt, return_tensors="pt").to(device)
# Generate
with torch.no_grad():
outputs = model.generate(inputs, max_new_tokens=max_new_tokens)
# Decode
generated_text = tokenizer.decode(outputs[0], skip_special_tokens=True)
return (generated_text,)
NODE_CLASS_MAPPINGS = {
"SmolLM3ModelLoader": SmolLM3ModelLoader,
"SmolLM3Sampler": SmolLM3Sampler,
"SmolLM3SimpleGenerate": SmolLM3SimpleGenerate,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SmolLM3ModelLoader": "SmolLM3 Model Loader",
"SmolLM3Sampler": "SmolLM3 Sampler (Chat)",
"SmolLM3SimpleGenerate": "SmolLM3 Simple Generate",
}