mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-28 23:07:09 +08:00
159 lines
7.9 KiB
Python
159 lines
7.9 KiB
Python
"""
|
|
Test batch invariance of inline VE (ViT + downsample + aligner)
|
|
|
|
A batch-invariance test that runs only image encoding, not the LLM:
|
|
for each image size, generate one fixed random image and use a batch containing just that image as the baseline;
|
|
then run two kinds of same-batch tests in which every image's embeds must be bit-identical
|
|
(torch.equal) to that image's baseline:
|
|
1. single-shape repeat: one batch runs n identical images;
|
|
2. shape mix: one batch runs a random mix of images of several shapes.
|
|
|
|
The 8-GPU DP only parallelizes different image sizes: image encoding is pure single-GPU compute and only VE-side weights are loaded,
|
|
so the single-shape test splits image sizes disjointly across ranks by [dp_rank::dp_size],
|
|
and the mixed test gives each rank a random combination with a different seed.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
from functools import partial
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
|
|
from examples._config import DEFAULT_MODEL_NAME, resolve_model_path, setup_cuda_visible_devices
|
|
from tests.models._runner import run_model_test
|
|
|
|
# NOTE: deepinfer-related modules (including examples._prompts, which imports deepinfer) must be loaded
|
|
# only after the subprocess's _setup_subprocess_env; names used at runtime are imported lazily inside functions.
|
|
|
|
# ========================= Config =========================
|
|
# Raw image sizes (width, height). Before encoding, compute_image_layout maps them to the best shape,
|
|
# and fixed images are generated at the best shape: the CPU resize is then an identity transform and what is compared is batch invariance of the pure GPU path.
|
|
# (1, 1) is the mandatory minimum raw size (enlarged by the min_pixels constraint to the smallest best shape).
|
|
IMAGE_SIZES: list[tuple[int, int]] = [
|
|
(1, 1),
|
|
(32, 32),
|
|
(256, 256),
|
|
(640, 480),
|
|
(480, 640),
|
|
(512, 1024),
|
|
(1536, 512),
|
|
(1024, 1024),
|
|
(2048, 2048),
|
|
]
|
|
# images per batch
|
|
N_IMAGES_LIST: list[int] = [1, 2, 64, 256]
|
|
# images per batch for the mixed-shape test (n=1 has no mixing; a fixed "exactly one per shape" set is also tested)
|
|
MIXED_N_IMAGES_LIST: list[int] = [8, 64, 256]
|
|
IMAGE_RNG_SEED = 20260722
|
|
|
|
|
|
def _encode_batch(tuner: object, image_processor: object, raw_images: list[tuple[bytes, int, int]]) -> list[torch.Tensor]:
|
|
"""Encode one batch of images and return the embeds per image."""
|
|
from examples._ve_helper import build_encode_inputs, encode_images
|
|
|
|
inputs = build_encode_inputs(tuner, image_processor, raw_images)
|
|
return list(encode_images(tuner, inputs).split(inputs.num_downsampled))
|
|
|
|
|
|
# ========================= Subprocess entry =========================
|
|
def _setup_subprocess_env(model_name: str, dp_rank: int) -> None:
|
|
"""Subprocess env setup. Must be called before importing any deepinfer module."""
|
|
setup_cuda_visible_devices(dp_rank, 1, torch.cuda.device_count())
|
|
|
|
|
|
def _run_ve_batch_invariant(model_name: str, dp_size: int, dp_rank: int) -> None:
|
|
_setup_subprocess_env(model_name, dp_rank)
|
|
|
|
# _prompts imports deepinfer internally, so it must be loaded after _setup_subprocess_env
|
|
from deepinfer.ve.processor.image_processor import compute_image_layout
|
|
from examples._prompts import random_bmp_bytes
|
|
from examples._ve_helper import load_ve_only
|
|
|
|
tuner, image_processor = load_ve_only(resolve_model_path(model_name))
|
|
|
|
# ========================= Build fixed images (one per size) =========================
|
|
# Dedupe the full IMAGE_SIZES by best shape first (different raw sizes may map to the same best shape,
|
|
# e.g. all enlarged by min_pixels; a fixed image is determined only by best shape + seed) to get a deduped list
|
|
# identical on every rank, then split it across ranks by [dp_rank::dp_size] for parallel testing.
|
|
best_shapes: dict[tuple[int, int], tuple[bytes, int, int]] = {} # best shape -> raw_image
|
|
for orig_w, orig_h in IMAGE_SIZES:
|
|
layout = compute_image_layout(
|
|
orig_w,
|
|
orig_h,
|
|
patch_size=image_processor.patch_size,
|
|
downsample_ratio=image_processor.downsample_ratio,
|
|
max_n_token=image_processor.max_n_token,
|
|
min_pixels=image_processor.min_pixels,
|
|
max_wh_ratio=image_processor.max_wh_ratio,
|
|
)
|
|
best_w, best_h = layout.best_width, layout.best_height
|
|
if (best_w, best_h) in best_shapes:
|
|
continue
|
|
seed = IMAGE_RNG_SEED + len(best_shapes) # index within the deduped list, identical on every rank
|
|
bmp = random_bmp_bytes(np.random.default_rng(seed), best_w, best_h)
|
|
best_shapes[(best_w, best_h)] = (bmp, best_w, best_h)
|
|
if dp_rank == 0:
|
|
print(f"size {(orig_w, orig_h)} -> best shape {(best_w, best_h)}, n_llm = {(layout.n_llm_w, layout.n_llm_h)}", flush=True)
|
|
|
|
shape_list = list(best_shapes.keys())
|
|
my_shapes = shape_list[dp_rank::dp_size]
|
|
print(f"[Rank {dp_rank}] testing best shapes: {my_shapes}", flush=True)
|
|
|
|
# ========================= Baselines (one image per batch per shape) =========================
|
|
# Every rank computes baselines for all shapes (single-image batches, negligible cost): the single-shape repeat test only uses
|
|
# this rank's shapes, but the mixed test's random combinations may hit any shape.
|
|
baselines = {shape: _encode_batch(tuner, image_processor, [best_shapes[shape]])[0] for shape in shape_list}
|
|
|
|
# ========================= Run & compare =========================
|
|
failures: list[str] = []
|
|
|
|
def check_batch(shapes: list[tuple[int, int]], tag: str) -> None:
|
|
"""Run one batch of the fixed images for shapes and compare each image bitwise against its shape's baseline."""
|
|
n = len(shapes)
|
|
results = _encode_batch(tuner, image_processor, [best_shapes[s] for s in shapes])
|
|
assert len(results) == n
|
|
for img_idx, embeds in enumerate(results):
|
|
ref_embeds = baselines[shapes[img_idx]]
|
|
what = f"[Rank {dp_rank}] {tag}, image {img_idx} (best shape {shapes[img_idx]})"
|
|
if embeds.shape != ref_embeds.shape:
|
|
failures.append(f"{what}: embeds shape mismatch {tuple(embeds.shape)} != {tuple(ref_embeds.shape)}")
|
|
continue
|
|
if not torch.equal(embeds, ref_embeds):
|
|
ne = embeds != ref_embeds
|
|
maxdiff = (embeds.float() - ref_embeds.float()).abs().max().item()
|
|
failures.append(f"{what}: embeds not bitwise equal, {int(ne.sum())}/{ne.numel()} elems differ, maxdiff={maxdiff:.9e}")
|
|
torch.cuda.empty_cache() # free this batch's activation memory so large n do not accumulate
|
|
print(f"[Rank {dp_rank}] {tag}: checked {n} images", flush=True)
|
|
|
|
# ---- single-shape repeat: one batch runs n identical images ----
|
|
for shape in my_shapes:
|
|
for n in N_IMAGES_LIST:
|
|
check_batch([shape] * n, f"single-shape {shape}, n={n}")
|
|
|
|
# ---- shape mix: one batch runs a random mix of shapes ----
|
|
# Each rank uses a different seed so the union covers more mixing patterns; fixed seeds keep it reproducible.
|
|
# Test a fixed "exactly one per shape" set (random order) first, then the random mixes for each n.
|
|
rng = random.Random(IMAGE_RNG_SEED + 1000 + dp_rank)
|
|
all_shapes_once = shape_list.copy()
|
|
rng.shuffle(all_shapes_once)
|
|
check_batch(all_shapes_once, "mixed all-shapes-once")
|
|
for n in MIXED_N_IMAGES_LIST:
|
|
check_batch([rng.choice(shape_list) for _ in range(n)], f"mixed n={n}")
|
|
|
|
assert not failures, "VE batch invariance failed:\n" + "\n".join(failures)
|
|
print(f"[Rank {dp_rank}] All VE batch invariance checks passed!", flush=True)
|
|
|
|
|
|
# ========================= pytest entry =========================
|
|
|
|
DP_SIZE = 8 # NOTE only for parallel shape testing; the full case also runs on a single GPU
|
|
MODEL_LIST = [DEFAULT_MODEL_NAME]
|
|
|
|
|
|
@pytest.mark.parametrize("model_name", MODEL_LIST)
|
|
def test_ve_batch_invariant(model_name: str) -> None:
|
|
run_model_test(partial(_run_ve_batch_invariant, model_name), DP_SIZE)
|