From 75eb35cda4cf17d98d35c0a9b1c95a5f850277ff Mon Sep 17 00:00:00 2001 From: xy200303 <3483421977@qq.com> Date: Thu, 30 Jul 2026 20:05:13 +0800 Subject: [PATCH 1/5] fix(utils): fix logger level fallback, profiler schedule validation, and USP crash - _get_logger_level caught ValueError, but getattr() raises AttributeError for an unknown level name, and the handler never assigned the fallback; an invalid ANGELSPEC_LOG_LEVEL crashed at import time. Catch AttributeError and fall back to WARNING. - split_usp_batch assigned a local variable named usp_chunk_size, shadowing the module-level function and making every call raise UnboundLocalError. Rename the local to chunk_len. - _create_torch_profiler produced active=0 with the default profile_step_start/end, tripping torch's schedule assertion. Validate the step range up front with a clear error. --- angelspec/utils/logging.py | 3 ++- angelspec/utils/profiling.py | 6 ++++++ angelspec/utils/usp.py | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/angelspec/utils/logging.py b/angelspec/utils/logging.py index 04a07be..0a9030b 100644 --- a/angelspec/utils/logging.py +++ b/angelspec/utils/logging.py @@ -34,8 +34,9 @@ def _get_logger_level(): level_str = os.getenv("ANGELSPEC_LOG_LEVEL", "INFO").upper() try: log_level = getattr(logging, level_str) - except ValueError: + except AttributeError: logging.warning("Invalid log level: %s, defaulting to WARNING", level_str) + log_level = logging.WARNING return log_level diff --git a/angelspec/utils/profiling.py b/angelspec/utils/profiling.py index 8384d12..c94551d 100644 --- a/angelspec/utils/profiling.py +++ b/angelspec/utils/profiling.py @@ -73,6 +73,12 @@ def _profile_simple_loop(iterator, args, name): def _create_torch_profiler(args, name): + if args.profile_step_end <= args.profile_step_start: + raise ValueError( + f"profile_step_end ({args.profile_step_end}) must be greater than " + f"profile_step_start ({args.profile_step_start}) when use_pytorch_profiler " + "is enabled." + ) return torch.profiler.profile( schedule=torch.profiler.schedule( wait=max(args.profile_step_start - 1, 0), diff --git a/angelspec/utils/usp.py b/angelspec/utils/usp.py index b262f5b..4aeee0c 100644 --- a/angelspec/utils/usp.py +++ b/angelspec/utils/usp.py @@ -125,8 +125,8 @@ def _slice_and_pad(tensor: torch.Tensor, axis: int, pad_value: int = 0): ) attention_mask[:, :valid_len] = 1 - usp_chunk_size = max(local_len - ttt_length, 0) - ring_chunk = usp_chunk_size * sp_ulysses_size + chunk_len = max(local_len - ttt_length, 0) + ring_chunk = chunk_len * sp_ulysses_size ring_start = ring_rank * ring_chunk position_ids = torch.arange( ring_start, ring_start + ring_chunk, device=input_ids.device, dtype=torch.long From 6afed5fbb68df98bdad855493d66887170409e13 Mon Sep 17 00:00:00 2001 From: xy200303 <3483421977@qq.com> Date: Thu, 30 Jul 2026 20:05:25 +0800 Subject: [PATCH 2/5] fix(data): fix deepseek-r1-distill parse crash and dataset loading issues - GeneralParser.parse called re.escape() on end_of_turn_token, which is None for the registered deepseek-r1-distill template, raising TypeError during dataset tokenization. Guard with 'or ""'; the existing |$ alternative already covers a missing end token. - _ensure_pad_token used a truthiness check that clobbered a legitimate pad_token_id of 0; check for None instead. - The Hub dataset path dropped every column outside a small keep-list, silently discarding per-sample 'tools' and 'reasoning_effort' fields that dataset.py threads into the chat template. Keep them. - .csv/.tsv/.txt are advertised as supported local extensions but were fed to the JSON loader; route them to the csv/text loaders instead. - The tokenized-dataset cache key omitted prompt_key, so re-running with a different prompt_key silently reused a stale cache. --- angelspec/data/dataset.py | 2 +- angelspec/data/parse.py | 4 ++-- angelspec/data/utils.py | 16 ++++++++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/angelspec/data/dataset.py b/angelspec/data/dataset.py index 4861d8c..08da91b 100644 --- a/angelspec/data/dataset.py +++ b/angelspec/data/dataset.py @@ -181,7 +181,7 @@ def load_conversation_dataset(args): drop_overlength_flag = getattr(args, "drop_overlength", False) cache_params = ( f"{dataset_name}-{args.train_data_path}{file_stat}-{args.target_model_path}" - f"-{max_length}-{chat_template_name}-ltlo={last_turn_loss_only_flag}" + f"-{max_length}-{chat_template_name}-{prompt_key}-ltlo={last_turn_loss_only_flag}" f"-defer={defer_tokenization}-decode={train_with_decode}" f"-mlt={min_loss_tokens_val}-drop={drop_overlength_flag}" ) diff --git a/angelspec/data/parse.py b/angelspec/data/parse.py index 263621f..24b63ae 100644 --- a/angelspec/data/parse.py +++ b/angelspec/data/parse.py @@ -101,7 +101,7 @@ def _prepare_text(self, conversation: "Conversation", preformatted: bool, **kwar return self.format(conversation, **kwargs) def _ensure_pad_token(self): - if not self.tokenizer.pad_token_id: + if self.tokenizer.pad_token_id is None: self.tokenizer.pad_token_id = self.tokenizer.unk_token_id def _tokenize_with_loss_mask( @@ -254,7 +254,7 @@ def parse( assistant_pattern = ( re.escape(self.assistant_message_separator) + r"([\s\S]*?(?:" - + re.escape(self.chat_template.end_of_turn_token) + + re.escape(self.chat_template.end_of_turn_token or "") + "|$))" ) return self._tokenize_with_loss_mask( diff --git a/angelspec/data/utils.py b/angelspec/data/utils.py index f709a10..b46a47c 100644 --- a/angelspec/data/utils.py +++ b/angelspec/data/utils.py @@ -832,6 +832,16 @@ def load_hf_dataset(data_path: str): load_local_json, gen_kwargs={"data_path": data_path} ) ext = os.path.splitext(data_path)[1].lower() + if ext in (".csv", ".tsv"): + return load_dataset( + "csv", + data_files=data_path, + sep="\t" if ext == ".tsv" else ",", + split="train", + streaming=True, + ) + if ext == ".txt": + return load_dataset("text", data_files=data_path, split="train", streaming=True) fmt = {".parquet": "parquet", ".arrow": "arrow"}.get(ext, "json") return load_dataset(fmt, data_files=data_path, split="train", streaming=True) @@ -863,8 +873,10 @@ def load_hf_dataset(data_path: str): raise FileNotFoundError(f"Local dataset path not found: {data_path}") # hub path — try native load_dataset first (handles Arrow, Parquet, etc.), - # fall back to manual JSON download for repos with mixed-type columns - _KEEP_COLUMNS = frozenset({"id", "conversations", "text", "messages"}) + # fall back to manual JSON download for repos with mixed-type columns. + # Keep the per-sample top-level fields the chat template consumes + # (dataset.py threads them into parser.format). + _KEEP_COLUMNS = frozenset({"id", "conversations", "text", "messages", "tools", "reasoning_effort"}) try: ds = load_dataset(data_path, split="train", streaming=True) drop_cols = [c for c in (ds.column_names or []) if c not in _KEEP_COLUMNS] From c05ea2041c56f665343b0776f9f75fd8dc3c5362 Mon Sep 17 00:00:00 2001 From: xy200303 <3483421977@qq.com> Date: Thu, 30 Jul 2026 20:05:43 +0800 Subject: [PATCH 3/5] fix(models): pass rope_theta in linear/dynamic/yarn RoPE branches The linear and dynamic scaling branches in Llama3 Eagle3 _init_rope, and the linear, dynamic, and yarn branches in DeepSeek Eagle3 _init_rope, omitted base=, silently defaulting to 10000 while every other branch passes the configured rope_theta. A draft config combining these scaling types with rope_theta != 10000 (e.g. Llama-3's 500000) would train with the wrong rotation frequencies. --- angelspec/models/draft/deepseek_eagle.py | 3 +++ angelspec/models/draft/llama3_eagle.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/angelspec/models/draft/deepseek_eagle.py b/angelspec/models/draft/deepseek_eagle.py index 7c7c219..6785e99 100644 --- a/angelspec/models/draft/deepseek_eagle.py +++ b/angelspec/models/draft/deepseek_eagle.py @@ -181,12 +181,14 @@ def _init_rope(self): self.rotary_emb = LlamaLinearScalingRotaryEmbedding( rope_dim, max_position_embeddings=self.max_position_embeddings, + base=rope_theta, scaling_factor=scaling_factor, ) elif scaling_type == "dynamic": self.rotary_emb = LlamaDynamicNTKScalingRotaryEmbedding( rope_dim, max_position_embeddings=self.max_position_embeddings, + base=rope_theta, scaling_factor=scaling_factor, ) elif scaling_type == "llama3": @@ -203,6 +205,7 @@ def _init_rope(self): self.rotary_emb = LlamaYarnRotaryEmbedding( rope_dim, max_position_embeddings=self.max_position_embeddings, + base=rope_theta, original_max_position_embeddings=rget("original_max_position_embeddings"), scaling_factor=scaling_factor, beta_fast=rget("beta_fast"), diff --git a/angelspec/models/draft/llama3_eagle.py b/angelspec/models/draft/llama3_eagle.py index 751f8b8..8cdabff 100644 --- a/angelspec/models/draft/llama3_eagle.py +++ b/angelspec/models/draft/llama3_eagle.py @@ -1176,6 +1176,7 @@ def rope_get(key, default=None): self.rotary_emb = LlamaLinearScalingRotaryEmbedding( self.head_dim, max_position_embeddings=self.max_position_embeddings, + base=getattr(self.config, "rope_theta", 10000), scaling_factor=scaling_factor, ) elif scaling_type == "dynamic": @@ -1186,6 +1187,7 @@ def rope_get(key, default=None): self.rotary_emb = LlamaDynamicNTKScalingRotaryEmbedding( self.head_dim, max_position_embeddings=self.max_position_embeddings, + base=getattr(self.config, "rope_theta", 10000), scaling_factor=scaling_factor, ) elif scaling_type == "llama3": From 14e3dd965d423865a09c2cd9db8315807026d766 Mon Sep 17 00:00:00 2001 From: xy200303 <3483421977@qq.com> Date: Thu, 30 Jul 2026 20:05:43 +0800 Subject: [PATCH 4/5] fix(transfer): use last_hidden_states dtype key on Mooncake GET The last_hidden_states tensor spec looked up dtypes['hidden_states'] instead of dtypes['last_hidden_states']. If the two are ever stored with different dtypes the receiver computes the wrong byte size (the host-buffer path raises, the GPU-direct path silently misinterprets). --- angelspec/transfer/mooncake/eagle_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/angelspec/transfer/mooncake/eagle_store.py b/angelspec/transfer/mooncake/eagle_store.py index 40aaa6b..6ef9b5c 100644 --- a/angelspec/transfer/mooncake/eagle_store.py +++ b/angelspec/transfer/mooncake/eagle_store.py @@ -377,7 +377,7 @@ def get( ( "last_hidden_states", shapes["last_hidden_states"], - dtypes.get("hidden_states", HIDDEN_STATES_STORAGE_DTYPE), + dtypes.get("last_hidden_states", HIDDEN_STATES_STORAGE_DTYPE), ) ) From 61e341157db741700d3d0818c5f1825cc02a0cb4 Mon Sep 17 00:00:00 2001 From: xy200303 <3483421977@qq.com> Date: Thu, 30 Jul 2026 20:05:43 +0800 Subject: [PATCH 5/5] fix(examples,tools): fix num_nodes override key and unbound resp - Both Hy3 example scripts passed training.num_nodes, which is not a schema field (the field is training_num_nodes), so the CLI override crashed at startup with ConfigKeyError. - generate_data.py read resp after the message loop even though resp is only assigned for user turns; a conversation with no user message crashed with UnboundLocalError. Return an error record instead. --- examples/hy3-dfly/run.sh | 2 +- examples/hy3-mtp/run.sh | 2 +- tools/generate_data.py | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/hy3-dfly/run.sh b/examples/hy3-dfly/run.sh index 3ec6b1c..22ed244 100755 --- a/examples/hy3-dfly/run.sh +++ b/examples/hy3-dfly/run.sh @@ -49,7 +49,7 @@ echo "==============================================" python3 -m angelspec.train_entry \ --config "$CONFIG_FILE" \ training.training_num_gpus_per_node=4 \ - training.num_nodes="$NUM_NODES" \ + training.training_num_nodes="$NUM_NODES" \ inference.inference_num_gpus="$INFERENCE_GPUS" \ inference.inference_num_gpus_per_engine=4 \ inference.inference_num_gpus_per_node="$GPUS_PER_NODE" \ diff --git a/examples/hy3-mtp/run.sh b/examples/hy3-mtp/run.sh index 2fbcd38..887d0e4 100755 --- a/examples/hy3-mtp/run.sh +++ b/examples/hy3-mtp/run.sh @@ -49,7 +49,7 @@ echo "==============================================" python3 -m angelspec.train_entry \ --config "$CONFIG_FILE" \ training.training_num_gpus_per_node=4 \ - training.num_nodes="$NUM_NODES" \ + training.training_num_nodes="$NUM_NODES" \ training.attention_backend=usp \ training.sp_ulysses_size=4 \ inference.inference_num_gpus="$INFERENCE_GPUS" \ diff --git a/tools/generate_data.py b/tools/generate_data.py index 267f7dd..d732142 100644 --- a/tools/generate_data.py +++ b/tools/generate_data.py @@ -165,6 +165,7 @@ def call_sglang( messages = data["conversations"] regenerated_messages = [] total_output_tokens = 0 + resp = None if messages[0]["role"] == "assistant": data["status"] = "error" @@ -199,6 +200,11 @@ def call_sglang( data["error"] = f"Invalid message role: {message['role']}" return data + if resp is None: + data["status"] = "error" + data["error"] = "No user message in conversation" + return data + data["output_tokens"] = total_output_tokens data["input_tokens"] = resp.usage.prompt_tokens data["context_length"] = data["input_tokens"] + total_output_tokens