Skip to content

Commit f027234

Browse files
authored
Merge branch 'main' into rob-luke/training/mlflow-description
2 parents 5361476 + f8c6113 commit f027234

13 files changed

Lines changed: 1135 additions & 38 deletions

File tree

‎examples/conversion/hf_to_megatron_generate_vlm.py‎

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
pad_input_ids_to_tp_multiple,
3939
patch_kimi_vision_processor,
4040
process_image_inputs,
41+
process_multi_image_inputs,
42+
process_video_inputs,
4143
to_cuda,
4244
)
4345

@@ -64,6 +66,8 @@ def __init__(
6466
image_grid_thw=None,
6567
image_sizes=None,
6668
mm_token_type_ids=None,
69+
pixel_values_videos=None,
70+
video_grid_thw=None,
6771
image_position_ids=None,
6872
):
6973
self.batch = dict(
@@ -79,6 +83,10 @@ def __init__(
7983
self.batch["image_sizes"] = image_sizes
8084
if mm_token_type_ids is not None:
8185
self.batch["mm_token_type_ids"] = mm_token_type_ids
86+
if pixel_values_videos is not None:
87+
self.batch["pixel_values_videos"] = pixel_values_videos
88+
if video_grid_thw is not None:
89+
self.batch["video_grid_thw"] = video_grid_thw
8290
if image_position_ids is not None:
8391
self.batch["image_position_ids"] = image_position_ids
8492
self._yielded = False
@@ -101,7 +109,15 @@ def vlm_forward_step(data_iterator, model, **kwargs) -> torch.Tensor:
101109
"position_ids": batch["position_ids"],
102110
"attention_mask": batch.get("attention_mask"),
103111
}
104-
for key in ("pixel_values", "image_grid_thw", "image_sizes", "mm_token_type_ids", "image_position_ids"):
112+
for key in (
113+
"pixel_values",
114+
"image_grid_thw",
115+
"image_sizes",
116+
"mm_token_type_ids",
117+
"pixel_values_videos",
118+
"video_grid_thw",
119+
"image_position_ids",
120+
):
105121
if key in batch:
106122
forward_args[key] = batch[key]
107123

@@ -215,27 +231,41 @@ def _disable_mtp(m):
215231
# ------------------------------------------------------------------
216232
# Process inputs
217233
# ------------------------------------------------------------------
234+
pixel_values = image_grid_thw = image_sizes = mm_token_type_ids = image_position_ids = None
235+
pixel_values_videos = video_grid_thw = None
236+
237+
if args.video_path:
238+
input_ids_raw, pixel_values_videos, video_grid_thw = process_video_inputs(
239+
processor, args.video_path, args.prompt, fps=args.video_fps
240+
)
241+
elif args.image_paths:
242+
input_ids_raw, pixel_values, image_grid_thw = process_multi_image_inputs(
243+
processor, args.image_paths, args.prompt
244+
)
245+
else:
246+
(
247+
input_ids_raw,
248+
pixel_values,
249+
image_grid_thw,
250+
image_sizes,
251+
mm_token_type_ids,
252+
image_position_ids,
253+
) = process_image_inputs(
254+
processor,
255+
args.image_path,
256+
args.prompt,
257+
is_gemma4=is_gemma4,
258+
is_kimi=is_kimi,
259+
image_token_id=image_token_id,
260+
)
218261

219-
(
220-
input_ids_raw,
221-
pixel_values,
222-
image_grid_thw,
223-
image_sizes,
224-
mm_token_type_ids,
225-
image_position_ids,
226-
) = process_image_inputs(
227-
processor,
228-
args.image_path,
229-
args.prompt,
230-
is_gemma4=is_gemma4,
231-
is_kimi=is_kimi,
232-
image_token_id=image_token_id,
233-
)
234262
input_ids_raw = input_ids_raw.cuda()
235263
pixel_values = to_cuda(pixel_values)
236264
image_grid_thw = to_cuda(image_grid_thw)
237265
image_sizes = to_cuda(image_sizes)
238266
mm_token_type_ids = to_cuda(mm_token_type_ids)
267+
pixel_values_videos = to_cuda(pixel_values_videos)
268+
video_grid_thw = to_cuda(video_grid_thw)
239269
image_position_ids = to_cuda(image_position_ids)
240270

241271
# ------------------------------------------------------------------
@@ -270,7 +300,9 @@ def _disable_mtp(m):
270300
image_grid_thw,
271301
image_sizes,
272302
mm_ids_padded,
273-
image_position_ids,
303+
pixel_values_videos=pixel_values_videos,
304+
video_grid_thw=video_grid_thw,
305+
image_position_ids=image_position_ids,
274306
)
275307

