Diff Coverage

Diff: origin/master...HEAD, staged and unstaged changes

Source File Diff Coverage (%) Missing Lines
hyper_parallel/components/optim/builders.py 100%  
hyper_parallel/components/optim/parameter_groups.py 100%  
hyper_parallel/core/optimizer/__init__.py 100%  
hyper_parallel/core/optimizer/lr_scheduler.py 0.0% 112-113
hyper_parallel/core/optimizer/muon.py 87.2% 110,499,501,523,530-531,544,766-767,949,973,981,1000,1058
hyper_parallel/core/optimizer/muon_shard.py 12.1% 50-52,63-69,74-81,83-91,93-101,106-107,109-111,113-119,136-137,139-143,154-157,159-161,163-167,169-170,174-177,188-191,193,195-200,202-203,212-213,215-217,219-223,225-231,233,235,243,253-254,256-257,259-260,265-266,268-269,271-273,275-276,280,282-283,285,296-302,304-307,315-317,337-339,341-348,350-355,366-367,369,384,389-391,393,400-401,416-420,422-425,427-432,434-435,437,439-442,444,446,458-459,461-466,468-475,477-478,480,482-489,491,493,497,502,508-509,511,513
hyper_parallel/core/optimizer/optimizer.py 0.0% 426
hyper_parallel/core/optimizer/utils.py 64.7% 28-29,42-44,49-51,58,60,68,70,76-80,89
hyper_parallel/data/batching/attention_runtime.py 0.0% 49,71
hyper_parallel/data/batching/build_collate_fn.py 0.0% 91-93,95,98,100
hyper_parallel/data/batching/build_dataloader.py 0.0% 233,251,391-392,399-400,437-438,444-445
hyper_parallel/data/batching/get_batch.py 0.0% 151,172,178-182,196
hyper_parallel/data/batching/sequence_boundaries.py 0.0% 28-29
hyper_parallel/data/indexed/indexed_data_config.py 0.0% 213,215-216,218
hyper_parallel/data/indexed/indexed_dataset.py 0.0% 185-186
hyper_parallel/data/indexed/indexed_simple_blended_dataset.py 0.0% 74-78
hyper_parallel/data/parallel/batch_parallel.py 0.0% 306-309
hyper_parallel/data/parallel/batch_sampler.py 100%  
hyper_parallel/data/text/build_data_transform.py 0.0% 63-64
hyper_parallel/data/text/transform_dataset.py 0.0% 19,260
hyper_parallel/data/tools/offline_preparation.py 0.0% 23,49,714-716,750
hyper_parallel/data/vlm/get_batch.py 0.0% 17
hyper_parallel/trainer/config/optimization.py 100%  
hyper_parallel/trainer/runtime/data_iterator.py 0.0% 22
hyper_parallel/core/optimizer/lr_scheduler.py
108
109
110
111
112
113
114
115
116
        if current_step > lr_decay_steps:
            return min_lr_ratio

        progress = float(current_step - num_warmup_steps) / float(max(1, lr_decay_steps - num_warmup_steps))
        if not 0 <= progress <= 1:
            raise ValueError(f"cosine schedule progress must be in [0, 1], but got {progress}")
        factor = 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress))
        factor = factor * (1 - min_lr_ratio) + min_lr_ratio
        return max(0, factor)
hyper_parallel/core/optimizer/muon.py
106
107
108
109
110
111
112
113
def _resolve_muon_advanced_options(options: Dict[str, Any]) -> _MuonAdvancedOptions:
    """Validate and fill low-frequency Muon keyword options."""
    unknown = options.keys() - _MUON_ADVANCED_DEFAULTS.keys()
    if unknown:
        raise TypeError(f"Muon got unexpected keyword arguments: {', '.join(sorted(unknown))}")
    resolved = dict(_MUON_ADVANCED_DEFAULTS)
    resolved.update(options)
    return _MuonAdvancedOptions(**resolved)
495
496
497
498
499
500
501
502
503
504
505
    ) -> Dict[torch.nn.Parameter, torch.Tensor]:
        """Compute first-order momentum and return bfloat16 NS inputs."""
        momentum = group["momentum"]
        if not isinstance(momentum, (list, tuple)):
            momentum = [momentum]
        if len(momentum) == 1:
            momentum = (momentum[0], momentum[0])
        momentum1, momentum2 = momentum
        # Pre-filter params with valid grads and ensure momentum buffers exist
        valid_params = []
        grads = []
519
520
521
522
523
524
525
526
527

        grads = [to_local_if_dtensor(grad) for grad in grads]
        bufs = [to_local_if_dtensor(buffer) for buffer in bufs]
        if self.momentum_update_fn is not None:
            custom_updates = list(self.momentum_update_fn(grads, bufs, momentum1, momentum2, group["nesterov"]))
            if len(custom_updates) != len(valid_params):
                raise ValueError(
                    "momentum_update_fn must return one update tensor per input gradient, "
                    f"got {len(custom_updates)} updates for {len(valid_params)} gradients"
526
527
528
529
530
531
532
533
534
535
                    "momentum_update_fn must return one update tensor per input gradient, "
                    f"got {len(custom_updates)} updates for {len(valid_params)} gradients"
                )
            custom_updates = [update.to(torch.bfloat16) for update in custom_updates]
            param_updates = dict(zip(valid_params, custom_updates))
            return param_updates

        # Match muon_update_core():
        # m_for_update = grad + momentum1 * m_old
        # m_new = grad + momentum2 * m_old
