| import json |
| import os |
| import re |
| import time |
| from typing import Any |
|
|
| import modal |
|
|
|
|
| APP_NAME = "virtual-characters-gemma-benchmark" |
| MODEL_ID = os.environ.get("VC_BENCH_MODEL", "google/gemma-4-12B-it") |
| GPU = os.environ.get("VC_BENCH_GPU", "L40S") |
| MODEL_DIR = "/root/.cache/huggingface" |
| HF_SECRET_NAME = os.environ.get("VC_HF_SECRET_NAME", "hf-token") |
|
|
|
|
| image = ( |
| modal.Image.from_registry("nvidia/cuda:12.9.0-devel-ubuntu22.04", add_python="3.12") |
| .entrypoint([]) |
| .uv_pip_install( |
| "accelerate>=1.8.0", |
| "huggingface-hub>=0.36.0", |
| "librosa>=0.10.2", |
| "pillow>=11.0.0", |
| "safetensors>=0.5.0", |
| "torch>=2.7.0", |
| "torchvision>=0.22.0", |
| "transformers>=4.57.0", |
| ) |
| .env({"HF_HUB_CACHE": MODEL_DIR, "HF_XET_HIGH_PERFORMANCE": "1"}) |
| ) |
|
|
| hf_cache = modal.Volume.from_name("vc-hf-cache", create_if_missing=True) |
| app = modal.App(APP_NAME, image=image) |
|
|
|
|
| def _strip_empty_thought_prefix(text: str) -> str: |
| return re.sub(r"^\s*(?:<\|channel\>)?thought\s*(?:<channel\|>)?", "", text, count=1).strip() |
|
|
|
|
| @app.function( |
| gpu=GPU, |
| timeout=60 * 45, |
| scaledown_window=60, |
| secrets=[modal.Secret.from_name(HF_SECRET_NAME)], |
| volumes={MODEL_DIR: hf_cache}, |
| ) |
| def run_benchmark(prompt: str, max_new_tokens: int = 64, enable_thinking: bool = False) -> dict[str, Any]: |
| import torch |
| from transformers import AutoModelForMultimodalLM, AutoProcessor |
|
|
| result: dict[str, Any] = { |
| "model_id": MODEL_ID, |
| "gpu": GPU, |
| "max_new_tokens": max_new_tokens, |
| "enable_thinking": enable_thinking, |
| } |
| remote_started = time.perf_counter() |
|
|
| t0 = time.perf_counter() |
| processor = AutoProcessor.from_pretrained(MODEL_ID) |
| result["processor_load_s"] = round(time.perf_counter() - t0, 3) |
|
|
| t0 = time.perf_counter() |
| model = AutoModelForMultimodalLM.from_pretrained(MODEL_ID, dtype="auto", device_map="auto") |
| model.eval() |
| if torch.cuda.is_available(): |
| torch.cuda.synchronize() |
| torch.cuda.reset_peak_memory_stats() |
| result["model_load_s"] = round(time.perf_counter() - t0, 3) |
|
|
| messages = [ |
| {"role": "system", "content": "你是一个中文虚拟角色对话模型。回答要自然、简短、有角色感。"}, |
| {"role": "user", "content": prompt}, |
| ] |
|
|
| t0 = time.perf_counter() |
| try: |
| inputs = processor.apply_chat_template( |
| messages, |
| tokenize=True, |
| return_dict=True, |
| return_tensors="pt", |
| add_generation_prompt=True, |
| enable_thinking=enable_thinking, |
| ).to(model.device) |
| except TypeError: |
| inputs = processor.apply_chat_template( |
| messages, |
| tokenize=True, |
| return_dict=True, |
| return_tensors="pt", |
| add_generation_prompt=True, |
| ).to(model.device) |
| input_len = int(inputs["input_ids"].shape[-1]) |
| result["prompt_tokens"] = input_len |
| result["prepare_input_s"] = round(time.perf_counter() - t0, 3) |
|
|
| generate_kwargs = { |
| **inputs, |
| "max_new_tokens": max_new_tokens, |
| "do_sample": False, |
| "pad_token_id": getattr(processor.tokenizer, "eos_token_id", None), |
| } |
|
|
| if torch.cuda.is_available(): |
| torch.cuda.synchronize() |
| t0 = time.perf_counter() |
| outputs = model.generate(**generate_kwargs) |
| if torch.cuda.is_available(): |
| torch.cuda.synchronize() |
| generation_s = time.perf_counter() - t0 |
|
|
| new_token_ids = outputs[0][input_len:] |
| output_tokens = int(new_token_ids.shape[-1]) |
| decoded = processor.decode(new_token_ids, skip_special_tokens=True) |
| preview = _strip_empty_thought_prefix(decoded) |
| result.update( |
| { |
| "output_tokens": output_tokens, |
| "generation_s": round(generation_s, 3), |
| "tokens_per_s": round(output_tokens / generation_s, 3) if generation_s > 0 else None, |
| "remote_function_s": round(time.perf_counter() - remote_started, 3), |
| "response_preview": preview[:300], |
| } |
| ) |
| if torch.cuda.is_available(): |
| result["cuda_name"] = torch.cuda.get_device_name(0) |
| result["cuda_peak_memory_gb"] = round(torch.cuda.max_memory_allocated() / 1024**3, 3) |
| return result |
|
|
|
|
| @app.local_entrypoint() |
| def main( |
| prompt: str = "我今天有点累,请你用虚拟角色的语气安慰我,并给我一个很短的建议。", |
| max_new_tokens: int = 64, |
| enable_thinking: bool = False, |
| ): |
| started = time.perf_counter() |
| result = run_benchmark.remote(prompt=prompt, max_new_tokens=max_new_tokens, enable_thinking=enable_thinking) |
| result["client_wall_s"] = round(time.perf_counter() - started, 3) |
| print(json.dumps(result, ensure_ascii=False, indent=2)) |
|
|