Skip to content

Commit 1bc3788

Browse files
Support multi-modal datasets (#495)
<!-- markdownlint-disable --> PLEASE FILL IN THE PR DESCRIPTION HERE ENSURING ALL CHECKLIST ITEMS (AT THE BOTTOM) HAVE BEEN CONSIDERED. ## Purpose <!--- Why your changes are needed --> FIX #290 By forwarding the conversations directly into Chat Completions API, and also observing that `AutoProcessor` returns tokenizer instance for text-only models, we can simplify the code a lot compared to #344. cc @shx2005 ## Description <!--- High-level concise summary of changes --> - Enable preprocessing of multimodal datasets. No user flag is required; we detect this automatically based on `isinstance(..., ProcessorMixin)`. - Add `--trust-remote-code` flag to data preparation and training scripts. - Add `hf_name` and `filter_fn` options to `DatasetConfig`. - Add support for ShareGPT4V dataset. - Only samples from COCO are supported right now. - You need to download the images separately since it's not handled by HF Datasets. Use the `COCO_DIR` environment variable to control where COCO images are read from. - To avoid copying the images when storing the preprocessed dataset and HTTP transfer to vLLM, we express image inputs in terms of file URLs instead of base64-encoded images. For vLLM to access those files, you should pass `--allowed-media-domain-paths /path/to/coco` when serving it. - Disable caching for dataset normalization as it makes debugging `normalize_fn` more difficult. To reduce overhead, this step is now executed after `raw_dataset.select`. - Use `--enforce-eager` for vLLM in e2e tests by default to reduce startup time. - Add `torchaudio` and `torchvision` to dependencies. - Fix whitespace issues in the help text of the scripts. ## Related Issue <!--- Link related issue if applicable --> #290 ## Tests <!--- Please describe in detail how you tested your changes. --> Add integration and e2e tests to ensure MM support. Note: - e2e smoke tests use a dummy COCO image so they can be run in CI. - e2e regression tests are only run if the real COCO images are downloaded. I have filled in: - [x] The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)". - [x] The test plan/results, such as providing test command and pasting the results. - [x] (Optional) The necessary documentation update. - [x] I (a human) have written or reviewed the code in this pr to the best of my ability. --------- Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>
1 parent 9296838 commit 1bc3788

21 files changed

Lines changed: 1223 additions & 378 deletions

docs/cli/prepare_data.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ python scripts/prepare_data.py \
2626

2727
Example: `meta-llama/Llama-3.1-8B-Instruct`
2828

29+
- **`--trust-remote-code`** (flag) Allow executing code from HF Hub when loading the target model's processor.
30+
2931
### Data Arguments
3032

3133
- **`--data`** (str, required, repeatable) Path to training data. Can be a HuggingFace dataset name or local path. Use multiple times to specify multiple datasets.

docs/cli/train.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ torchrun --standalone --nproc_per_node=4 scripts/train.py \
3232

3333
- **`--verifier-name-or-path`** (str, required) HuggingFace model ID or local path for the verifier/target model.
3434

35+
- **`--trust-remote-code`** (flag) Allow executing code from HF Hub when loading the verifier's tokenizer.
36+
3537
- **`--speculator-type`** (str, default: `"eagle3"`) Type of speculator model to train. Options: `eagle3`, `dflash`
3638

3739
- **`--from-pretrained`** (str, default: `""`) Path to a pretrained draft model to finetune.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ dependencies = [
5050
"safetensors",
5151
"setuptools",
5252
"torch>=2.9.0,<=2.11.0",
53+
"torchaudio",
54+
"torchvision",
5355
"tqdm>=4.66.3,<=4.67.3",
5456
"transformers>=4.56.1,<5.9.0",
5557
"typer>=0.12.0",
@@ -249,6 +251,7 @@ select = [
249251
"PTH", # os.path is acceptable in scripts
250252
"T201", # print statements are acceptable in scripts
251253
"SLF001", # allow private member access for model configuration
254+
"PLR0915", # allow long parse_args functions
252255
]
253256

254257
"examples/**/*.py" = [

scripts/data_generation_offline.py

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
DEFAULT_REQUEST_TIMEOUT,
3232
generate_hidden_states_async,
3333
)
34+
from speculators.train.data import build_client_item
3435
from speculators.train.logger import setup_root_logger
3536

3637
logger = logging.getLogger(__name__)
@@ -66,8 +67,8 @@ def parse_args():
6667
type=str,
6768
default=None,
6869
help=(
69-
"HuggingFace model ID or local path for target model (default auto select)."
70-
"For verification purposes only."
70+
"HuggingFace model ID or local path for target model "
71+
"(default auto select). For verification purposes only."
7172
),
7273
)
7374
parser.add_argument(
@@ -113,16 +114,16 @@ def parse_args():
113114
type=int,
114115
default=32,
115116
help=(
116-
"Number of active vLLM requests at a time."
117+
"Number of active vLLM requests at a time. "
117118
"Note: number of async workers set to 2*concurrency"
118119
),
119120
)
120121
parser.add_argument(
121122
"--validate-outputs",
122123
action="store_true",
123124
help=(
124-
"Load generated safetensor files and check output token ids match prompt"
125-
" tokens and hidden states seq_len matches num tokens"
125+
"Load generated safetensor files and check output token ids match "
126+
"prompt tokens and hidden states seq_len matches num tokens"
126127
),
127128
)
128129
parser.add_argument(
@@ -276,16 +277,14 @@ async def worker(
276277
queue.task_done()
277278
continue
278279

279-
input_ids = item["input_ids"].tolist()
280-
281280
target_hidden_states_path = hidden_states_output_dir / f"hs_{idx}.safetensors"
282281

283282
try:
284283
async with vllm_semaphore: # Limit number of active generate calls
285284
hidden_states_path = await generate_hidden_states_async(
286285
client,
287286
model,
288-
input_ids,
287+
item,
289288
timeout=request_timeout,
290289
max_retries=max_retries,
291290
)
@@ -295,7 +294,9 @@ async def worker(
295294
)
296295
if validate_outputs:
297296
await asyncio.to_thread(
298-
check_safetensors_file, target_hidden_states_path, input_ids
297+
check_safetensors_file,
298+
target_hidden_states_path,
299+
item["input_ids"],
299300
)
300301
except Exception as e:
301302
if fail_on_error:
@@ -325,12 +326,15 @@ async def _feed_queue(to_process, dataset, queue, cancel_event):
325326
for i in to_process:
326327
if cancel_event.is_set():
327328
break
328-
item = dataset[i]
329+
330+
dataset_item = dataset[i]
331+
client_item = build_client_item(dataset_item) | {"idx": i}
332+
329333
# Check cancel_event while waiting for queue space to avoid
330334
# deadlocking when all workers have died.
331335
while not cancel_event.is_set():
332336
try:
333-
queue.put_nowait({"idx": i, "input_ids": item["input_ids"]})
337+
queue.put_nowait(client_item)
334338
break
335339
except asyncio.QueueFull:
336340
await asyncio.sleep(0.1)
@@ -397,7 +401,7 @@ async def generate_and_save_hidden_states(args, dataset):
397401
if args.model and args.model != model_id:
398402
raise ValueError(
399403
f"An explicit model name was passed ({args.model}) which doesn't match"
400-
"found model_id {model_id}."
404+
f" found model_id {model_id}."
401405
"Please make sure --endpoint is set to the correct vllm instance."
402406
)
403407

scripts/prepare_data.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ def parse_args():
4949
required=True,
5050
help="HuggingFace model ID or local path for target model",
5151
)
52+
parser.add_argument(
53+
"--trust-remote-code",
54+
action="store_true",
55+
help=(
56+
"Allow executing code from HF Hub when loading the target model's "
57+
"processor."
58+
),
59+
)
5260

5361
# Data arguments
5462
parser.add_argument(
@@ -75,7 +83,7 @@ def parse_args():
7583
type=str,
7684
default=None,
7785
help=(
78-
"Path to save token frequency distribution"
86+
"Path to save token frequency distribution "
7987
"(default: args.output / 'token_freq.pt')"
8088
),
8189
)
@@ -177,6 +185,7 @@ def main():
177185
assistant_pattern=args.assistant_pattern,
178186
turn_dropout=args.turn_dropout,
179187
minimum_valid_tokens=args.minimum_valid_tokens,
188+
trust_remote_code=args.trust_remote_code,
180189
)
181190

182191
log.info("Done preparing data")

scripts/train.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ def main(args: argparse.Namespace):
259259
args.verifier_name_or_path,
260260
transformer_layer_config.vocab_size,
261261
args.mask_token_id,
262+
trust_remote_code=args.trust_remote_code,
262263
)
263264

264265
registry = SpeculatorModel.registry
@@ -398,6 +399,11 @@ def _checkpoint_freq(value: str) -> float:
398399
def parse_args():
399400
parser = argparse.ArgumentParser()
400401
parser.add_argument("--verifier-name-or-path", type=str, required=True)
402+
parser.add_argument(
403+
"--trust-remote-code",
404+
action="store_true",
405+
help="Allow executing code from HF Hub when loading the verifier's tokenizer.",
406+
)
401407
parser.add_argument(
402408
"--speculator-type",
403409
type=str,

src/speculators/data_generation/configs.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Configuration registries for data generation pipeline."""
22

3+
import os
34
from collections.abc import Callable
45
from dataclasses import dataclass
56

@@ -9,14 +10,15 @@
910
]
1011

1112

12-
@dataclass
13+
@dataclass(kw_only=True)
1314
class DatasetConfig:
1415
"""Configuration for loading a dataset"""
1516

1617
name: str
1718
hf_path: str
18-
split: str
1919
subset: str | None = None
20+
split: str
21+
filter_fn: Callable[[dict], bool] | None = None
2022
normalize_fn: Callable[[dict], dict] | None = None
2123

2224

@@ -35,6 +37,60 @@ def _normalize_gsm8k(example: dict) -> dict:
3537
}
3638

3739

40+
def get_coco_dir():
41+
return os.getenv("COCO_DIR") or "coco/"
42+
43+
44+
def _parse_sharegpt4v_part(part: str, image_path: str):
45+
if part == "<image>":
46+
return {"type": "image", "path": image_path}
47+
48+
return {"type": "text", "text": part}
49+
50+
51+
def _parse_sharegpt4v_user_content(content: str, image_path: str):
52+
return [_parse_sharegpt4v_part(part, image_path) for part in content.split("\n")]
53+
54+
55+
def _parse_sharegpt4v_assistant_content(content: str):
56+
return [{"type": "text", "text": content}]
57+
58+
59+
def _filter_sharegpt4v_coco(example: dict) -> bool:
60+
return example["image"].startswith("coco/")
61+
62+
63+
def _normalize_sharegpt4v_coco(example: dict) -> dict:
64+
coco_dir = get_coco_dir()
65+
image_path = os.path.join(coco_dir, example["image"].removeprefix("coco/"))
66+
67+
if not os.path.exists(image_path):
68+
state_str = "set to" if os.getenv("COCO_DIR") else "default"
69+
70+
raise ValueError(
71+
f"No image found at <{image_path}>. "
72+
f"Please download COCO 2017 Train Images from "
73+
f"<http://images.cocodataset.org/zips/train2017.zip> and place the "
74+
f"extracted folder under `COCO_DIR` ({state_str}: `{coco_dir}`)."
75+
)
76+
77+
messages = [
78+
(
79+
turn
80+
| {
81+
"value": (
82+
_parse_sharegpt4v_user_content(turn["value"], image_path)
83+
if turn["from"] in ("human", "user")
84+
else _parse_sharegpt4v_assistant_content(turn["value"])
85+
)
86+
}
87+
)
88+
for turn in example["conversations"]
89+
]
90+
91+
return {"conversations": messages}
92+
93+
3894
DATASET_CONFIGS: dict[str, DatasetConfig] = {
3995
"sharegpt": DatasetConfig(
4096
name="sharegpt",
@@ -50,8 +106,17 @@ def _normalize_gsm8k(example: dict) -> dict:
50106
"gsm8k": DatasetConfig(
51107
name="gsm8k",
52108
hf_path="openai/gsm8k",
53-
split="train",
54109
subset="main",
110+
split="train",
55111
normalize_fn=_normalize_gsm8k,
56112
),
113+
# NOTE: You need to serve vLLM with `--allowed-local-media-path /path/to/coco`
114+
"sharegpt4v_coco": DatasetConfig(
115+
name="sharegpt4v_coco",
116+
hf_path="Lin-Chen/ShareGPT4V",
117+
subset="ShareGPT4V",
118+
split="train",
119+
filter_fn=_filter_sharegpt4v_coco,
120+
normalize_fn=_normalize_sharegpt4v_coco,
121+
),
57122
}

0 commit comments

Comments
 (0)