540
541
542
543
544
545
546
547
        torch._foreach_add_(bufs, grads)

        if group["nesterov"]:
            torch._foreach_mul_(local_us, momentum1)
            torch._foreach_add_(local_us, grads)

        if local_us[0].dtype != torch.bfloat16:
            local_us = [update.to(torch.bfloat16) for update in local_us]
762
763
764
765
766
767
768
769
770
771
                reshaped_inputs[0].shape,
            )

        for reshaped_input in reshaped_inputs:
            if reshaped_input.untyped_storage().data_ptr() != working_input.untyped_storage().data_ptr():
                raise ValueError("reshape_fn must return views that share storage with the working NS input tensor")

        return working_input, reshaped_inputs

    def _prepare_ns_transform(
945
946
947
948
949
950
951
952
953
            no_shard: bool = False,
    ) -> Dict[torch.nn.Parameter, torch.Tensor]:
        """Compute native reshape/view NS updates without transform bookkeeping."""
        if not p_list:
            return {}
        state = {"reshape_groups": defaultdict(list), "origin_shapes": {}, "working_inputs": {}, "restored": {}}
        for param in p_list:
            local_shape = getattr(param, "local_shape", None)
            if local_shape is None:
969
970
971
972
973
974
975
976
977
                scale = compute_muon_slice_scale(
                    update, group["matched_adamw_rms"], zero_rms_scale_mode=group["zero_rms_scale_mode"]
                )
                if group["apply_lr_in_update"]:
                    scale *= -group["lr"]
                update.mul_(scale)
                reshaped_input.copy_(update.contiguous().view_as(reshaped_input))

        for param in p_list:
