Skip to content

Expand DSpark target support and training scalability#710

Merged
jiapingW merged 5 commits into
sgl-project:mainfrom
Boreas618:expand-dspark-target-support
Jul 21, 2026
Merged

Expand DSpark target support and training scalability#710
jiapingW merged 5 commits into
sgl-project:mainfrom
Boreas618:expand-dspark-target-support

Conversation

@Boreas618

Copy link
Copy Markdown
Contributor

Motivation

The DSpark training path still assumed a conventionally registered Hugging Face target config, an unpadded vocabulary, fully materialized objective logits, and capture servers with radix caching disabled. Those assumptions prevent released targets such as GLM-5.2 and Inkling from using the unified disaggregated trainer reliably and make large-vocabulary objectives unnecessarily memory hungry.

This draft broadens target compatibility, bounds objective memory, and hardens distributed training and capture behavior while keeping the existing algorithm/provider boundaries.

Modifications

  • Add checked-in DSpark draft and disaggregated-training recipes for GLM-5.2, Inkling, and Qwen3-8B, and make the GQA/MHA contract explicit.
  • Support raw target config.json fallback, padded vocabularies, optional tokenizer padding IDs, and muP LM-head folding for released targets with nonstandard Transformers metadata.
  • Add GLM-5.2 and Inkling parsing/loss-mask metadata while continuing to use each target tokenizer's own chat template.
  • Add checkpointed, chunked objective reductions for DFlash, Domino, and DSpark; fix DSpark anchor selection, apply Markov sampling consistently, and aggregate ratio telemetry from global numerators and denominators.
  • Add optional CPU placement for BF16 optimizer master weights and Adam state, plus world-consistent checkpoint resume validation.
  • Keep SGLang radix caching available by assigning every capture attempt a unique cache namespace, extend managed-server tuning fields, and update the checked-in capture patch for the supported public layouts.
  • Add an algorithm-agnostic specforge benchmark sglang runner and update benchmark/configuration documentation.

Related Issues

None.

Accuracy Test

  • Changed-area suite: 243 passed, 2 skipped, plus 387 unittest subtests.
  • Training/runtime regression suite after installing the declared yunchang dependency: 46 passed; one existing EAGLE3 exact-float assertion remains different by 2.38e-7 (2.1442596912384033 versus 2.1442599296569824) on the local CUDA environment, which also reports nondeterministic CUDA attention.
  • Unit coverage includes chunked-versus-full losses, metrics, and gradients; DSpark anchor and Markov behavior; CPU-offloaded optimizer parity; distributed ratio reduction; target config/vocab loading; checkpoint resume consistency; and radix-cache-isolated capture requests.

End-to-end model accuracy and live SGLang capture numerics still require the target GPU environment and are intentionally left pending while this PR is a draft.

Benchmark & Profiling

No target-cluster throughput numbers are included yet. The new objective chunking bounds peak vocabulary-logit memory at the cost of activation recomputation, and this PR adds a general existing-server SGLang benchmark command for follow-up target-only/speculative comparisons.

Validation

  • pre-commit run --files <changed files>
  • git diff --check
  • Secret/private-key scan over the publish set

The full unittest discovery was not used as the final signal because an existing regeneration integration test began downloading a 2.08 GB model and launching an SGLang server; the run was stopped and replaced with the focused suites above.

Checklist

  • Format code with pre-commit.
  • Add unit tests for the changed behavior.
  • Update documentation, configuration references, and examples.
  • Add target-cluster throughput and end-to-end accuracy results before marking ready for review.
  • For reviewers: remove merge-only co-authors when applicable.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for GLM-5.2 and Inkling models, including custom templates and tokenizers, adds memory-bounded training objectives via chunked reductions, supports optimizer CPU offloading, and improves the SGLang inference benchmark suite. Additionally, it updates capture request logic to isolate cache attempts using a unique extra_key. The review comments identify critical issues that must be addressed: missing imports (os, json, Optional, hashlib) in target_utils.py and assembly.py, a potential type error when eos_token_id is a list or tuple, and a dimension alignment bug in the chunked reduction utility when handling negative dimensions.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 7 to 11
import torch
import torch.nn as nn
from huggingface_hub import snapshot_download
from huggingface_hub import hf_hub_download, snapshot_download
from safetensors import safe_open
from transformers import AutoConfig

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The standard library modules os and json are used in load_target_config but are not imported in this file. Additionally, Optional is used in the type hints but is not imported. This will cause a NameError when the fallback path is triggered.

import json
import os
from typing import Optional

import torch
import torch.nn as nn
from huggingface_hub import hf_hub_download, snapshot_download
from safetensors import safe_open
from transformers import AutoConfig

Comment on lines +147 to +156
elif tokenizer.pad_token_id is None:
fallback_id = tokenizer.eos_token_id
if fallback_id is None:
fallback_id = tokenizer.unk_token_id
if fallback_id is None:
raise ValueError(
"target tokenizer has no pad, EOS, or unknown token ID; set "
"model.tokenizer_pad_token_id explicitly"
)
tokenizer.pad_token_id = fallback_id

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The tokenizer.eos_token_id can be a list or tuple of integers (as is the case for GLM-5.2). If it is, fallback_id will be a list/tuple, and setting tokenizer.pad_token_id = fallback_id will assign a list/tuple to pad_token_id, which is invalid and will cause downstream errors in PyTorch/Transformers (which expect an integer). We should extract the first element if eos_token_id is a list or tuple.