276308
output = fwd_bwd_function(
@@ -343,7 +375,18 @@ def _disable_mtp(m):
343375
"--pp_layout", type=str, default=None, help="Pipeline model parallel layout (e.g. 'Et*15|t*15|t*16|t*15L')"
344376
)
345377
parser.add_argument("--megatron_model_path", type=str, default=None, help="Path to Megatron model checkpoint")
346-
parser.add_argument("--image_path", type=str, default=None, help="Path or URL to image (optional).")
378+
parser.add_argument("--image_path", type=str, default=None, help="Path or URL to a single image (optional).")
379+
parser.add_argument(
380+
"--image_paths",
381+
type=str,
382+
nargs="+",
383+
default=None,
384+
help="Paths to N image files in order (multi-image; Qwen-family only).",
385+
)
386+
parser.add_argument("--video_path", type=str, default=None, help="Path to a video file (Qwen-family only).")
387+
parser.add_argument(
388+
"--video_fps", type=float, default=2.0, help="Frames per second to sample from the video (default: 2.0)."
389+
)
347390
parser.add_argument("--trust_remote_code", action="store_true", help="Trust remote code for HF model loading")
348391
args = parser.parse_args()
349392

‎examples/conversion/vlm_generate_utils.py‎

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ def _patched_from_dict(cls, config_dict, **kwargs):
6565
pass
6666

6767

68-
def pre_expand_image_tokens(input_ids, grid_thws, image_token_id, spatial_merge_size=2):
69-
"""Pre-expand single image placeholders to N placeholders matching vision
70-
feature count.
68+
def pre_expand_vision_tokens(input_ids, grid_thws, vision_token_id, spatial_merge_size=2):
69+
"""Pre-expand single vision placeholders (image or video) to N placeholders
70+
matching vision feature count.
7171
7272
With PP > 1 the pipeline schedule needs to know the actual sequence length
7373
upfront. Dynamic expansion inside the model changes seq_length during
@@ -86,8 +86,8 @@ def pre_expand_image_tokens(input_ids, grid_thws, image_token_id, spatial_merge_
8686
expanded = []
8787
feat_idx = 0
8888
for token_id in input_ids[0]:
89-
if token_id.item() == image_token_id and feat_idx < len(feature_counts):
90-
expanded.extend([image_token_id] * feature_counts[feat_idx])
89+
if token_id.item() == vision_token_id and feat_idx < len(feature_counts):
90+
expanded.extend([vision_token_id] * feature_counts[feat_idx])
9191
feat_idx += 1
9292
else:
9393
expanded.append(token_id.item())
@@ -132,6 +132,62 @@ def to_cuda(x):
132132
return x.cuda()
133133

134134

135+
def process_multi_image_inputs(processor, image_paths: list[str], prompt: str):
136+
"""Process N ordered images + prompt into model inputs (Qwen-family).
137+
138+
Returns:
139+
(input_ids, pixel_values, image_grid_thw)
140+
"""
141+
if not _HAS_QWEN_VL_UTILS:
142+
raise ImportError("qwen-vl-utils required: pip install qwen-vl-utils")
143+
pils = [load_image(p).convert("RGB") for p in image_paths]
144+
messages = [
145+
{
146+
"role": "user",
147+
"content": [{"type": "image", "image": p} for p in pils] + [{"type": "text", "text": prompt}],
148+
}
149+
]
150+
image_inputs, video_inputs = process_vision_info(messages)
151+
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
152+
inputs = processor(text=[text], images=image_inputs, videos=video_inputs, padding=True, return_tensors="pt")
153+
return inputs.input_ids, inputs.get("pixel_values"), inputs.get("image_grid_thw")
154+
155+
156+
def process_video_inputs(processor, video_path: str, prompt: str, *, fps: float = 2.0):
157+
"""Process a video + prompt into model inputs (Qwen-family).
158+
159+
Frame decoding mirrors the Qwen3-VL training pipeline: fetch_video decodes at
160+
``fps``, then video_processor is called with do_sample_frames=False to use the
161+
pre-decoded frames as-is.
162+
163+
Returns:
164+
(input_ids, pixel_values_videos, video_grid_thw)
165+
"""
166+
if not _HAS_QWEN_VL_UTILS:
167+
raise ImportError("qwen-vl-utils required: pip install qwen-vl-utils")
168+
from qwen_vl_utils import fetch_video
169+
170+
frames = fetch_video({"video": video_path, "fps": fps})
171+
messages = [
172+
{
173+
"role": "user",
174+
"content": [{"type": "video"}, {"type": "text", "text": prompt}],
175+
}
176+
]
177+
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
178+
text_inputs = processor(text=[text], padding=True, return_tensors="pt")
179+
video_proc = processor.video_processor(videos=[frames], return_tensors="pt", do_sample_frames=False)
180+
# processor(text=...) without videos produces a single <|video_pad|> placeholder.
181+
# Pre-expand to match actual vision feature count so PP send/recv shapes are correct.
182+
video_token_id = processor.tokenizer.convert_tokens_to_ids("<|video_pad|>")
183+
input_ids = pre_expand_vision_tokens(
184+
text_inputs["input_ids"],
185+
video_proc["video_grid_thw"],
186+
vision_token_id=video_token_id,
187+
)
188+
return input_ids, video_proc.get("pixel_values_videos"), video_proc.get("video_grid_thw")
189+
190+
135191
def process_image_inputs(
136192
processor,
137193
image_path: Optional[str],
@@ -179,7 +235,7 @@ def _process_kimi_inputs(processor, image_path, prompt, image_token_id):
179235
]
180236
inputs = processor(messages=messages)
181237
grid_thws = getattr(inputs, "grid_thws", None)
182-
input_ids = pre_expand_image_tokens(inputs.input_ids, grid_thws, image_token_id)
238+
input_ids = pre_expand_vision_tokens(inputs.input_ids, grid_thws, image_token_id)
183239
return input_ids, inputs.pixel_values, grid_thws, None, None
184240

185241

‎examples/models/qwen/qwen3_vl/README.md‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ See the [peft.sh](peft.sh) script for LoRA fine-tuning with sequence-packing.
113113

114114
**Note:** LoRA/DoRA significantly reduces memory requirements, allowing for larger batch sizes and fewer GPUs.
115115

116+
## Controlling visual tokens computation budget
117+
Three independent CLI-overridable controls bound a sample's GPU cost. They compose:
118+
- **`dataset.min_pixels` / `dataset.max_pixels`** — image/frame resolutions lower and upper bound (defaults `200704` / `1003520`).
119+
- **`dataset.max_num_images` / `dataset.max_num_frames`** - limit count of images/frames (defaults `10` / `60`). Too many images → sample is dropped. Too many frames → frame list truncated.
120+
- **`dataset.max_visual_tokens`** — limit total visual tokens across all images and frames in a sample, computed post-rescaling as `prod(T,H,W) // merge_size²` (default `16384`; set to `None` to disable). Catches cases the other two miss (few images at high resolution, or many at low resolution). Exceeding samples are dropped.
121+
116122
## Finetuning with Energon Dataset
117123

118124
Follow the instructions [here](https://github.com/NVIDIA/Megatron-LM/tree/main/examples/multimodal#pretraining) to prepare `LLaVA-Pretrain` dataset in Energon format. Change the file `.nv-meta/dataset.yaml` to the following:
@@ -127,10 +133,65 @@ field_map:
127133
128134
Then, update the dataset path (`dataset.path=/path/to/energon/dataset`) in [peft_energon.sh](peft_energon.sh) and run the script.
129135

130-
131136
### Expected Training Dynamics
132137
We provide a [Weights & Biases report](https://api.wandb.ai/links/nvidia-nemo-fw-public/lczz4ixx) for the expected loss curves and grad norms.
133138

139+
## Dataset with Multiple Images
140+
141+
Below is an example for finetuning on a dataset containing multiple images in a sample, using a subset of [TIGER-Lab/Mantis-Instruct](https://huggingface.co/datasets/TIGER-Lab/Mantis-Instruct) dataset.
142+
143+
1. Download the `llava_665k_multi` subset of TIGER-Lab/Mantis-Instruct dataset from Hugging Face and unzip the images folder (NOTE: 44GB of disk space required):
144+
145+
```
146+
pip install -U "huggingface_hub[cli]"
147+
huggingface-cli download TIGER-Lab/Mantis-Instruct \
148+
--include "llava_665k_multi/*" \
149+
--repo-type dataset \
150+
--local-dir /path/to/Mantis-Instruct-LLaVA
151+
```
152+
153+
2. Run the following script to convert the data to webdataset format:
154+
155+
```
156+
python examples/models/vlm/qwen3_vl/prepare_mantis_energon.py \
157+
--source-dir/path/to/Mantis-Instruct-LLaVA \
158+
--output-dir /path/to/Mantis-Instruct-LLaVA/wds \
159+
--max-samples-per-tar 10000
160+
```
161+
162+
3. Run the following command to convert to megatron-energon format:
163+
164+
```
165+
cd /path/to/Mantis-Instruct-LLaVA/wds
166+
energon prepare ./
167+
```
168+
169+
select the following values for the presented options:
170+
171+
```
172+
> Please enter a desired train/val/test split like "0.5, 0.2, 0.3" or "8,1,1": 9,1,0
173+
> Do you want to create a dataset.yaml interactively? [Y/n]: Y
174+
> Please enter a number to choose a class: 9 (VQASample)
175+
> Do you want to set a simple field_map[Y] (or write your own sample_loader [n])? [Y/n]: Y
176+
> Please enter a webdataset field name for 'image' (<class 'torch.Tensor'>): jpg
177+
> Please enter a webdataset field name for 'context' (<class 'str'>): json[0][value]
178+
> Please enter a webdataset field name for 'answers' (typing.Optional[typing.List[str]], default: None): json[1][value]
179+
> Please enter a webdataset field name for 'answer_weights' (typing.Optional[torch.Tensor], default: None):
180+
```
181+
182+
4. Change the file `.nv-meta/dataset.yaml` to the following:
183+
184+
```yaml
185+
__module__: megatron.bridge.recipes.qwen_vl.data.energon.task_encoder
186+
__class__: ChatMLWebdataset
187+
field_map:
188+
imgs: jpgs
189+
conversation: json
190+
```
191+
192+
Follow previous instruction to run the finetuning with the prepared dataset.
193+
194+
134195
## Evaluation
135196

136197
Coming soon.

0 commit comments

Comments
 (0)