977
978
979
980
981
982
983
984
985
        for param in p_list:
            update = ns_inputs[param].view(state["origin_shapes"][param])
            working_input = state["working_inputs"][param]
            if working_input.untyped_storage().data_ptr() != update.untyped_storage().data_ptr():
                update.copy_(working_input)
            state["restored"][param] = update
        return state["restored"]

    def _compute_batched_ns_updates_with_transform(
 996
 997
 998
 999
1000
1001
1002
1003
1004

        """

        if not p_list:
            return {}

        state: _TransformNSState = {
            "reshape_groups": defaultdict(list),
            "origin_shapes": {},
1054
1055
1056
1057
1058
1059
1060
1061
            reshaped_update = ns_inputs[p].view(state["origin_shapes"][p])

            if (state["working_inputs"][p].untyped_storage().data_ptr()
                    != reshaped_update.untyped_storage().data_ptr()):
                reshaped_update.copy_(state["working_inputs"][p])
            state["restored"][p] = reshaped_update

        return state["restored"]
hyper_parallel/core/optimizer/muon_shard.py
46
47
48
49
50
51
52
53
54
55
56
        param: torch.Tensor,
        stage_metas: List[ParamShardStageMeta],
) -> ParamShardMeta:
    """Construct shard metadata from real local-shape plans."""
    global_shape = tuple(param.shape)
    local_shape = tuple(to_local_if_dtensor(param.data).shape)
    return ParamShardMeta(
        global_shape=global_shape,
        global_numel=math.prod(global_shape),
        local_shape=local_shape,
        local_numel=math.prod(local_shape),
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123


def _get_mesh_shape(device_mesh) -> Tuple[int, ...]:
    """Get mesh shape."""
    mesh = getattr(device_mesh, "mesh", None)
    if mesh is not None:
        return tuple(mesh.shape)
    mesh_shape = getattr(device_mesh, "mesh_shape", None)
    if mesh_shape is not None:
        return tuple(mesh_shape)
    return ()


def _get_local_shape_kinds(global_shape: Tuple[int, ...], shard_meta: ParamShardMeta) -> List[Tuple[int, ...]]:
    """Collect all distinct local shapes implied by the shard metadata."""
    dim_size_options: Dict[int, List[int]] = {}
    for stage_meta in shard_meta.stage_metas:
        tensor_dim = stage_meta.tensor_dim
        seen_sizes = []
        for split_size in stage_meta.split_sizes:
            if split_size not in seen_sizes:
                seen_sizes.append(split_size)
        dim_size_options[tensor_dim] = seen_sizes

    shape_kinds = [list(global_shape)]
    for tensor_dim, size_options in sorted(dim_size_options.items()):
        next_shape_kinds = []
        for shape_kind in shape_kinds:
            for size in size_options:
                next_shape = list(shape_kind)
                next_shape[tensor_dim] = size
                next_shape_kinds.append(next_shape)
        shape_kinds = next_shape_kinds

    unique_shape_kinds = []
    seen = set()
    for shape_kind in shape_kinds:
        shape_kind = tuple(shape_kind)
        if shape_kind in seen:
            continue
        seen.add(shape_kind)
        unique_shape_kinds.append(shape_kind)
    return unique_shape_kinds


def _debug_param_shard_metadata(hsdp_group, param_to_meta: Dict[torch.nn.Parameter, ParamShardMeta]) -> None:
    """Log shard metadata for the group on rank 0 when distributed is ready."""
    if not dist.is_available() or not dist.is_initialized():
        return

    rank = dist.get_rank()
    if rank != 0:
        return

    for record in hsdp_group.records:
        param = record.param
        shard_meta = param_to_meta.get(param)
        if shard_meta is None:
            continue
        local_shape_kinds = _get_local_shape_kinds(shard_meta.global_shape, shard_meta)
        logger.debug_rank0(
            "name=%s, placements=%s, mesh_shape=%s, global_shape=%s, local_shapes=%s",
            getattr(param, "model_name", f"param_{record.index}"),
            [str(placement) for placement in param.placements],
            _get_mesh_shape(param.device_mesh),
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
        shard_size: int,
        device: torch.device,
) -> List[List[Tuple[int, ...]]]:
    """All-gather per-parameter shapes within one shard process group."""
    if shard_pg is None or shard_size <= 1:
        return [[tuple(shape) for shape in local_shapes]]

    shape_tensor = torch.tensor(local_shapes, dtype=torch.int64, device=device)
    gathered = torch.empty((shard_size, *shape_tensor.shape), dtype=torch.int64, device=device)
    dist.all_gather_into_tensor(gathered.view(-1), shape_tensor.contiguous().view(-1), group=shard_pg)
    gathered_cpu = gathered.cpu().tolist()
    return [
        [tuple(shape) for shape in rank_shapes]
        for rank_shapes in gathered_cpu
    ]
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
        hsdp_group: HSDPCommGroup,
) -> Dict[torch.Tensor, ParamShardMeta]:
    """Build per-parameter shard metadata before HSDP batching."""
    # pylint: disable=too-many-locals
    layout_spec = hsdp_group.layout_spec
    shard_pgs = hsdp_group.shard_pgs
    if layout_spec is None or not layout_spec.shard_axes or not hsdp_group.records:
        return {}

    current_shapes = [tuple(to_local_if_dtensor(record.param.data).shape) for record in hsdp_group.records]
    stage_metas_per_param: List[List[ParamShardStageMeta]] = [[] for _ in hsdp_group.records]
    local_device = to_local_if_dtensor(hsdp_group.records[0].param.data).device

    for (_, tensor_dim), shard_pg in zip(layout_spec.shard_axes, shard_pgs):
        shard_size = dist.get_world_size(shard_pg) if shard_pg is not None else 1
        gathered_shapes = _allgather_shapes_for_group(current_shapes, shard_pg, shard_size, local_device)
        cur_rank = dist.get_rank(shard_pg) if shard_pg is not None and shard_size > 1 else 0
        next_shapes: List[Tuple[int, ...]] = []

        for record_idx, _ in enumerate(hsdp_group.records):
            rank_shapes = tuple(
                tuple(gathered_shapes[rank_idx][record_idx])
                for rank_idx in range(shard_size)
            )
            tensor_dim_norm = tensor_dim % len(rank_shapes[0])
            split_sizes = tuple(shape[tensor_dim_norm] for shape in rank_shapes)
            pad_shape = tuple(max(shape[dim] for shape in rank_shapes) for dim in range(len(rank_shapes[0])))
            stage_metas_per_param[record_idx].append(
                ParamShardStageMeta(
                    tensor_dim=tensor_dim_norm,
                    shard_size=shard_size,
                    cur_rank_in_shard_group=cur_rank,
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
                    pad_shape=pad_shape,
                )
            )

            output_shape = list(rank_shapes[cur_rank])
            if shard_size > 1:
                output_shape[tensor_dim_norm] = sum(split_sizes)
            next_shapes.append(tuple(output_shape))

        current_shapes = next_shapes

    param_to_meta: Dict[torch.Tensor, ParamShardMeta] = {}
    updated_records = []
    for record, stage_metas in zip(hsdp_group.records, stage_metas_per_param):
        shard_meta = make_param_shard_meta(record.param, stage_metas)
        param_to_meta[record.param] = shard_meta
        updated_records.append(record.__class__(index=record.index, param=record.param, shard_meta=shard_meta))

    hsdp_group.records = updated_records
    return param_to_meta


def build_pad_ns_inputs(
        comm_params: List[torch.Tensor],
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
        param_to_ns_input: Dict[torch.Tensor, torch.Tensor],
        param_shard_metadata: Optional[Dict[torch.Tensor, ParamShardMeta]] = None,
) -> List[torch.Tensor]:
    """Build aligned local NS inputs for all communication params."""
    if not comm_params:
        return []

    ref_tensor = next(iter(param_to_ns_input.values()), None)
    default_dtype = ref_tensor.dtype if ref_tensor is not None else to_local_if_dtensor(comm_params[0].data).dtype
    local_inputs: List[torch.Tensor] = []

    for param in comm_params:
        ns_input = param_to_ns_input.get(param)
        if ns_input is not None:
            local_inputs.append(ns_input)
            continue

        shard_meta = None
        if param_shard_metadata is not None:
            shard_meta = param_shard_metadata.get(param)
        if shard_meta is None and hasattr(param, "shard_meta"):
            shard_meta = getattr(param, "shard_meta")
        if shard_meta is None:
            local_shape = tuple(to_local_if_dtensor(param.data).shape)
        else:
            local_shape = shard_meta.local_shape

        local_inputs.append(
            torch.zeros(
                local_shape,
                dtype=default_dtype,
                device=to_local_if_dtensor(param.data).device,
239
240
241
242
243
244
245
246
247
                device=to_local_if_dtensor(param.data).device,
            )
        )

    return local_inputs


def chunk_update_by_layout(
        global_update: torch.Tensor,
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
        layout_spec: ParamLayoutSpec,
        param_shard_meta: Optional[ParamShardMeta] = None,
) -> torch.Tensor:
    """Slice a full update back to the local shard using narrow."""
    if not hasattr(param, "device_mesh") or layout_spec is None or not layout_spec.shard_axes:
        return global_update

    device_mesh = param.device_mesh
    mesh_coordinates = device_mesh.get_coordinate()

    shard_axes = layout_spec.shard_axes
    local_update = global_update

    # ``stage_metas`` are built while all-gathering local shards back to the
    # global tensor shape, so slicing a global update back to the local shard
    # must reverse that order.
    for axis_idx, (mesh_dim, tensor_dim) in reversed(list(enumerate(shard_axes))):
        num_chunks = device_mesh.size(mesh_dim)

        if num_chunks <= 1:
            continue

        local_rank = mesh_coordinates[mesh_dim]
        if param_shard_meta is not None and axis_idx < len(param_shard_meta.stage_metas):
            split_sizes = param_shard_meta.stage_metas[axis_idx].split_sizes
        else:
            full_chunk_size = (local_update.size(tensor_dim) + num_chunks - 1) // num_chunks
            split_sizes = tuple(
                min(full_chunk_size, max(local_update.size(tensor_dim) - rank * full_chunk_size, 0))
                for rank in range(num_chunks)
            )
        local_update = local_update.narrow(tensor_dim, sum(split_sizes[:local_rank]), split_sizes[local_rank])

    if not local_update.is_contiguous():
        local_update = local_update.contiguous()

    return local_update


def _get_or_alloc_buffer(
        cache: Optional[Dict],
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
        dtype: torch.dtype,
        device: torch.device,
) -> torch.Tensor:
    """Return a cached buffer with at least `numel` elements."""
    if cache is not None and key in cache:
        buf = cache[key]
        if buf.numel() >= numel:
            return buf
        buf = torch.empty(numel, dtype=dtype, device=device)
        cache[key] = buf
        return buf

    buf = torch.empty(numel, dtype=dtype, device=device)
    if cache is not None:
        cache[key] = buf
    return buf


def _early_return_tensors(
        local_tensors: List[torch.Tensor],
311
312
313
314
315
316
317
318
319
320
321
        local_tensors: List[torch.Tensor],
        keep_indices: Optional[set],
) -> List[Optional[torch.Tensor]]:
    """Return tensors directly when no communication is needed."""
    if keep_indices is None:
        return list(local_tensors)
    return [t if i in keep_indices else None for i, t in enumerate(local_tensors)]


@dataclass(frozen=True)
class _GatherParamMeta:
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
        shard_size: int,
        stage_metas: Optional[List[Optional[ParamShardStageMeta]]] = None,
) -> Tuple[List[torch.Tensor], List[_GatherParamMeta], int]:
    """Move shard dim to dim0 and compute padding metadata."""
    gather_inputs: List[torch.Tensor] = []
    param_meta: List[_GatherParamMeta] = []
    total_padded_numel = 0

    for idx, gather_input in enumerate(current_tensors):
        normalized_dim = tensor_dim % gather_input.dim()
        if normalized_dim != 0 or not gather_input.is_contiguous():
            gather_input = gather_input.movedim(normalized_dim, 0).contiguous()
        stage_meta = stage_metas[idx] if stage_metas is not None else None
        if stage_meta is None:
            split_sizes = tuple(gather_input.shape[0] for _ in range(shard_size))
            padded_numel_raw = gather_input.numel()
        else:
            split_sizes = stage_meta.split_sizes
            pad_shape = list(stage_meta.pad_shape)
            if normalized_dim != 0:
                pad_shape[0], pad_shape[normalized_dim] = pad_shape[normalized_dim], pad_shape[0]
            padded_numel_raw = math.prod(pad_shape)
        param_meta.append(
            _GatherParamMeta(
                offset=total_padded_numel,
                actual_numel=gather_input.numel(),
                padded_numel=(
362
363
364
365
366
367
368
369
370
371
372
373
                rest_shape=tuple(gather_input.shape[1:]),
                split_sizes=split_sizes,
            )
        )
        gather_inputs.append(gather_input)
        total_padded_numel += param_meta[-1].padded_numel

    return gather_inputs, param_meta, total_padded_numel


def _pack_and_allgather(
        gather_inputs: List[torch.Tensor],
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
        shard_size: int,
        buffer_cache: Optional[Dict],
) -> torch.Tensor:
    """Pack local shards into one buffer, all-gather, return gathered view."""
    pack_buffer = _get_or_alloc_buffer(
        buffer_cache, ("fused_allgather", axis_idx, dtype, device), total_padded_numel,
        dtype, device,
    )[:total_padded_numel]

    pack_buffer.zero_()
    for gi, meta in zip(gather_inputs, param_meta):
        pack_buffer[meta.offset:meta.offset + meta.actual_numel].copy_(gi.view(-1))

    gathered_buffer = _get_or_alloc_buffer(
        buffer_cache,
        ("fused_allgather_out", axis_idx, dtype, device),
        total_padded_numel * shard_size,
        dtype, device,
396
397
398
399
400
401
402
403
404
405
        total_padded_numel * shard_size,
        dtype, device,
    )[:total_padded_numel * shard_size]

    dist.all_gather_into_tensor(gathered_buffer, pack_buffer, group=shard_pg)
    return gathered_buffer.view(shard_size, total_padded_numel)


def _unpack_gathered_results(
        gathered_view: torch.Tensor,
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
        is_last_axis: bool,
        keep_indices: Optional[set],
) -> List[Optional[torch.Tensor]]:
    """Slice gathered buffer back to per-parameter full tensors."""
    new_tensors: List[Optional[torch.Tensor]] = []
    for i in range(n_params):
        if is_last_axis and keep_indices is not None and i not in keep_indices:
            new_tensors.append(None)
            continue

        meta = param_meta[i]
        rest_shape = meta.rest_shape
        rest_numel = math.prod(rest_shape) if rest_shape else 1
        param_slice = gathered_view[:, meta.offset:meta.offset + meta.padded_numel]

        param_chunks = []
        for rank in range(shard_size):
            valid_numel = meta.split_sizes[rank] * rest_numel
            if valid_numel == 0:
                continue
            param_chunks.append(param_slice[rank, :valid_numel].contiguous().view(-1))

        if param_chunks:
            param_data = torch.cat(param_chunks, dim=0)
        else:
            param_data = gather_inputs[i].new_empty((0,))

        result = param_data.view(sum(meta.split_sizes), *rest_shape)
        tensor_dim_norm = tensor_dim % current_tensors[i].dim()
        if tensor_dim_norm == 0:
            new_tensors.append(result)
        else:
            new_tensors.append(result.movedim(0, tensor_dim_norm))

    return new_tensors


def fused_allgather_dtensor_params(
        local_tensors: List[torch.Tensor],
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
        buffer_cache: Optional[Dict] = None,
        keep_indices: Optional[set] = None,
) -> List[Optional[torch.Tensor]]:
    """Fuse many parameter shards into one all-gather per shard axis."""
    if not shard_pgs or not local_tensors:
        return _early_return_tensors(local_tensors, keep_indices)

    n_params = len(local_tensors)
    device = local_tensors[0].device
    dtype = local_tensors[0].dtype
    alignment_bytes = 512
    element_size = local_tensors[0].element_size()
    alignment_elements = max(1, alignment_bytes // element_size)

    active_axes = []
    for axis_idx, ((_, tensor_dim), shard_pg) in enumerate(zip(layout_spec.shard_axes, shard_pgs)):
        if shard_pg is None:
            continue
        shard_size = dist.get_world_size(shard_pg)
        if shard_size <= 1:
            continue
        active_axes.append((axis_idx, tensor_dim, shard_pg, shard_size))

    if not active_axes:
        return _early_return_tensors(local_tensors, keep_indices)

    current_tensors: List[torch.Tensor] = list(local_tensors)

    for active_pos, (axis_idx, tensor_dim, shard_pg, shard_size) in enumerate(active_axes):
        is_last_axis = active_pos == len(active_axes) - 1
        stage_metas: Optional[List[Optional[ParamShardStageMeta]]] = None
        if param_shard_metadata is not None:
            stage_metas = []
            for meta in param_shard_metadata:
                if meta is None or axis_idx >= len(meta.stage_metas):
                    stage_metas.append(None)
                else:
                    stage_metas.append(meta.stage_metas[axis_idx])

        gather_inputs, param_meta, total_padded_numel = _prepare_gather_inputs(
            current_tensors, tensor_dim, alignment_elements, shard_size, stage_metas,
        )

        gathered_view = _pack_and_allgather(
            gather_inputs, param_meta, total_padded_numel,
            axis_idx, dtype, device, shard_pg, shard_size, buffer_cache,
        )

        new_tensors = _unpack_gathered_results(
            gathered_view, gather_inputs, param_meta,
            current_tensors, tensor_dim, shard_size,
            n_params, is_last_axis, keep_indices,
        )
504
505
506
507
508
509
510
511
512
513
            current_tensors, tensor_dim, shard_size,
            n_params, is_last_axis, keep_indices,
        )

        if is_last_axis:
            return new_tensors

        current_tensors = new_tensors  # type: ignore[assignment]

    return current_tensors
hyper_parallel/core/optimizer/optimizer.py
422
423
424
425
426
427
428
429
430

        # Cache Hit Check
        if cache_key in self._split_sub_pg_cache:
            sub_pg_map = self._split_sub_pg_cache[cache_key]
            for sub_pg in sub_pg_map.values():
                if sub_pg is not None:
                    try:
                        dist.get_rank(group=sub_pg)
                        return sub_pg
hyper_parallel/core/optimizer/utils.py
24
25
26
27
28
29
30
31
32
33
def _get_rank() -> int:
    try:
        if torch.distributed.is_initialized():
            return torch.distributed.get_rank()
    except (ImportError, RuntimeError):
        pass
    return int(os.environ.get("RANK", os.environ.get("LOCAL_RANK", "0")))


def info_rank0(self, msg, *args, **kwargs) -> None:
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55


def warning_rank0(self, msg, *args, **kwargs) -> None:
    """Log a warning message only on rank zero."""
    if _get_rank() == 0:
        kwargs.setdefault("stacklevel", 2)
        self.warning(msg, *args, **kwargs)


def debug_rank0(self, msg, *args, **kwargs) -> None:
    """Log a debug message only on rank zero."""
    if _get_rank() == 0:
        kwargs.setdefault("stacklevel", 2)
        self.debug(msg, *args, **kwargs)


def get_device_count() -> int:
    """Return the active accelerator count, defaulting to 1."""
54
55
56
57
58
59
60
61
62
63
64
def get_device_count() -> int:
    """Return the active accelerator count, defaulting to 1."""
    npu = getattr(torch, "npu", None)
    if npu is not None and npu.is_available():
        return npu.device_count()
    if torch.cuda.is_available():
        return torch.cuda.device_count()
    return 1


def get_current_device() -> torch.device:
64
65
66
67
68
69
70
71
72
73
74
def get_current_device() -> torch.device:
    """Return the current accelerator device, or CPU when none is available."""
    npu = getattr(torch, "npu", None)
    if npu is not None and npu.is_available():
        return torch.device("npu", npu.current_device())
    if torch.cuda.is_available():
        return torch.device("cuda", torch.cuda.current_device())
    return torch.device("cpu")


def empty_accelerator_cache() -> None:
72
73
74
75
76
77
78
79
80
81
82
83


def empty_accelerator_cache() -> None:
    """Clear the active accelerator cache when supported."""
    npu = getattr(torch, "npu", None)
    if npu is not None and npu.is_available():
        npu.empty_cache()
    elif torch.cuda.is_available():
        torch.cuda.empty_cache()


_INSTALLED = False
85
86
87
88
89
90
91
92
93

def _install_logger_methods() -> None:
    global _INSTALLED
    if _INSTALLED:
        return
    logging.Logger.info_rank0 = info_rank0
    logging.Logger.warning_rank0 = warning_rank0
    logging.Logger.debug_rank0 = debug_rank0
    _INSTALLED = True
hyper_parallel/data/batching/attention_runtime.py
45
46
47
48
49
50
51
52
53

    if any(end <= start for start, end in zip(boundaries[:-1], boundaries[1:])):
        raise ValueError("cu_seq_lens must be strictly increasing")

    if boundaries[-1] != micro_batch_size * seq_length:
        raise ValueError(
            "cu_seq_lens must cover the physical batch length "
            f"({micro_batch_size * seq_length}), but got {boundaries[-1]}"
        )
67
68
69
70
71
72
73
74
    swa_mask = None
    if sliding_window is not None:
        positions = torch.arange(seq_length, dtype=torch.int64, device=device)
        # Broadcast query and key positions to identify tokens outside the sliding window.
        outside_window = positions.unsqueeze(1) - positions.unsqueeze(0) > sliding_window
        swa_mask = attention_mask & ~outside_window.unsqueeze(0).unsqueeze(0)

    return attention_mask, swa_mask
hyper_parallel/data/batching/build_collate_fn.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
        for field in ("input_ids", "labels"):
            values = [model_sample[field] for model_sample in model_samples]
            packed_batch[field] = torch.cat(values, dim=-1).unsqueeze(0)

        input_ids = packed_batch.get("input_ids")
        if input_ids is None or packed_batch.get("labels") is None:
            raise ValueError("packed batch requires input_ids and labels")

        packed_seq_len = input_ids.shape[-1]
        pad_len = (-packed_seq_len) % self.sequence_parallel_size
        if pad_len:
            input_padding = input_ids.new_zeros((1, pad_len))
            label_padding = packed_batch["labels"].new_full((1, pad_len), IGNORE_INDEX)
            packed_batch["input_ids"] = torch.cat((input_ids, input_padding), dim=-1)
            packed_batch["labels"] = torch.cat((packed_batch["labels"], label_padding), dim=-1)

        seq_lens = model_samples[0]["input_ids"].new_tensor(
            [model_sample["input_ids"].shape[-1] for model_sample in model_samples]
hyper_parallel/data/batching/build_dataloader.py
229
230
231
232
233
234
235
236
237
                data_sharding=getattr(dataloader_target, "data_sharding", False),
                seed=training_config.seed if training_config.seed is not None else default_seed,
            )

        dataloaders[split_index] = dataloader_target.build(
            dataset=dataset,
            collate_fn=collate_fn,
            batch_sampler=batch_sampler,
            batch_size=training_config.micro_batch_size,
247
248
249
250
251
252
253
254
255
        )
        batch_samplers[split_index] = batch_sampler

    logger.debug("Finished building train/valid/test DataLoaders")
    return tuple(dataloaders), tuple(batch_samplers)


class FixedBatchDataLoader(StatefulDataLoader):
    """Build fixed-sample batches while retaining Trainer iterator policy."""
387
388
389
390
391
392
393
394
395
396
    def __init__(self, source_dataset: Any) -> None:
        """Store a Dataset that emits source items with stable output indices."""
        self.source_dataset = source_dataset

    @staticmethod
    def put_source_item(source_item: Any, batcher: TextTokenBatcher) -> None:
        """Flatten one indexed source item and retain each sample index."""
        model_samples_item, output_index = source_item
        model_samples = _normalize_source_samples(model_samples_item)
        for sample_idx, model_sample in enumerate(model_samples):
395
396
397
398
399
400
401
402
403
        model_samples = _normalize_source_samples(model_samples_item)
        for sample_idx, model_sample in enumerate(model_samples):
            batcher.put_item(model_sample, (output_index, sample_idx))

    @staticmethod
    def get_buffer_state(batcher: TextTokenBatcher) -> list[Any]:
        """Return compact output-index entries for buffered ModelSamples."""
        if len(batcher.buffer) != len(batcher.buffer_output_indices):
            raise RuntimeError("Dynamic sample and output-index buffers are inconsistent")
433
434
435
436
437
438
439
440
441
442
        """Retain the source Dataset and an optional index replay adapter."""
        self.source_dataset = dataset
        self.replay_dataset = replay_dataset

    @staticmethod
    def put_source_item(source_item: Any, batcher: TextTokenBatcher) -> None:
        """Flatten one streaming source item into the runtime buffer."""
        model_samples = _normalize_source_samples(source_item)
        for model_sample in model_samples:
            batcher.put_item(model_sample)
440
441
442
443
444
445
446
447
448
        model_samples = _normalize_source_samples(source_item)
        for model_sample in model_samples:
            batcher.put_item(model_sample)

    @staticmethod
    def get_buffer_state(batcher: TextTokenBatcher) -> list[Any]:
        """Return full samples already pulled beyond the streaming cursor."""
        if len(batcher.buffer) != len(batcher.buffer_output_indices):
            raise RuntimeError("Dynamic sample and output-index buffers are inconsistent")
hyper_parallel/data/batching/get_batch.py
147
148
149
150
151
152
153
154
155
        parallel_batch = self.tp_broadcaster.broadcast(
            cp_local_batch,
            cu_seq_lens,
        )
        self._log_batch_flow(canonical_batch, parallel_batch)

        position_ids = self._build_local_position_ids(
            parallel_batch["input_ids"],
            parallel_batch["cu_seq_lens"],
168
169
170
171
172
173
174
175
176
        model_inputs, loss_inputs = self._split_model_and_loss_inputs(parallel_batch)

        return model_inputs, loss_inputs

    def _log_batch_flow(
            self,
            canonical_batch: Mapping[str, Any] | None,
            parallel_batch: Mapping[str, Any],
    ) -> None:
174
175
176
177
178
179
180
181
182
183
184
185
186
            canonical_batch: Mapping[str, Any] | None,
            parallel_batch: Mapping[str, Any],
    ) -> None:
        """Log the resolved source and parallel batch shapes once."""
        if not self._batch_flow_logged:
            source_shape = None if canonical_batch is None else tuple(canonical_batch["input_ids"].shape)
            local_shape = tuple(parallel_batch["input_ids"].shape)
            num_boundaries = 0 if parallel_batch["cu_seq_lens"] is None else parallel_batch["cu_seq_lens"].numel()
            logger.debug(
                "Parallel batch flow: source=%s, tp_rank=%d/%d, cp_rank=%d/%d, "
                "source_owner=%s, source_shape=%s, local_shape=%s, global_boundaries=%d",
                self.source_type,
                self.parallel_context.tp_rank,
192
193
194
195
196
197
198
199
200
                local_shape,
                num_boundaries,
                enabled=True,
            )
            self._batch_flow_logged = True

    def _read_source_batch(self, data_iterator: Any) -> Mapping[str, Any] | None:
        """Read one complete batch on TP rank zero of each CP coordinate."""
        if self.parallel_context.build_on_rank():
hyper_parallel/data/batching/sequence_boundaries.py
24
25
26
27
28
29
30
31
32
33

class OnlineBoundaryResolver:
    """Read global cumulative sequence boundaries emitted by Online packing."""

    @staticmethod
    def resolve(canonical_batch: Mapping[str, Any]) -> Any:
        """Read leading-zero ``cu_seq_lens`` from an Online batch.

        Args:
            canonical_batch: Normalized batch produced by the Online packing collator.
hyper_parallel/data/indexed/indexed_data_config.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222

    # 4. Prefer the numeric shard order encoded by the corpus filenames.
    # Unknown naming schemes remain usable in their original walk order.
    try:
        def numeric_shard_key(path: str) -> tuple[int, int]:
            """Extract numeric corpus and shard identifiers from a path."""
            path_parts = os.path.basename(path).split("_")
            return int(path_parts[2]), int(path_parts[-1])

        file_paths.sort(key=numeric_shard_key)
    except (IndexError, TypeError, ValueError):
        logger.warning("Cannot sort indexed files with the numeric filename rule; keeping walk order")

    # 5. Automatically discovered sources use equal blend weights.
hyper_parallel/data/indexed/indexed_dataset.py
181
182
183
184
185
186
187
188
189
        """Delegate GPT Dataset option normalization."""
        config = build_gpt_dataset_config(data_paths, self.data_config)
        return config

    @staticmethod
    def _select_dataset_type(config: GPTDatasetConfig) -> type:
        """Select GPT, Mock GPT, or MR GPT Dataset."""
        if config.mock:
            return MockGPTDataset
hyper_parallel/data/indexed/indexed_simple_blended_dataset.py
70
71
72
73
74
75
76
77
78
79

    def _build_interleaved_locations(self) -> list[tuple[int, int]]:
        """Alternate over non-empty Datasets until every sample is exposed."""
        max_size = max(len(dataset) for dataset in self.datasets)
        locations = []
        for sample_id in range(max_size):
            for dataset_id, dataset in enumerate(self.datasets):
                if sample_id < len(dataset):
                    locations.append((dataset_id, sample_id))
        return locations
hyper_parallel/data/parallel/batch_parallel.py
302
303
304
305
306
307
308
309
310
311
312
313
                local_cu_seq_lens = torch.empty((num_boundaries,), dtype=torch.int32, device=self.device)

        # Broadcast only model fields that cannot be regenerated locally.
        for field in ("input_ids", "labels"):
            tensor = parallel_batch.get(field)
            if tensor is None:
                raise ValueError(f"parallel batch requires {field!r}")
            dist.broadcast(tensor, group=tp_group, group_src=0)

        # Packed boundaries remain global across CP ranks and variable in size.
        if local_cu_seq_lens is not None:
            dist.broadcast(local_cu_seq_lens, group=tp_group, group_src=0)
hyper_parallel/data/text/build_data_transform.py
59
60
61
62
63
64
65
66
67
        """
        self.tokenizer = tokenizer
        self.chat_template = chat_template

    @staticmethod
    def __call__(sample: Any) -> Any:
        """Return the input sample without modification."""
        return sample

hyper_parallel/data/text/transform_dataset.py
15
16
17
18
19
20
21
22
"""Lazy plaintext or conversation transforms for Online LLM sources."""

from __future__ import annotations

from collections.abc import Callable, Iterable, Mapping, Sequence
from typing import Any, TypeAlias

from torch.utils.data import IterableDataset  # pylint: disable=forbidden-backend-import
256
257
258
259
260
261
262
263
264
        )
        logger.debug("Wrapped mapping Dataset with transform=%s", type(transform).__name__)
        return transformed_dataset

    if isinstance(dataset, Iterable):
        transformed_dataset = _LLMIterableTransformDataset(
            dataset,
            transform,
            skip_invalid_samples=skip_invalid_samples,
hyper_parallel/data/tools/offline_preparation.py
19
20
21
22
23
24
25
26
27
import argparse
import glob
import gzip
import json
import logging
import math
import multiprocessing
import os
import sys
45
46
47
48
49
50
51
52
53

# Store generated samples in the indexed ``.bin/.idx`` format.
from hyper_parallel.data.tools import io as indexed_dataset

logger = logging.getLogger(__name__)


class CustomLanguageVars(PunktLanguageVars):
    """Preserve newline runs when Punkt detects sentence boundaries."""
710
711
712
713
714
715
716
717
718
719
720
    for name in names:
        process = multiprocessing.Process(target=_split_sentences_worker, args=(args, workers, name, queue))
        process.start()
        processes.append(process)
    split_results = _wait_for_processes(processes, queue)
    if any(result is not None for result in split_results):
        raise RuntimeError("Sentence-splitting workers returned unexpected results")


def _encode_partitions(
    args: argparse.Namespace,
746
747
748
749
750
751
752
753
754

    performance = {}
    input_files = _resolve_input_files(args.dataset_name_or_path)
    for workers in worker_candidates:
        logger.info("Processing data with %s workers", workers)
        workers_per_partition = workers // args.partitions

        if args.split_sentences:
            nltk.download("punkt", quiet=True, download_dir=os.environ.get("NLTK_DATA"))
hyper_parallel/data/vlm/get_batch.py
13
14
15
16
17
18
19
20
# limitations under the License.
# ============================================================================
"""Temporary self-contained VLM batch preparation."""

__all__ = ["VLMBatchProcessor", "VLMGetBatch", "build_vlm_get_batch"]

from collections.abc import Mapping
from typing import Any
hyper_parallel/trainer/runtime/data_iterator.py
18
19
20
21
22
23
24
25
``auto_models/trainer/base.py`` in stage 7 (05 ยง15.11 step 3); class names,
signatures and checkpointing semantics are unchanged.
"""

__all__ = [
    "BackgroundPrefetcher",
    "HyperIter",
]