Suggested change
elif tokenizer.pad_token_id is None:
fallback_id = tokenizer.eos_token_id
if fallback_id is None:
fallback_id = tokenizer.unk_token_id
if fallback_id is None:
raise ValueError(
"target tokenizer has no pad, EOS, or unknown token ID; set "
"model.tokenizer_pad_token_id explicitly"
)
tokenizer.pad_token_id = fallback_id
elif tokenizer.pad_token_id is None:
fallback_id = tokenizer.eos_token_id
if isinstance(fallback_id, (list, tuple)):
fallback_id = fallback_id[0] if fallback_id else None
if fallback_id is None:
fallback_id = tokenizer.unk_token_id
if fallback_id is None:
raise ValueError(
"target tokenizer has no pad, EOS, or unknown token ID; set "
"model.tokenizer_pad_token_id explicitly"
)
tokenizer.pad_token_id = fallback_id

Comment on lines +318 to +325
source_path = path or cfg.data.prompts_path or cfg.data.train_data_path
content_hash = None
if source_path and os.path.isfile(source_path):
source_hasher = hashlib.sha256()
with open(source_path, "rb") as source_file:
for chunk in iter(lambda: source_file.read(8 * 1024 * 1024), b""):
source_hasher.update(chunk)
content_hash = source_hasher.hexdigest()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The standard library modules os and hashlib are used in _prompt_cache_key but are not imported in this file. This will cause a NameError when generating the prompt cache key. We should import them inside the function to ensure they are available.

    import hashlib
    import os

    source_path = path or cfg.data.prompts_path or cfg.data.train_data_path
    content_hash = None
    if source_path and os.path.isfile(source_path):
        source_hasher = hashlib.sha256()
        with open(source_path, "rb") as source_file:
            for chunk in iter(lambda: source_file.read(8 * 1024 * 1024), b""):
                source_hasher.update(chunk)
        content_hash = source_hasher.hexdigest()

Comment on lines +37 to +70
first = tensors[0]
normalized_dim = dim if dim >= 0 else first.ndim + dim
if normalized_dim < 0 or normalized_dim >= first.ndim:
raise ValueError(f"dim {dim} is invalid for a {first.ndim}D tensor")
length = first.shape[normalized_dim]
if length == 0:
raise ValueError("chunked reduction received an empty dimension")

for tensor in tensors[1:]:
tensor_dim = dim if dim >= 0 else tensor.ndim + dim
if tensor_dim < 0 or tensor_dim >= tensor.ndim:
raise ValueError(f"dim {dim} is invalid for a {tensor.ndim}D tensor")
if tensor.shape[tensor_dim] != length:
raise ValueError(
"chunked reduction inputs must be aligned: "
f"expected dimension length {length}, got {tensor.shape[tensor_dim]}"
)

effective_chunk_size = chunk_size or length
totals: Optional[ChunkTerms] = None
for start in range(0, length, effective_chunk_size):
width = min(effective_chunk_size, length - start)
chunk_args = tuple(
(
tensor.narrow(
dim if dim >= 0 else tensor.ndim + dim,
start,
width,
)
if tensor is not None
else None
)
for tensor in aligned_tensors
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When dim is negative, resolving it per-tensor using tensor.ndim + dim can lead to slicing different logical dimensions if the input tensors have different ndim (for example, a 4D hidden state tensor and a 3D target IDs tensor). Since the tensors are aligned, we should resolve the negative dim to an absolute dimension index using the first tensor, and use that same absolute dimension for all tensors.

    first = tensors[0]
    normalized_dim = dim if dim >= 0 else first.ndim + dim
    if normalized_dim < 0 or normalized_dim >= first.ndim:
        raise ValueError(f"dim {dim} is invalid for a {first.ndim}D tensor")
    length = first.shape[normalized_dim]
    if length == 0:
        raise ValueError("chunked reduction received an empty dimension")

    for tensor in tensors[1:]:
        if normalized_dim >= tensor.ndim:
            raise ValueError(f"dim {dim} (resolved to {normalized_dim}) is invalid for a {tensor.ndim}D tensor")
        if tensor.shape[normalized_dim] != length:
            raise ValueError(
                "chunked reduction inputs must be aligned: "
                f"expected dimension length {length}, got {tensor.shape[normalized_dim]}"
            )

    effective_chunk_size = chunk_size or length
    totals: Optional[ChunkTerms] = None
    for start in range(0, length, effective_chunk_size):
        width = min(effective_chunk_size, length - start)
        chunk_args = tuple(
            (
                tensor.narrow(
                    normalized_dim,
                    start,
                    width,
                )
                if tensor is not None
                else None
            )
            for tensor in aligned_tensors
        )

SGLang is the only supported benchmark target, so the `sglang`
sub-subcommand added no discrimination. Move its arguments and help
directly onto `specforge benchmark`; update the docs and dispatch test.

Also replace the hand-maintained flag lists in `_managed_local_services`
with a `_sglang_argv` helper that maps every `sglang_*` ModelConfig field
to its `--foo-bar` flag, taking server-level and fallback values via
overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@jiapingW
jiapingW marked this pull request as ready for review July 21, 2026 11:43
@jiapingW
jiapingW force-pushed the expand-dspark-target-support branch from 5ea8c05 to 213aae2 Compare July 21, 2026 14:18
@jiapingW
jiapingW merged commit 47140a9 into sgl-project:main Jul 21, 2026
2 of 3 checks passed
maocheng23 pushed a commit that referenced this pull request Jul 22, 2026
Expand DSpark target support and training scalability

Co-authored-by: Doğaç Eldenk <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants