@@ -50,9 +50,10 @@ def __init__(
5050 max_latent_size = 64 ,
5151 vit_patch_size = 14 ,
5252 max_num_patch_per_side = 70 ,
53- max_num_tokens = 32768 ,
54- expected_num_tokens = 31000 ,
53+ max_num_tokens = 36864 ,
54+ expected_num_tokens = 32768 ,
5555 max_num_tokens_per_sample = 16384 ,
56+ max_buffer_size = 50 ,
5657 ):
5758 self .text_cond_dropout_prob = text_cond_dropout_prob
5859 self .vit_cond_dropout_prob = vit_cond_dropout_prob
@@ -64,6 +65,7 @@ def __init__(
6465 self .max_num_tokens = max_num_tokens
6566 self .expected_num_tokens = expected_num_tokens
6667 self .max_num_tokens_per_sample = max_num_tokens_per_sample
68+ self .max_buffer_size = max_buffer_size
6769
6870
6971@lru_cache (maxsize = 16 )
@@ -123,14 +125,12 @@ def __init__(
123125 self ,
124126 data_config : BagelDataConfig ,
125127 interpolate_pos : bool = False ,
126- max_seq_length : Optional [int ] = None ,
127128 ):
128129 super ().__init__ ()
129130 self .tokenizer = get_tokenizer ()
130131 self .special_tokens = self .tokenizer .new_special_token_ids
131132 print (f"{ self .special_tokens = } " )
132133 self .data_config = data_config
133- self .group_size = max_seq_length if max_seq_length is not None else 4096
134134
135135 self .handlers = {
136136 name : cls (self .tokenizer , self .special_tokens , self .data_config )
@@ -148,6 +148,11 @@ def __init__(
148148 self ._tp_size = getattr (_args , 'tensor_model_parallel_size' , 1 )
149149 self ._sequence_parallel = getattr (_args , 'sequence_parallel' , False )
150150
151+ # Overflow buffer: samples that didn't fit in the previous pack are held
152+ # here and prepended to the next select_samples_to_pack call so that no
153+ # sample is wasted and every yielded pack meets expected_num_tokens.
154+ self ._overflow_buffer : List = []
155+
151156 def _build_transforms (self , subflavors ):
152157 transforms_item = {}
153158 image_args = subflavors .get ('image_transform_args' )
@@ -176,76 +181,133 @@ def _build_transforms(self, subflavors):
176181
177182 return transforms_item
178183
179- def select_samples_to_pack (self , samples : List [BagelSample ]) -> List [List [Dict [ str , torch . Tensor ] ]]:
180- """Select samples from buffer to form packs.
184+ def select_samples_to_pack (self , samples : List [BagelSample ]) -> List [List [BagelSample ]]:
185+ """Select samples from buffer to form packs with overflow management .
181186
182- Implements Bagel's packing strategy (mirrors dataset_base.py __iter__):
183- - Each pack starts by consuming all mandatory samples first
184- - Then greedily fills with non-mandatory samples
185- - Uses expected_num_tokens as the "pack is ready" threshold
186- - Uses max_num_tokens as the hard upper limit
187+ Mirrors the original Bagel PackedDataset.__iter__ packing strategy:
188+ - Prepend overflow samples from the previous call (like the original buffer)
189+ - Uses expected_num_tokens as the soft threshold to yield a pack
190+ - Uses max_num_tokens as the hard upper limit per pack
187191 - Samples exceeding max_num_tokens_per_sample are skipped
188- - Overflow samples go into a buffer for priority use in next pack
189-
190- Selects which samples will be packed together.
191-
192- This function receives a list of samples (size according to the selected packing_buffer_size),
193- and partitions those samples into groups that shall be packed together.
192+ - If the last pack doesn't reach expected_num_tokens, its samples are
193+ held in the overflow buffer for the next call (no short batches)
194194
195195 Args:
196- samples (List[Dict[str, torch.Tensor]]): List of samples from the buffer, each containing
197- tokenized data with keys like 'input_ids', 'labels', 'loss_mask', etc.
196+ samples: List of samples from Energon's reading buffer.
198197
199198 Returns:
200- List[List[Dict[str, torch.Tensor]]]: List of groups, where each group is a list of samples
201- that should be packed together. Each group's total length will not exceed group_size.
199+ List of groups where each group is a list of samples to pack together.
200+ Every returned pack is guaranteed to have >= expected_num_tokens
201+ (except when the data source is exhausted).
202202
203203 NOTE: Energon dataloader calls this method internally if packing is used.
204204 Please see https://nvidia.github.io/Megatron-Energon/advanced/packing.html
205205 """
206+ # --- Step 1: Load packing configuration ---
207+ # max_tokens: hard ceiling for a single pack (prevents OOM)
208+ # max_per_sample: discard any sample longer than this
209+ # expected: soft target — once a pack reaches this, emit it
210+ # max_buffer_size: cap on how many "doesn't fit" samples we hold locally
206211 max_tokens = self .data_config .max_num_tokens
207212 max_per_sample = self .data_config .max_num_tokens_per_sample
208213 expected = self .data_config .expected_num_tokens
214+ max_buffer_size = self .data_config .max_buffer_size
215+
216+ # --- Step 2: Merge overflow from previous call with new samples ---
217+ # Overflow samples are placed first so they get priority (equivalent to
218+ # the original code's "prefer_buffer_before" behavior where buffered
219+ # samples are consumed before drawing new ones from the data stream).
220+ all_samples = self ._overflow_buffer + list (samples )
221+ self ._overflow_buffer = []
222+
223+ # --- Step 3: Filter out oversized samples ---
224+ # Samples exceeding max_num_tokens_per_sample are permanently discarded,
225+ # matching the original "skip a sample with length ..." behavior.
226+ # Token count includes +2 per segment in sequence_plan (bos/eos overhead).
227+ valid_samples = []
228+ for s in all_samples :
229+ token_count = s .num_tokens + 2 * len (s .sequence_plan )
230+ if token_count <= max_per_sample :
231+ valid_samples .append ((s , token_count ))
209232
210- # Filter out oversized samples
211- valid_samples = [s for s in samples if s .num_tokens <= max_per_sample ]
212- print (f"{ len (valid_samples )= } , { valid_samples = } " )
213233 if not valid_samples :
214234 return []
215235
216- # # Separate mandatory and non-mandatory
217- # mandatory = [s for s in valid_samples if s.is_mandatory]
218- # non_mandatory = [s for s in valid_samples if not s.is_mandatory]
219-
236+ # --- Step 4: Greedy bin-packing with overflow buffer ---
237+ # Walk through valid_samples one by one, trying to fit each into
238+ # current_pack. Three outcomes per sample:
239+ # (a) Fits and pack not yet full → append to current_pack
240+ # (b) Fits and pack reaches expected → emit pack, start fresh
241+ # (c) Doesn't fit → stash in pending_candidates for later
220242 packs = []
221243 current_pack = []
222244 current_tokens = 0
245+ pending_candidates = []
223246
224- # # Start each pack with a mandatory sample if available
225- # if mandatory:
226- # m_sample = mandatory.pop(0)
227- # current_pack.append(m_sample)
228- # current_tokens = m_sample.num_tokens + 2 * len(m_sample.sequence_plan)
229-
230- # # Fill with remaining samples (sorted by size for better packing)
231- # remaining = mandatory + non_mandatory
232- # random.shuffle(remaining)
233-
234- for sample in samples :
235- sample_tokens = sample .num_tokens + 2 * len (sample .sequence_plan )
236- if current_tokens + sample_tokens <= max_tokens :
247+ for sample , token_count in valid_samples :
248+ if current_tokens + token_count <= max_tokens :
249+ # Case (a)/(b): sample fits within the hard limit
237250 current_pack .append (sample )
238- current_tokens += sample_tokens
239- elif current_pack :
240- # Current pack is full, start a new one
241- packs .append (current_pack )
242- current_pack = [sample ]
243- current_tokens = sample_tokens
244-
245- if current_pack :
246- packs .append (current_pack )
251+ current_tokens += token_count
252+
253+ if current_tokens >= expected :
254+ # Pack reached the soft target — emit it
255+ packs .append (current_pack )
256+ current_pack = []
257+ current_tokens = 0
258+
259+ # Drain pending_candidates into the fresh pack immediately.
260+ # This gives previously-buffered (typically large) samples a
261+ # chance to be placed while the new pack is still empty.
262+ for pend_sample , pend_tokens in pending_candidates :
263+ if current_tokens + pend_tokens <= max_tokens :
264+ current_pack .append (pend_sample )
265+ current_tokens += pend_tokens
266+ if current_tokens >= expected :
267+ packs .append (current_pack )
268+ current_pack = []
269+ current_tokens = 0
270+ else :
271+ # Still doesn't fit — persist to cross-call overflow
272+ self ._overflow_buffer .append (pend_sample )
273+ pending_candidates = []
274+ else :
275+ # Case (c): sample would exceed the hard limit for current pack
276+ if len (pending_candidates ) < max_buffer_size :
277+ # Stash it; we'll try again after the current pack is emitted
278+ pending_candidates .append ((sample , token_count ))
279+ else :
280+ # Overflow buffer is full — force-emit the current pack even
281+ # though it may not have reached `expected`. This matches the
282+ # original behavior: "buffer full + can't fit → yield batch".
283+ if current_pack :
284+ packs .append (current_pack )
285+ # Start a new pack with the sample that triggered the flush
286+ current_pack = [sample ]
287+ current_tokens = token_count
288+ # Try to fit pending_candidates into the new pack
289+ for pend_sample , pend_tokens in pending_candidates :
290+ if current_tokens + pend_tokens <= max_tokens :
291+ current_pack .append (pend_sample )
292+ current_tokens += pend_tokens
293+ else :
294+ self ._overflow_buffer .append (pend_sample )
295+ pending_candidates = []
296+
297+ # --- Step 5: Handle leftover samples at the end of this call ---
298+ # current_pack is within max_tokens, but pending_candidates may push
299+ # the total over. Only emit if both conditions are met: total tokens
300+ # >= expected AND <= max_tokens. Otherwise, hold everything for next call.
301+ remaining = current_pack + [s for s , _ in pending_candidates ]
302+ if remaining :
303+ remaining_tokens = sum (
304+ s .num_tokens + 2 * len (s .sequence_plan ) for s in remaining
305+ )
306+ if remaining_tokens >= expected and remaining_tokens <= max_tokens :
307+ packs .append (remaining )
308+ else :
309+ self ._overflow_buffer .extend (remaining )
247310
248- print (f"{ len (packs )= } , { packs = } " )
249311 return packs
250312
251313 @stateless
@@ -275,10 +337,10 @@ def encode_sample(self, sample: Dict[str, Any]):
275337 - 'vlm': VLM SFT data (jsonl conversations + images)
276338 - 't2i': Text-to-image data (parquet image + caption)
277339 """
278- print (f"{ sample = } " )
340+ # print(f"{sample=}")
279341 subflavors = sample .get ('__subflavors__' , {})
280342 task = subflavors .get ('task' , None )
281- print (f"{ subflavors = } " )
343+ # print(f"{subflavors=}")
282344
283345 kwargs = self ._build_transforms (subflavors )
284346 handler = self .handlers .get (task )
@@ -304,7 +366,7 @@ def encode_batch(self, batch: BagelPackedBatch) -> BagelPackedBatch:
304366 return batch
305367
306368
307- def bagel_vlm_dataloader_provider (train_val_test_num_samples , max_seq_length : Optional [ int ] = None ):
369+ def bagel_vlm_dataloader_provider (train_val_test_num_samples ):
308370 args = get_args ()
309371
310372 bagel_config = BagelDataConfig (
@@ -315,17 +377,17 @@ def bagel_vlm_dataloader_provider(train_val_test_num_samples, max_seq_length: Op
315377 vit_patch_size = getattr (args , 'vit_patch_size' , 14 ),
316378 max_latent_size = getattr (args , 'max_latent_size' , 64 ),
317379 max_num_patch_per_side = getattr (args , 'max_num_patch_per_side' , 70 ),
318- max_num_tokens = getattr (args , 'max_num_tokens' , 16384 ), # 36864
319- expected_num_tokens = getattr (args , 'expected_num_tokens' , 16384 ), # 32768
380+ max_num_tokens = getattr (args , 'max_num_tokens' , 36864 ),
381+ expected_num_tokens = getattr (args , 'expected_num_tokens' , 32768 ),
320382 max_num_tokens_per_sample = getattr (args , 'max_num_tokens_per_sample' , 16384 ),
383+ max_buffer_size = getattr (args , 'max_buffer_size' , 50 ),
321384 )
322385
323386 return train_valid_test_dataloaders_provider (
324387 train_val_test_num_samples ,
325388 task_encoder = BagelTaskEncoder (
326389 data_config = bagel_config ,
327390 interpolate_pos = args .interpolate_pos ,
328- max_seq_length = max_seq_length ,
329391 )
330392 )
331393
@@ -384,15 +446,14 @@ def save_state(self):
384446 return self ._dataloader .save_state_rank ()
385447
386448
387- def train_valid_test_dataloaders_provider (train_val_test_num_samples , task_encoder = None ):
449+ def train_valid_test_dataloaders_provider (train_val_test_num_samples , task_encoder ):
388450
389451 args = get_args ()
390452
391453 # Dataloader is only on specific ranks.
392454 if not is_dataloader_rank ():
393455 return None , None , None
394456
395- tokenizer = get_tokenizer ()
396457 worker_debug_path = None
397458 worker_log_level = 0
398459
@@ -409,25 +470,12 @@ def train_valid_test_dataloaders_provider(train_val_test_num_samples, task_encod
409470 worker_log_level = worker_log_level ,
410471 )
411472
412- # task_encoder = BagelTaskEncoder(
413- # tokenizer=tokenizer,
414- # special_tokens=tokenizer.new_special_token_ids,
415- # data_config=bagel_config,
416- # )
417- # task_encoder=BagelTaskEncoder(
418- # data_config=bagel_config,
419- # interpolate_pos=args.interpolate_pos,
420- # max_seq_length=max_seq_length,
421- # )
422-
423- # # Build dataset paths with weights
424- # dataset_configs = getattr(args, 'datasets', [])
425- # if not dataset_configs:
426- # raise ValueError("data_config must contain 'datasets' list")
427- dname = args .data_path [0 ] if type (args .data_path ) is list else args .data_path
473+ # Build dataset paths with weights
474+ assert (isinstance (args .data_path , list ) and len (args .data_path ) == 1 ) or \
475+ isinstance (args .data_path , str )
476+ dname = args .data_path [0 ] if isinstance (args .data_path , list ) else args .data_path
428477
429478 # For single dataset
430- # if len(dataset_configs) == 1:
431479 dataset = get_train_dataset (
432480 dname ,
433481 batch_size = args .micro_batch_size ,
@@ -440,23 +488,6 @@ def train_valid_test_dataloaders_provider(train_val_test_num_samples, task_encod
440488 handler = print_error_handler ,
441489 image_decode = "pil" ,
442490 )
443- # else:
444- # # For blended datasets
445- # blend_config = []
446- # for ds_cfg in dataset_configs:
447- # blend_config.append({
448- # 'path': ds_cfg['path'],
449- # 'weight': ds_cfg.get('weight', 1.0),
450- # 'subflavors': ds_cfg.get('subflavors', {}),
451- # })
452- # dataset = get_train_dataset(
453- # blend_config,
454- # worker_config=worker_config,
455- # batch_size=args.micro_batch_size,
456- # task_encoder=task_encoder,
457- # max_samples_per_sequence=getattr(args, 'max_samples_per_sequence', None),
458- # shuffle_buffer_size=getattr(args, 'shuffle_buffer_size', 1000),
459- # )
460491
461492 # Build savable dataloader
462493 dataloader = get_savable_loader (
0 commit comments