-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Add VLM support to the dynamic-batching inference server #6260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a392b42
2402b1d
23fad30
5080610
e43f374
c373111
c7833b4
0299f03
5ee057b
25dccea
bdf13de
592fd9c
d513661
3a39431
4418ab6
bd99491
591f560
151c8ce
76a89ef
2aba70e
fdff099
3ed1164
78adb52
7909b4f
643a615
084c7be
67c7239
5e61e13
d96819c
ebc35b7
a972aa2
b20d25e
4eb95a4
4915df7
1af7efc
a9464c7
f1180d4
43a3650
9e56ee8
54e2c47
93f19e7
17bd43a
7d2cb12
11abf32
cffcdbc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,6 @@ | |
|
|
||
| import torch | ||
|
|
||
| from megatron.core.extensions.transformer_engine import HAVE_TE | ||
| from megatron.core.fusions.fused_bias_dropout import get_bias_dropout_add | ||
| from megatron.core.models.hybrid.hybrid_block import HybridStack, HybridStackSubmodules | ||
| from megatron.core.ssm.mamba_layer import MambaLayer, MambaLayerSubmodules | ||
|
|
@@ -15,9 +14,11 @@ | |
| from megatron.core.transformer.enums import AttnMaskType | ||
| from megatron.core.transformer.identity_op import IdentityOp | ||
| from megatron.core.transformer.mlp import MLP, MLPSubmodules | ||
| from megatron.core.models.gpt.moe_module_specs import get_moe_module_spec | ||
| from megatron.core.transformer.spec_utils import ModuleSpec | ||
| from megatron.core.transformer.transformer_layer import TransformerLayer, TransformerLayerSubmodules | ||
| from megatron.core.transformer.transformer_layer import MoETransformerLayer, TransformerLayer, TransformerLayerSubmodules | ||
| from megatron.core.typed_torch import not_none | ||
| from megatron.core.extensions.transformer_engine import HAVE_TE | ||
|
|
||
| if HAVE_TE: | ||
| from megatron.core.extensions.transformer_engine import ( | ||
|
|
@@ -127,12 +128,34 @@ def get_layer_spec_te(is_vit=False, padding=False) -> ModuleSpec: | |
| ) | ||
|
|
||
|
|
||
| def get_hybrid_layer_spec_te(padding=False) -> ModuleSpec: | ||
| def get_hybrid_layer_spec_te(config=None, padding=False) -> ModuleSpec: | ||
| """Hybrid (Mamba + attention + MLP [+ MoE]) layer spec. | ||
|
|
||
| Args: | ||
| config: language-model ``TransformerConfig``. Required for MoE hybrids | ||
| (e.g. nemotron6-moe): the moe_layer branch reads | ||
| ``num_moe_experts`` / ``moe_grouped_gemm`` off it to match the | ||
| checkpoint's architecture. Non-MoE hybrids may pass ``None``; | ||
| they never traverse the moe_layer branch. | ||
| padding: use padding-causal attention mask (needed for context | ||
| parallel + sequence parallel). | ||
| """ | ||
| attn_mask_type = AttnMaskType.causal | ||
| # Padding mask is needed for e.g. Context Parallel. | ||
| if padding: | ||
| attn_mask_type = AttnMaskType.padding_causal | ||
|
|
||
| if config is not None: | ||
| assert config.num_moe_experts is not None, ( | ||
| "get_hybrid_layer_spec_te: config.num_moe_experts must be set to " | ||
| "build the MoE branch of the hybrid stack." | ||
| ) | ||
| num_experts = config.num_moe_experts | ||
| moe_grouped_gemm = config.moe_grouped_gemm | ||
| else: | ||
| num_experts = None | ||
| moe_grouped_gemm = None | ||
|
|
||
| return ModuleSpec( | ||
| module=HybridStack, | ||
| submodules=HybridStackSubmodules( | ||
|
|
@@ -182,6 +205,19 @@ def get_hybrid_layer_spec_te(padding=False) -> ModuleSpec: | |
| mlp_bda=get_bias_dropout_add, | ||
| ), | ||
| ), | ||
| moe_layer=ModuleSpec( | ||
| module=MoETransformerLayer, | ||
| submodules=TransformerLayerSubmodules( | ||
| pre_mlp_layernorm=TENorm, | ||
| mlp=get_moe_module_spec( | ||
| use_te=True, | ||
| num_experts=num_experts, | ||
| moe_grouped_gemm=moe_grouped_gemm, | ||
| moe_use_legacy_grouped_gemm=False, | ||
| ), | ||
| mlp_bda=get_bias_dropout_add, | ||
|
Comment on lines
+208
to
+218
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [CRITICAL Correctness] Why it matters: the expert count is an architecture parameter, so a mismatch against the checkpoint is a load failure or, worse, a silent partial load — the router's output dimension and the expert weight shapes are both derived from it. A checkpoint trained with 16 or 64 experts cannot be loaded through this spec at all. It also silently disagrees with Fix: thread the config through, matching how def get_hybrid_layer_spec_te(config, padding=False) -> ModuleSpec:
...
moe_layer=ModuleSpec(
module=MoETransformerLayer,
submodules=TransformerLayerSubmodules(
pre_mlp_layernorm=TENorm,
mlp=get_moe_module_spec(
use_te=True,
num_experts=config.num_moe_experts,
moe_grouped_gemm=config.moe_grouped_gemm,
moe_use_legacy_grouped_gemm=False,
),
mlp_bda=get_bias_dropout_add,
),
),and update the call site in Also flagging for the linting gate: the import reordering in this file's header hunk leaves |
||
| ), | ||
| ), | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,16 +8,19 @@ | |||||||||||||||||||
| from layer_specs import (get_layer_spec, get_layer_spec_te, get_mlp_module_spec, get_norm_mlp_module_spec_te, | ||||||||||||||||||||
| get_hybrid_layer_spec_te) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| from megatron.core.models.gpt.gpt_layer_specs import get_gpt_decoder_block_spec | ||||||||||||||||||||
| from megatron.core.models.multimodal.llava_model import IMAGE_TOKEN, LLaVAModel | ||||||||||||||||||||
| from megatron.core.models.vision.clip_vit_model import get_num_image_embeddings | ||||||||||||||||||||
| from megatron.core.transformer.spec_utils import import_module | ||||||||||||||||||||
| from megatron.training import get_args, get_tokenizer, print_rank_0 | ||||||||||||||||||||
| from megatron.training.arguments import core_transformer_config_from_args | ||||||||||||||||||||
| from megatron.core.utils import log_single_rank | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def model_provider( | ||||||||||||||||||||
| pre_process=True, post_process=True, add_encoder=True, add_decoder=True, parallel_output=True, | ||||||||||||||||||||
| vp_stage=None, config=None, pg_collection=None | ||||||||||||||||||||
| vp_stage=None, config=None, pg_collection=None, | ||||||||||||||||||||
| ) -> LLaVAModel: | ||||||||||||||||||||
|
Comment on lines
21
to
24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [CRITICAL Correctness] The explicit Why it matters: each of the three has real consequences when dropped, and none of them raises:
The docstring entries were deleted in the same hunk, so there is no longer even a record that these were once accepted. And because Fix: restore the explicit parameters and thread them through to the
Suggested change
and re-add the three docstring lines. If |
||||||||||||||||||||
| """Builds the model. | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -28,10 +31,10 @@ def model_provider( | |||||||||||||||||||
| will live on only a subset of the pipeline stages (specifically, only the first stage). | ||||||||||||||||||||
| add_decoder (bool): Construct the decoder module (used with pipeline parallelism). Defaults to True. When we use pipelining, the decoder | ||||||||||||||||||||
| will live on only a subset of the pipeline stages (specifically, every stage after the first one). | ||||||||||||||||||||
| parallel_output (bool): Enable parallel model output. | ||||||||||||||||||||
| vp_stage: Optional virtual pipeline stage. Used with virtual pipeline parallelism. | ||||||||||||||||||||
| config: Optional transformer config. If None, will be created from args. | ||||||||||||||||||||
| pg_collection: Optional process group collection. If None, will use default. | ||||||||||||||||||||
| parallel_output (bool): Enable parallel model output. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Returns: | ||||||||||||||||||||
| model: A multimodal model. | ||||||||||||||||||||
|
|
@@ -41,28 +44,38 @@ def model_provider( | |||||||||||||||||||
|
|
||||||||||||||||||||
| print_rank_0('building a multimodal model ...') | ||||||||||||||||||||
|
|
||||||||||||||||||||
| num_image_embeddings = get_num_image_embeddings( | ||||||||||||||||||||
| args.img_h, | ||||||||||||||||||||
| args.img_w, | ||||||||||||||||||||
| args.patch_dim, | ||||||||||||||||||||
| args.vision_model_type, | ||||||||||||||||||||
| args.disable_vision_class_token, | ||||||||||||||||||||
| 1, | ||||||||||||||||||||
| args.pixel_shuffle, | ||||||||||||||||||||
| args.use_tile_tags, | ||||||||||||||||||||
| args.max_num_tiles, | ||||||||||||||||||||
| args.tokenizer_prompt_format | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| old_seq_length = args.seq_length | ||||||||||||||||||||
| args.seq_length = args.encoder_seq_length = num_image_embeddings | ||||||||||||||||||||
| if old_seq_length != args.seq_length: | ||||||||||||||||||||
| log_single_rank( | ||||||||||||||||||||
| logging.getLogger(__name__), | ||||||||||||||||||||
| logging.WARNING, | ||||||||||||||||||||
| f"Changed seq_length and encoder_seq_length (vision model sequence length) from {old_seq_length} to num_image_tokens ({num_image_embeddings})" | ||||||||||||||||||||
| if getattr(args, 'dynamic_resolution', False): | ||||||||||||||||||||
| max_num_image_embeddings = args.seq_length | ||||||||||||||||||||
| num_image_embeddings = args.seq_length | ||||||||||||||||||||
| if args.pixel_shuffle: | ||||||||||||||||||||
| max_num_image_embeddings //= 4 | ||||||||||||||||||||
| num_image_embeddings //= 4 | ||||||||||||||||||||
| if getattr(args, 'conv_merging', False): | ||||||||||||||||||||
| max_num_image_embeddings //= 4 | ||||||||||||||||||||
| num_image_embeddings //= 4 | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| num_image_embeddings = get_num_image_embeddings( | ||||||||||||||||||||
| args.img_h, | ||||||||||||||||||||
| args.img_w, | ||||||||||||||||||||
| args.patch_dim, | ||||||||||||||||||||
| args.vision_model_type, | ||||||||||||||||||||
| args.disable_vision_class_token, | ||||||||||||||||||||
| 1, | ||||||||||||||||||||
| args.pixel_shuffle, | ||||||||||||||||||||
| args.use_tile_tags, | ||||||||||||||||||||
| args.max_num_tiles, | ||||||||||||||||||||
| args.tokenizer_prompt_format | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| old_seq_length = args.seq_length | ||||||||||||||||||||
| args.seq_length = args.encoder_seq_length = num_image_embeddings | ||||||||||||||||||||
| if old_seq_length != args.seq_length: | ||||||||||||||||||||
| log_single_rank( | ||||||||||||||||||||
| logging.getLogger(__name__), | ||||||||||||||||||||
| logging.WARNING, | ||||||||||||||||||||
| f"Changed seq_length and encoder_seq_length (vision model sequence length) from {old_seq_length} to num_image_tokens ({num_image_embeddings})" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| max_num_image_embeddings = max((args.max_num_tiles + int(args.use_thumbnail)), args.num_frames) * num_image_embeddings | ||||||||||||||||||||
| max_num_image_embeddings = max((args.max_num_tiles + int(args.use_thumbnail)), args.num_frames) * num_image_embeddings | ||||||||||||||||||||
|
|
||||||||||||||||||||
| assert ( | ||||||||||||||||||||
| args.decoder_seq_length is not None | ||||||||||||||||||||
|
|
@@ -79,7 +92,7 @@ def model_provider( | |||||||||||||||||||
| language_model_type = args.language_model_type | ||||||||||||||||||||
| vision_model_type = args.vision_model_type | ||||||||||||||||||||
|
|
||||||||||||||||||||
| base_config = core_transformer_config_from_args(get_args()) | ||||||||||||||||||||
| base_config = config or core_transformer_config_from_args(get_args()) | ||||||||||||||||||||
| base_config.language_model_type = args.language_model_type | ||||||||||||||||||||
| base_config.vision_model_type = args.vision_model_type | ||||||||||||||||||||
| base_config.calculate_per_token_loss = True | ||||||||||||||||||||
|
|
@@ -98,8 +111,19 @@ def model_provider( | |||||||||||||||||||
| elif use_te: | ||||||||||||||||||||
| # Padding mask needed for SP/CP. | ||||||||||||||||||||
| padding = args.context_parallel_size > 1 and args.sequence_parallel | ||||||||||||||||||||
| if args.language_model_type.startswith('nemotron5-hybrid'): | ||||||||||||||||||||
| language_transformer_layer_spec = get_hybrid_layer_spec_te(padding=padding) | ||||||||||||||||||||
| if args.spec is not None: | ||||||||||||||||||||
| language_transformer_layer_spec = import_module(args.spec) | ||||||||||||||||||||
| elif args.language_model_type.startswith(('nemotron5-hybrid', 'nemotron6-moe')): | ||||||||||||||||||||
| language_transformer_layer_spec = get_hybrid_layer_spec_te( | ||||||||||||||||||||
| config=language_config, padding=padding | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| elif getattr(args, 'num_experts', None): | ||||||||||||||||||||
| language_transformer_layer_spec = get_gpt_decoder_block_spec( | ||||||||||||||||||||
| language_config, | ||||||||||||||||||||
| use_transformer_engine=use_te, | ||||||||||||||||||||
| normalization=args.normalization, | ||||||||||||||||||||
| qk_l2_norm=getattr(args, 'qk_l2_norm', False), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| language_transformer_layer_spec = get_layer_spec_te( | ||||||||||||||||||||
| is_vit=False, padding=padding | ||||||||||||||||||||
|
|
@@ -110,11 +134,12 @@ def model_provider( | |||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| vision_config = deepcopy(base_config) | ||||||||||||||||||||
| vision_config = get_vision_model_config( | ||||||||||||||||||||
| vision_config, apply_query_key_layer_scaling=args.apply_query_key_layer_scaling | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| vision_config = get_vision_model_config(vision_config) | ||||||||||||||||||||
| # Most ViT checkpoints use bias in linear layers; override --disable-bias-linear. | ||||||||||||||||||||
| # Pixtral (both sizes) uses no bias — config.py already sets add_bias_linear=False. | ||||||||||||||||||||
| if vision_model_type not in ("pixtral-vit", "pixtral-vit-large"): | ||||||||||||||||||||
| vision_config.add_bias_linear = True | ||||||||||||||||||||
| if vision_model_type.startswith("hf://"): | ||||||||||||||||||||
| assert not args.sequence_parallel, "Huggingface models do not support --sequence-parallel" | ||||||||||||||||||||
| assert args.context_parallel_size < 2, "Huggingface models do not support --context-parallel-size > 1" | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if vision_model_type in ["clip", "siglip", "radio", "cradio-g"]: | ||||||||||||||||||||
|
|
@@ -141,6 +166,13 @@ def model_provider( | |||||||||||||||||||
| elif vision_model_type == "internvit300M": | ||||||||||||||||||||
| from nvlm.internvit import get_internvit300M_layer_spec | ||||||||||||||||||||
| vision_transformer_layer_spec = get_internvit300M_layer_spec(use_te=use_te) | ||||||||||||||||||||
| elif vision_model_type in ("pixtral-vit", "pixtral-vit-large", "qwen-vl", "kimi-vit"): | ||||||||||||||||||||
| if use_te: | ||||||||||||||||||||
| vision_transformer_layer_spec = get_layer_spec_te(is_vit=True) | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| vision_transformer_layer_spec = get_layer_spec( | ||||||||||||||||||||
| is_vit=True, normalization=vision_config.normalization | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| elif vision_model_type.startswith("hf://"): | ||||||||||||||||||||
| vision_transformer_layer_spec = None | ||||||||||||||||||||
| else: | ||||||||||||||||||||
|
|
@@ -159,10 +191,13 @@ def model_provider( | |||||||||||||||||||
| # Make sure the vision model does not inherit first and last pipeline num layers from the language model. | ||||||||||||||||||||
| vision_config.first_pipeline_num_layers = vision_config.last_pipeline_num_layers = None | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # ``get_*_module_spec_te`` returns ``functools.partial(MLP.as_mlp_submodule, | ||||||||||||||||||||
| # submodules=...)`` (see PR #3435). Pull the submodules out of the partial's | ||||||||||||||||||||
| # bound kwargs so the vision projection sees an ``MLPSubmodules`` value. | ||||||||||||||||||||
| if vision_projection_config.normalization: | ||||||||||||||||||||
| vision_projection_layer_spec = get_norm_mlp_module_spec_te().submodules | ||||||||||||||||||||
| vision_projection_layer_spec = get_norm_mlp_module_spec_te().keywords["submodules"] | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| vision_projection_layer_spec = get_mlp_module_spec(use_te=use_te).submodules | ||||||||||||||||||||
| vision_projection_layer_spec = get_mlp_module_spec(use_te=use_te).keywords["submodules"] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Toggle --recompute* for the vision and language model separately. | ||||||||||||||||||||
| if args.recompute_vision: | ||||||||||||||||||||
|
|
@@ -202,7 +237,7 @@ def model_provider( | |||||||||||||||||||
| drop_vision_class_token=args.disable_vision_class_token, | ||||||||||||||||||||
| vision_projection_config=vision_projection_config, | ||||||||||||||||||||
| vision_projection_layer_spec=vision_projection_layer_spec, | ||||||||||||||||||||
| vision_projection_type="mlp", | ||||||||||||||||||||
| vision_projection_type=args.vision_projection_type, | ||||||||||||||||||||
| allow_missing_vision_projection_checkpoint=args.allow_missing_vision_projection_checkpoint, | ||||||||||||||||||||
| parallel_output=parallel_output, | ||||||||||||||||||||
| share_embeddings_and_output_weights=not args.untie_embeddings_and_output_weights, | ||||||||||||||||||||
|
|
@@ -221,9 +256,20 @@ def model_provider( | |||||||||||||||||||
| fp16_lm_cross_entropy=args.fp16_lm_cross_entropy, | ||||||||||||||||||||
| image_token_index=image_token_index, | ||||||||||||||||||||
| pixel_shuffle=args.pixel_shuffle, | ||||||||||||||||||||
| conv_merging=getattr(args, "conv_merging", False), | ||||||||||||||||||||
| tile_tags=tile_tags, | ||||||||||||||||||||
| max_num_tiles=args.max_num_tiles, | ||||||||||||||||||||
| tokenizer_type=args.tokenizer_prompt_format, | ||||||||||||||||||||
| use_vision_backbone_fp8_arch=getattr(args, "use_vision_backbone_fp8_arch", False), | ||||||||||||||||||||
| dynamic_resolution=getattr(args, "dynamic_resolution", False), | ||||||||||||||||||||
| class_token_len=getattr(args, "class_token_len", None), | ||||||||||||||||||||
| radio_force_eval_mode=getattr(args, "radio_force_eval_mode", False), | ||||||||||||||||||||
| radio_force_cpe_eval_mode=getattr(args, "radio_force_cpe_eval_mode", False), | ||||||||||||||||||||
| radio_interpolate_only_cpe=getattr(args, "radio_interpolate_only_cpe", False), | ||||||||||||||||||||
| radio_cpe_aspect_ratio_select=getattr(args, "radio_cpe_aspect_ratio_select", False), | ||||||||||||||||||||
| radio_disable_cpe=getattr(args, "radio_disable_cpe", False), | ||||||||||||||||||||
| vp_stage=vp_stage, | ||||||||||||||||||||
| pg_collection=pg_collection, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| model.freeze( | ||||||||||||||||||||
|
|
||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[IMPORTANT Compatibility]
configis inserted as the first positional parameter, and the promised assert is missing — soconfig=Nonenow fails at spec-build time for every hybrid, not just MoE ones.Two issues:
Positional shift.
get_hybrid_layer_spec_te(config=None, padding=False)movespaddingfrom position 0 to position 1. The sole in-repo caller (examples/multimodal/model.py:117) uses keywords, but any external caller doingget_hybrid_layer_spec_te(True)now silently passesTrueas the config.Missing guard. The comment at 139-141 says the intent is to "assert only when it would actually be consulted", but no assert exists. And the deferral doesn't hold: the
moe_layerModuleSpec at 198-209 is constructed eagerly, sonum_experts=None(fromgetattr(config, 'num_experts', None)on aNoneconfig) flows intoget_moe_module_spec→get_moe_module_spec_for_backend, which doesassert num_experts is not None(megatron/core/models/gpt/moe_module_specs.py:72). So aNoneconfig raises for dense hybrids too, with a confusing MoE-flavored assertion message.Fix: give
configa keyword-only position to preserve the old positional order, and add the guard the comment describes:Alternatively, build the
moe_layerspec lazily so a dense hybrid never consultsnum_experts— which would make the existing comment accurate.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed by f294dbe