Skip to content

fix: stabilize D2F LoRA decoding under preempt-rebuild scenarios#40

Merged
drewjin merged 5 commits into
SJTU-DENG-Lab:mainfrom
Osc-7:fix-llada-lora-bug
Jun 26, 2026
Merged

fix: stabilize D2F LoRA decoding under preempt-rebuild scenarios#40
drewjin merged 5 commits into
SJTU-DENG-Lab:mainfrom
Osc-7:fix-llada-lora-bug

Conversation

@Osc-7

@Osc-7 Osc-7 commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR fixes multiple instability issues in D2F LoRA decoding, especially under preempt + rebuild scenarios.

The main problems were caused by inconsistent indexing and request state handling during prefill and decode phases, which could lead to incorrect logits alignment and unstable generation.


Key Changes

  • LoRA

    • Align target module matching with packed_modules_mapping
  • D2F / Decode flow

    • Stabilize LLaDA decode after preemption
    • Introduce rebuild-aware request flow
  • Sampler

    • Fix misalignment of prefill indices affecting token selection
  • Request / KV cache

    • Bound prefill-to-cache indices within visible logits window
    • Prevent invalid cache access during rebuild

Results

Model Before After
LLaDA (D2F LoRA) 9.0 79.0

Notes

  • No interface changes
  • Fully backward compatible
  • Mainly affects D2F + LoRA pipelines under preemption

Checklist

  • Tested on GSM8K benchmarks
  • No regression observed
  • Rebased onto latest upstream/main

Summary by CodeRabbit

  • New Features

    • Added support for configuring a custom mask token ID from the server CLI or settings.
    • Improved LoRA loading so target module selection works better with packed module layouts.
  • Bug Fixes

    • Prevented invalid sampling errors during prefill, improving stability on multi-block requests.
    • Fixed cached-logit lookup edge cases to avoid out-of-bounds behavior.
    • Made request resuming and truncation handling more reliable during generation.

@coderabbitai

coderabbitai Bot commented Apr 21, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95882a22-7886-4aaf-bac5-3125a01082e1

📥 Commits

Reviewing files that changed from the base of the PR and between 966b75f and 92cc4e6.

📒 Files selected for processing (6)
  • diffulex/engine/engine.py
  • diffulex/engine/request.py
  • diffulex/sampler/base/no_shift.py
  • diffulex/sampler/base/shift.py
  • diffulex/server/args.py
  • diffulex/utils/loader.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • diffulex/server/args.py
  • diffulex/utils/loader.py
  • diffulex/sampler/base/shift.py

📝 Walkthrough

Walkthrough

The PR adds optional mask-token-id plumbing from server args into engine startup, changes request and sampler logic to handle resumed prefill and terminal-context blocks, and updates LoRA loading to resolve target modules through packed-module metadata.

Changes

Mask token override and resumed prefill handling

Layer / File(s) Summary
CLI arg and engine override
diffulex/server/args.py, diffulex/engine/engine.py
Adds optional mask_token_id parsing and passes it into engine startup, where tokenizer-derived values can still override the dataclass default when the CLI did not set it explicitly.
Resume boundary and terminal context state
diffulex/engine/request.py
Initializes _resume_prefill_until and _terminal_context_block_id, rewrites cached-prefix visibility and last-block completion checks, and adds terminal-context accessors.
Request flow around prefill resumption
diffulex/engine/request.py
Updates preempt, lazy activation, dummy-block handling, prefix postprocessing, and truncation handling to honor the resume boundary and terminal-context block.
Prefill sampler guards
diffulex/sampler/base/no_shift.py, diffulex/sampler/base/shift.py
Skips prefill sampling when the resumed window is empty or out of range, and avoids indexing logits with empty or invalid local-id selections.

Packed-module LoRA mapping

Layer / File(s) Summary
Packed target-module lookup
diffulex/utils/loader.py
Extends enable_lora_for_model to accept packed_modules_mapping, normalizes string target-module config, and matches LoRA targets through the mapped checkpoint leaf names while load_model passes the model mapping through.

Sequence Diagram(s)

sequenceDiagram
  participant parse_args
  participant ServerArgs
  participant DiffulexEngine
  participant maybe_override_mask_token_id
  participant tokenizer

  parse_args->>ServerArgs: populate mask_token_id from ns.mask_token_id
  DiffulexEngine->>maybe_override_mask_token_id: call with mask_token_id_explicit
  maybe_override_mask_token_id->>tokenizer: read tokenizer.mask_token_id
  maybe_override_mask_token_id->>DiffulexEngine: update config.mask_token_id when still default
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

I twitch my nose at tokens bright,
and hop through caches left and right.
When prefill pauses, I take a seat,
then bound ahead on careful feet. 🐰
LoRA paths and mask ids gleam,
a tidy little coder-dream.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main fix: stabilizing D2F LoRA decoding during preempt-rebuild scenarios.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Osc-7
Osc-7 marked this pull request as ready for review April 22, 2026 02:32

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
diffulex/strategy_template/multi_block/engine/request.py (1)

11-25: ⚠️ Potential issue | 🟡 Minor

Initialize _resume_prefill_until in _restore_req_runtime_state to prevent AttributeError on unpickling.

_resume_prefill_until is initialized only in init_multi_block (line 31) but not restored in _restore_req_runtime_state. When a multi-block request is unpickled via __setstate__, if the attribute is missing from the pickled state (edge case: old pickled requests, failed migrations, etc.), subsequent accesses at lines 232, 415, 507, 550 will raise AttributeError. The sampler already uses defensive getattr(req, "_resume_prefill_until", 0) at line 99 of no_shift.py, establishing the pattern.

Initialize it in _restore_req_runtime_state to match the mixin pattern shown in the test suite:

Suggested fix
     def _restore_req_runtime_state(self):
         super()._restore_req_runtime_state()

         if not self.is_multi_block:
             return

+        self._resume_prefill_until = getattr(self, "_resume_prefill_until", 0)
+
         dllm_blocks = self.dllm_blocks
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@diffulex/strategy_template/multi_block/engine/request.py` around lines 11 -
25, The _restore_req_runtime_state method must ensure the _resume_prefill_until
attribute exists after unpickling to avoid AttributeError; update the
_restore_req_runtime_state implementation to initialize
self._resume_prefill_until = getattr(self, "_resume_prefill_until", 0) (or
assign 0 if missing) before any sampler or block logic runs—do this alongside
the existing multi-block handling in _restore_req_runtime_state (the same place
that accesses dllm_blocks and dllm_block_buffer) so it matches initialization in
init_multi_block and the defensive pattern used in no_shift.py.
🧹 Nitpick comments (2)
diffulex_bench/configs/llada_instruct_gsm8k.yml (1)

19-21: Prefill concurrency is effectively capped to 1 with these values.

With max_num_batched_tokens == max_model_len (Line 20 and Line 19), warmup prefill concurrency becomes 1 even though max_num_reqs is 24 (Line 21). If this profile is meant for stability-only that’s fine, but if benchmark throughput matters, consider setting max_num_batched_tokens above max_model_len (or add a comment explaining the intentional tradeoff).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@diffulex_bench/configs/llada_instruct_gsm8k.yml` around lines 19 - 21, The
config sets max_num_batched_tokens equal to max_model_len which forces warmup
prefill concurrency to 1 despite max_num_reqs being 24; either raise
max_num_batched_tokens above max_model_len (e.g., to a multiple of
max_model_len) to allow higher prefill concurrency or add a comment nearby
documenting that this equality is intentional to cap concurrency for stability;
update the values or add the explanatory comment referencing the keys
max_model_len, max_num_batched_tokens, and max_num_reqs.
diffulex/strategy_template/multi_block/engine/request.py (1)

397-401: Redundant continue in the preempt demotion loop.

If block.is_to_cache is True, block.is_in_cache is False (the statuses are mutually exclusive), so the if block.is_in_cache: branch is already a no-op for those blocks. The continue at line 399 therefore skips nothing.

Either drop the condition, or if the intent is to document that TO_CACHE blocks within rebuild_until are deliberately preserved, convert it to a clarifying comment. As written, future readers may infer that TO_CACHE would otherwise be demoted here, which is not the case.

♻️ Suggested simplification
         if self.is_multi_block:
             rebuild_until = 0
             if self.is_decoding:
                 rebuild_until = int(self.running_seq_start)
                 self._resume_prefill_until = rebuild_until
-            # Scheduler preemption frees this req's page_table immediately after moving it
-            # back to WAITING. Any block still marked IN_CACHE would then point to KV pages
-            # that no longer belong to the req. Demote those blocks back to TO_CACHE so a
-            # resumed req rebuilds its cached prefix instead of reusing stale block state.
-            for block in self.dllm_blocks:
-                if rebuild_until > 0 and block.end <= rebuild_until and block.is_to_cache:
-                    continue
-                if block.is_in_cache:
-                    block.status = DllmBlockStatus.TO_CACHE
+            # Scheduler preemption frees this req's page_table immediately after moving it
+            # back to WAITING. Any block still marked IN_CACHE would then point to KV pages
+            # that no longer belong to the req. Demote IN_CACHE back to TO_CACHE so a
+            # resumed req rebuilds its cached prefix instead of reusing stale block state.
+            # TO_CACHE blocks are left untouched.
+            for block in self.dllm_blocks:
+                if block.is_in_cache:
+                    block.status = DllmBlockStatus.TO_CACHE
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@diffulex/strategy_template/multi_block/engine/request.py` around lines 397 -
401, The preempt demotion loop contains a redundant continue: in the loop over
self.dllm_blocks, the check "if rebuild_until > 0 and block.end <= rebuild_until
and block.is_to_cache: continue" is unnecessary because block.is_to_cache and
block.is_in_cache are mutually exclusive; remove the continue (or replace the
condition with a short clarifying comment) so the code simply checks "if
block.is_in_cache: block.status = DllmBlockStatus.TO_CACHE" and, if you keep a
note, add a comment near the loop that TO_CACHE blocks within rebuild_until are
intentionally preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@diffulex/sampler/base/no_shift.py`:
- Around line 18-31: The method _maybe_log_prefill_alignment currently only
tracks req_ids in self._debug_prefill_alignment_logged and never emits logs,
wasting CPU because prefill_block_summaries is always built; fix by either
removing this dead scaffolding or implementing real gated logging: in the hot
path where prefill_block_summaries is constructed (the code that builds
per-block summaries and calls _maybe_log_prefill_alignment), wrap that
construction and the call with a logger.isEnabledFor(logging.DEBUG) check, and
update _maybe_log_prefill_alignment to call logger.debug(...) with a concise
summary (using req_id and the passed block_summaries) while still using
self._debug_prefill_alignment_logged to de-duplicate; reference
_maybe_log_prefill_alignment, prefill_block_summaries (the summary construction
site), and self._debug_prefill_alignment_logged when making the changes.

In `@diffulex/utils/loader.py`:
- Around line 54-57: Normalize target_modules to a sequence before the
comparison: if target_modules is a str (or other non-iterable intended as a
single module), wrap it into a list so the any(...) check in the loader logic
works on module names rather than characters. Update the code surrounding
target_modules (used with name, leaf, rev_mapping and should_apply) to coerce
strings into [target_modules] (or otherwise ensure it's a list/tuple) before
computing effective and should_apply.

---

Outside diff comments:
In `@diffulex/strategy_template/multi_block/engine/request.py`:
- Around line 11-25: The _restore_req_runtime_state method must ensure the
_resume_prefill_until attribute exists after unpickling to avoid AttributeError;
update the _restore_req_runtime_state implementation to initialize
self._resume_prefill_until = getattr(self, "_resume_prefill_until", 0) (or
assign 0 if missing) before any sampler or block logic runs—do this alongside
the existing multi-block handling in _restore_req_runtime_state (the same place
that accesses dllm_blocks and dllm_block_buffer) so it matches initialization in
init_multi_block and the defensive pattern used in no_shift.py.

---

Nitpick comments:
In `@diffulex_bench/configs/llada_instruct_gsm8k.yml`:
- Around line 19-21: The config sets max_num_batched_tokens equal to
max_model_len which forces warmup prefill concurrency to 1 despite max_num_reqs
being 24; either raise max_num_batched_tokens above max_model_len (e.g., to a
multiple of max_model_len) to allow higher prefill concurrency or add a comment
nearby documenting that this equality is intentional to cap concurrency for
stability; update the values or add the explanatory comment referencing the keys
max_model_len, max_num_batched_tokens, and max_num_reqs.

In `@diffulex/strategy_template/multi_block/engine/request.py`:
- Around line 397-401: The preempt demotion loop contains a redundant continue:
in the loop over self.dllm_blocks, the check "if rebuild_until > 0 and block.end
<= rebuild_until and block.is_to_cache: continue" is unnecessary because
block.is_to_cache and block.is_in_cache are mutually exclusive; remove the
continue (or replace the condition with a short clarifying comment) so the code
simply checks "if block.is_in_cache: block.status = DllmBlockStatus.TO_CACHE"
and, if you keep a note, add a comment near the loop that TO_CACHE blocks within
rebuild_until are intentionally preserved.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7bbaa9ce-b02f-4e4c-a2d5-c1076ad99b9d

📥 Commits

Reviewing files that changed from the base of the PR and between 0b9c6a1 and f5352f9.

📒 Files selected for processing (9)
  • .gitignore
  • diffulex/sampler/base/no_shift.py
  • diffulex/sampler/base/shift.py
  • diffulex/strategy_template/multi_block/engine/request.py
  • diffulex/utils/loader.py
  • diffulex_bench/configs/dream_base_gsm8k.yml
  • diffulex_bench/configs/fast_dllm_v2_gsm8k.yml
  • diffulex_bench/configs/llada_instruct_gsm8k.yml
  • diffulex_bench/configs/sdar_chat_gsm8k.yml

Comment thread diffulex/sampler/base/no_shift.py Outdated
Comment thread diffulex/utils/loader.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
diffulex/strategy_template/multi_block/engine/request.py (1)

586-598: ⚠️ Potential issue | 🔴 Critical

Set a terminal context for every truncation path.

Line 586 only sets terminal_context_block for EOS/max-token/model-len stops, but Line 592 also gates deactivation on max_nfe_reached and max_repetition_run_reached. For those two stop reasons, last_block_finished stays false because terminal_context_block is still None, so the request can livelock instead of completing.

🐛 Proposed fix
-        if self.eos_token_generated or self.max_new_tokens_reached or self.max_model_len_reached:
+        if self.is_truncated:
             self.set_terminal_context_block(self.dllm_block_buffer.last_valid_block)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@diffulex/strategy_template/multi_block/engine/request.py` around lines 586 -
598, The code only calls
set_terminal_context_block(self.dllm_block_buffer.last_valid_block) when
eos_token_generated/max_new_tokens_reached/max_model_len_reached are true, but
later the completion check also includes max_nfe_reached and
max_repetition_run_reached; ensure terminal_context_block is set for those two
stop reasons too. Update the logic around set_terminal_context_block so that if
any of eos_token_generated, max_new_tokens_reached, max_model_len_reached,
max_nfe_reached, or max_repetition_run_reached is true you call
set_terminal_context_block(self.dllm_block_buffer.last_valid_block) before
evaluating the final conditional that checks last_block_finished; reference the
flags named above and the methods/attributes set_terminal_context_block,
terminal_context_block, last_block_finished, and
dllm_block_buffer.last_valid_block when making the change.
diffulex/engine/engine.py (1)

75-84: ⚠️ Potential issue | 🟠 Major

Move tokenizer loading and mask-token normalization before spawning worker processes.

With the spawn context, config is serialized when process.start() executes. Worker ranks are spawned on lines 29–31 before the tokenizer is loaded and config.mask_token_id is normalized on lines 32–35, causing workers to retain the stale/default mask token ID while rank 0 receives the corrected value.

Proposed fix
         self.ps = []
         self.events = []
+        self.tokenizer = AutoTokenizer.from_pretrained(config.model, use_fast=True, trust_remote_code=True)
+        config.tokenizer_vocab_size = len(self.tokenizer)
+        config.eos = self.tokenizer.eos_token_id
+        maybe_override_mask_token_id(config, self.tokenizer)
+
         ctx = mp.get_context("spawn")
         for i in range(1, self.model_parallel_world_size):
             event = ctx.Event()
             process = ctx.Process(target=AutoModelRunner.from_config, args=(config, i, event))
             process.start()
             self.ps.append(process)
             self.events.append(event)
-        self.tokenizer = AutoTokenizer.from_pretrained(config.model, use_fast=True, trust_remote_code=True)
-        config.tokenizer_vocab_size = len(self.tokenizer)
-        config.eos = self.tokenizer.eos_token_id
-        maybe_override_mask_token_id(config, self.tokenizer)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@diffulex/engine/engine.py` around lines 75 - 84, Move the tokenizer
initialization and mask-token normalization so they run before spawning worker
processes: load AutoTokenizer.from_pretrained and set
config.tokenizer_vocab_size, config.eos and call
maybe_override_mask_token_id(config, tokenizer) prior to the loop that creates
ctx.Event and ctx.Process and starts AutoModelRunner.from_config; this ensures
the serialized config passed to each child (via the ctx.Process start loop that
appends to self.ps/self.events and targets AutoModelRunner.from_config) contains
the corrected mask_token_id and tokenizer-derived fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@diffulex/engine/engine.py`:
- Around line 32-40: maybe_override_mask_token_id is unconditionally overwriting
user-provided mask_token_id; detect whether the user explicitly passed
--mask-token-id by checking config_kwargs in DiffulexEngine.__init__ (set a
boolean mask_token_id_explicit) and pass that flag into
maybe_override_mask_token_id; inside maybe_override_mask_token_id, skip the
tokenizer-based override when mask_token_id_explicit is True so CLI/config
values are preserved, and ensure you set/pass the explicit flag before worker
processes are spawned so non-rank-0 runners receive the correct config.

---

Outside diff comments:
In `@diffulex/engine/engine.py`:
- Around line 75-84: Move the tokenizer initialization and mask-token
normalization so they run before spawning worker processes: load
AutoTokenizer.from_pretrained and set config.tokenizer_vocab_size, config.eos
and call maybe_override_mask_token_id(config, tokenizer) prior to the loop that
creates ctx.Event and ctx.Process and starts AutoModelRunner.from_config; this
ensures the serialized config passed to each child (via the ctx.Process start
loop that appends to self.ps/self.events and targets
AutoModelRunner.from_config) contains the corrected mask_token_id and
tokenizer-derived fields.

In `@diffulex/strategy_template/multi_block/engine/request.py`:
- Around line 586-598: The code only calls
set_terminal_context_block(self.dllm_block_buffer.last_valid_block) when
eos_token_generated/max_new_tokens_reached/max_model_len_reached are true, but
later the completion check also includes max_nfe_reached and
max_repetition_run_reached; ensure terminal_context_block is set for those two
stop reasons too. Update the logic around set_terminal_context_block so that if
any of eos_token_generated, max_new_tokens_reached, max_model_len_reached,
max_nfe_reached, or max_repetition_run_reached is true you call
set_terminal_context_block(self.dllm_block_buffer.last_valid_block) before
evaluating the final conditional that checks last_block_finished; reference the
flags named above and the methods/attributes set_terminal_context_block,
terminal_context_block, last_block_finished, and
dllm_block_buffer.last_valid_block when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17fad98f-c049-44e2-a280-cc0a73608e66

📥 Commits

Reviewing files that changed from the base of the PR and between f5352f9 and 1fca0f5.

📒 Files selected for processing (6)
  • diffulex/engine/engine.py
  • diffulex/sampler/base/no_shift.py
  • diffulex/server/args.py
  • diffulex/strategy_template/multi_block/engine/request.py
  • diffulex/utils/loader.py
  • examples/streamlit_block_append_chat.py
✅ Files skipped from review due to trivial changes (1)
  • examples/streamlit_block_append_chat.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • diffulex/utils/loader.py

Comment thread diffulex/engine/engine.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
diffulex/engine/engine.py (1)

24-55: ⚠️ Potential issue | 🟠 Major

Preserve explicit mask_token_id overrides from CLI/config.

maybe_override_mask_token_id still unconditionally replaces config.mask_token_id when tokenizer/hf_config has a different value, so explicit --mask-token-id can still be ignored (see diffulex/server/args.py Line 92-93 passing it through). This was previously flagged and remains unresolved.

🐛 Proposed fix
-def maybe_override_mask_token_id(config: Config, tokenizer) -> None:
+def maybe_override_mask_token_id(
+    config: Config,
+    tokenizer,
+    *,
+    mask_token_id_explicit: bool = False,
+) -> None:
@@
+    if mask_token_id_explicit:
+        return
+
     tokenizer_mask_token_id = getattr(tokenizer, "mask_token_id", None)
@@
-        maybe_override_mask_token_id(config, self.tokenizer)
+        maybe_override_mask_token_id(
+            config,
+            self.tokenizer,
+            mask_token_id_explicit="mask_token_id" in config_kwargs,
+        )

Also applies to: 77-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@diffulex/engine/engine.py` around lines 24 - 55, The function
maybe_override_mask_token_id currently unconditionally replaces
config.mask_token_id from the tokenizer branch, which lets explicit CLI/config
overrides be lost; change both override sites in maybe_override_mask_token_id
(the tokenizer branch that sets config.mask_token_id from
tokenizer.mask_token_id and the hf_config branch) to only assign when the
current config.mask_token_id equals the class default sentinel
(Config.mask_token_id). In other words, obtain default_mask_token_id =
Config.mask_token_id and wrap the tokenizer override in a guard like "and
config.mask_token_id == default_mask_token_id" (the hf_config branch already has
this check—ensure the same logic is applied to the tokenizer branch and any
duplicate occurrence around the later block).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@diffulex/engine/engine.py`:
- Around line 24-55: The function maybe_override_mask_token_id currently
unconditionally replaces config.mask_token_id from the tokenizer branch, which
lets explicit CLI/config overrides be lost; change both override sites in
maybe_override_mask_token_id (the tokenizer branch that sets
config.mask_token_id from tokenizer.mask_token_id and the hf_config branch) to
only assign when the current config.mask_token_id equals the class default
sentinel (Config.mask_token_id). In other words, obtain default_mask_token_id =
Config.mask_token_id and wrap the tokenizer override in a guard like "and
config.mask_token_id == default_mask_token_id" (the hf_config branch already has
this check—ensure the same logic is applied to the tokenizer branch and any
duplicate occurrence around the later block).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2ea6e083-500e-40c3-9eb8-32544fe5a09c

📥 Commits

Reviewing files that changed from the base of the PR and between 1fca0f5 and 5e8b082.

📒 Files selected for processing (11)
  • diffulex/engine/engine.py
  • diffulex/strategy_template/multi_block/engine/request.py
  • diffulex_bench/configs/dream_base_gsm8k.yml
  • diffulex_bench/configs/llada_instruct_gsm8k.yml
  • docs/cookbook/benchmark.md
  • docs/cookbook/index.md
  • docs/cookbook/models/dream_base.md
  • docs/cookbook/models/fast_dllm_v2.md
  • docs/cookbook/models/llada_instruct.md
  • docs/cookbook/models/sdar_chat.md
  • docs/cookbook/server.md
✅ Files skipped from review due to trivial changes (7)
  • docs/cookbook/models/fast_dllm_v2.md
  • docs/cookbook/server.md
  • docs/cookbook/benchmark.md
  • docs/cookbook/models/llada_instruct.md
  • docs/cookbook/models/dream_base.md
  • docs/cookbook/index.md
  • docs/cookbook/models/sdar_chat.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • diffulex_bench/configs/llada_instruct_gsm8k.yml
  • diffulex_bench/configs/dream_base_gsm8k.yml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
diffulex/engine/engine.py (1)

24-33: ⚠️ Potential issue | 🟡 Minor

Explicit mask_token_id is still ambiguous when user passes the default value.

The helper infers “explicitness” by value (config.mask_token_id == Config.mask_token_id), so an explicitly provided default can still be overridden. Please pass an explicit boolean from config_kwargs to preserve user intent in all cases.

🐛 Minimal fix
-def maybe_override_mask_token_id(config: Config, tokenizer) -> None:
+def maybe_override_mask_token_id(
+    config: Config,
+    tokenizer,
+    *,
+    mask_token_id_explicit: bool = False,
+) -> None:
@@
+    if mask_token_id_explicit:
+        return
+
     default_mask_token_id = Config.mask_token_id
@@
-        maybe_override_mask_token_id(config, self.tokenizer)
+        maybe_override_mask_token_id(
+            config,
+            self.tokenizer,
+            mask_token_id_explicit="mask_token_id" in config_kwargs,
+        )

Also applies to: 35-42, 85-85

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@diffulex/engine/engine.py` around lines 24 - 33, The current
maybe_override_mask_token_id logic infers whether the user explicitly set
mask_token_id by comparing values, which misclassifies an explicitly-provided
default; change the function to accept an explicit boolean flag (e.g.,
mask_token_id_explicit) passed from config_kwargs and use that flag instead of
value equality to decide whether to skip tokenizer/HF overrides; update the call
sites that construct Config (where config_kwargs are built) to set this boolean
when the user provided mask_token_id, and apply the same change to the other
similar checks referenced (the duplicate logic around lines 35-42 and the check
at 85) to use the explicit boolean parameter so user intent is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@diffulex/engine/engine.py`:
- Around line 24-33: The current maybe_override_mask_token_id logic infers
whether the user explicitly set mask_token_id by comparing values, which
misclassifies an explicitly-provided default; change the function to accept an
explicit boolean flag (e.g., mask_token_id_explicit) passed from config_kwargs
and use that flag instead of value equality to decide whether to skip
tokenizer/HF overrides; update the call sites that construct Config (where
config_kwargs are built) to set this boolean when the user provided
mask_token_id, and apply the same change to the other similar checks referenced
(the duplicate logic around lines 35-42 and the check at 85) to use the explicit
boolean parameter so user intent is preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1f832dc-d836-4e03-a73e-4c0f992d3fd4

📥 Commits

Reviewing files that changed from the base of the PR and between 5e8b082 and 966b75f.

📒 Files selected for processing (6)
  • diffulex/engine/engine.py
  • docs/cookbook/models/dream_base.md
  • docs/cookbook/models/fast_dllm_v2.md
  • docs/cookbook/models/llada_instruct.md
  • docs/cookbook/models/sdar_chat.md
  • docs/cookbook/server.md
✅ Files skipped from review due to trivial changes (3)
  • docs/cookbook/models/llada_instruct.md
  • docs/cookbook/models/fast_dllm_v2.md
  • docs/cookbook/models/dream_base.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/cookbook/models/sdar_chat.md
  • docs/cookbook/server.md

Osc-7 added 5 commits June 24, 2026 07:33
…uest flow

- add preempt-rebuild request state handling in multi_block request template
- prevent decode livelock by recycling cached head block when frontier is lost
- align no_shift prefill sampling with resume_prefill_until window to avoid out-of-window block sampling

This keeps the fix scoped to request/no_shift without experimental guards or benchmark config changes.
@Osc-7
Osc-7 force-pushed the fix-llada-lora-bug branch from 4550c0b to 92cc4e6 Compare June 24, 2026 19:24

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@diffulex/engine/engine.py`:
- Around line 376-389: Validate the `sampling_params` input in `engine.py`
before the `zip(prompts, sampling_params)` loop inside the request scheduling
logic. The issue is that a list whose length differs from `prompts` will be
silently truncated by `zip`, so add an explicit length check after the
`sampling_params` normalization and before building `req_id_to_prompt_id` in the
scheduling path. If the lengths do not match, raise a clear error from this
request-adding flow so every prompt has exactly one corresponding
`sampling_params` entry.
- Around line 91-98: Refresh the derived runtime config after
tokenizer-dependent mutations in Engine initialization. In `Engine.__init__`,
after `auto_tokenizer_from_pretrained`, `maybe_override_mask_token_id`, and the
updates to `config.tokenizer_vocab_size` and `config.eos`, also propagate the
final values into `config.runtime.model` so `tokenizer_vocab_size`, `eos`, and
`mask_token_id` stay in sync for consumers of `config.runtime`.

In `@diffulex/sampler/base/no_shift.py`:
- Line 72: The zip loop in the sampling path is vulnerable to silently
truncating mismatched inputs; update the loop in the no_shift sampler’s request
iteration to use zip with strict checking so temperature, request, and logits
lengths must match. Make this change in the enumerate(zip(...)) loop inside the
relevant sampling function in no_shift.py, ensuring any mismatch fails fast
instead of producing partial output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95882a22-7886-4aaf-bac5-3125a01082e1

📥 Commits

Reviewing files that changed from the base of the PR and between 966b75f and 92cc4e6.

📒 Files selected for processing (6)
  • diffulex/engine/engine.py
  • diffulex/engine/request.py
  • diffulex/sampler/base/no_shift.py
  • diffulex/sampler/base/shift.py
  • diffulex/server/args.py
  • diffulex/utils/loader.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • diffulex/server/args.py
  • diffulex/utils/loader.py
  • diffulex/sampler/base/shift.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@diffulex/engine/engine.py`:
- Around line 376-389: Validate the `sampling_params` input in `engine.py`
before the `zip(prompts, sampling_params)` loop inside the request scheduling
logic. The issue is that a list whose length differs from `prompts` will be
silently truncated by `zip`, so add an explicit length check after the
`sampling_params` normalization and before building `req_id_to_prompt_id` in the
scheduling path. If the lengths do not match, raise a clear error from this
request-adding flow so every prompt has exactly one corresponding
`sampling_params` entry.
- Around line 91-98: Refresh the derived runtime config after
tokenizer-dependent mutations in Engine initialization. In `Engine.__init__`,
after `auto_tokenizer_from_pretrained`, `maybe_override_mask_token_id`, and the
updates to `config.tokenizer_vocab_size` and `config.eos`, also propagate the
final values into `config.runtime.model` so `tokenizer_vocab_size`, `eos`, and
`mask_token_id` stay in sync for consumers of `config.runtime`.

In `@diffulex/sampler/base/no_shift.py`:
- Line 72: The zip loop in the sampling path is vulnerable to silently
truncating mismatched inputs; update the loop in the no_shift sampler’s request
iteration to use zip with strict checking so temperature, request, and logits
lengths must match. Make this change in the enumerate(zip(...)) loop inside the
relevant sampling function in no_shift.py, ensuring any mismatch fails fast
instead of producing partial output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95882a22-7886-4aaf-bac5-3125a01082e1

📥 Commits

Reviewing files that changed from the base of the PR and between 966b75f and 92cc4e6.

📒 Files selected for processing (6)
  • diffulex/engine/engine.py
  • diffulex/engine/request.py
  • diffulex/sampler/base/no_shift.py
  • diffulex/sampler/base/shift.py
  • diffulex/server/args.py
  • diffulex/utils/loader.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • diffulex/server/args.py
  • diffulex/utils/loader.py
  • diffulex/sampler/base/shift.py
🛑 Comments failed to post (3)
diffulex/engine/engine.py (2)

91-98: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Refresh derived config after tokenizer overrides.

Config.__post_init__ has already built config.runtime before these tokenizer-derived mutations. Workers receive the mutated config, but consumers reading config.runtime.model.* can still see stale tokenizer_vocab_size, eos, or default mask_token_id.

Proposed fix
         maybe_override_mask_token_id(
             config,
             self.tokenizer,
             mask_token_id_explicit="mask_token_id" in config_kwargs,
         )
+        hf_config = getattr(config, "hf_config", None)
+        for target in (hf_config, getattr(hf_config, "text_config", None)):
+            if target is not None:
+                setattr(target, "mask_token_id", config.mask_token_id)
+        config._build_runtime_config()
 
         ctx = mp.get_context("spawn")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

        self.tokenizer = auto_tokenizer_from_pretrained(config.model, use_fast=True, trust_remote_code=True)
        config.tokenizer_vocab_size = len(self.tokenizer)
        config.eos = self.tokenizer.eos_token_id
        maybe_override_mask_token_id(
            config,
            self.tokenizer,
            mask_token_id_explicit="mask_token_id" in config_kwargs,
        )
        hf_config = getattr(config, "hf_config", None)
        for target in (hf_config, getattr(hf_config, "text_config", None)):
            if target is not None:
                setattr(target, "mask_token_id", config.mask_token_id)
        config._build_runtime_config()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@diffulex/engine/engine.py` around lines 91 - 98, Refresh the derived runtime
config after tokenizer-dependent mutations in Engine initialization. In
`Engine.__init__`, after `auto_tokenizer_from_pretrained`,
`maybe_override_mask_token_id`, and the updates to `config.tokenizer_vocab_size`
and `config.eos`, also propagate the final values into `config.runtime.model` so
`tokenizer_vocab_size`, `eos`, and `mask_token_id` stay in sync for consumers of
`config.runtime`.

376-389: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate sampling_params length before zipping.

When a list length differs from prompts, zip() silently drops extras, leaving missing outputs or ignored params.

Proposed fix
             if not isinstance(sampling_params, list):
                 sampling_params = [sampling_params] * len(prompts)
+            elif len(sampling_params) != len(prompts):
+                raise ValueError(
+                    "sampling_params length must match prompts length, "
+                    f"got {len(sampling_params)} and {len(prompts)}."
+                )
             total_expected_tokens = sum(max(getattr(sp, "max_tokens", 0) or 0, 0) for sp in sampling_params)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            if not isinstance(sampling_params, list):
                sampling_params = [sampling_params] * len(prompts)
            elif len(sampling_params) != len(prompts):
                raise ValueError(
                    "sampling_params length must match prompts length, "
                    f"got {len(sampling_params)} and {len(prompts)}."
                )
            total_expected_tokens = sum(max(getattr(sp, "max_tokens", 0) or 0, 0) for sp in sampling_params)

            req_id_to_prompt_id = {}
            for prompt_id, (prompt, sp) in tqdm(
                enumerate(zip(prompts, sampling_params)),
                total=len(prompts),
                desc="Adding Requests to Scheduler",
                dynamic_ncols=True,
                disable=not use_tqdm,
                leave=False,
                mininterval=1.0,
            ):
🧰 Tools
🪛 Ruff (0.15.18)

[warning] 382-382: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@diffulex/engine/engine.py` around lines 376 - 389, Validate the
`sampling_params` input in `engine.py` before the `zip(prompts,
sampling_params)` loop inside the request scheduling logic. The issue is that a
list whose length differs from `prompts` will be silently truncated by `zip`, so
add an explicit length check after the `sampling_params` normalization and
before building `req_id_to_prompt_id` in the scheduling path. If the lengths do
not match, raise a clear error from this request-adding flow so every prompt has
exactly one corresponding `sampling_params` entry.

Source: Linters/SAST tools

diffulex/sampler/base/no_shift.py (1)

72-72: 🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Python/tooling target supports zip(..., strict=...)
fd -HI "pyproject.toml|setup.cfg|tox.ini|.python-version|runtime.txt|.tool-versions" . \
  | xargs -I{} sh -c 'echo "### {}"; sed -n "1,220p" "{}"'

Repository: SJTU-DENG-Lab/Diffulex

Length of output: 2197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant file and nearby call sites for length alignment.
file="diffulex/sampler/base/no_shift.py"
wc -l "$file"
sed -n '1,180p' "$file"

printf '\n--- call sites / related helpers ---\n'
rg -n "split_logits|temperatures|reqs" diffulex/sampler -S

Repository: SJTU-DENG-Lab/Diffulex

Length of output: 15849


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether Python 3.11+ is the project target and whether strict zip is available in that target.
python3 - <<'PY'
import sys
print(sys.version)
print("zip strict supported:", sys.version_info >= (3, 10))
PY

Repository: SJTU-DENG-Lab/Diffulex

Length of output: 237


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,180p' diffulex/sampler/base/no_shift.py
printf '\n---\n'
rg -n "split_logits|temperatures|reqs" diffulex/sampler -S

Repository: SJTU-DENG-Lab/Diffulex

Length of output: 15778


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the sampler call chain and how temperatures are shaped/passed in.
rg -n "temperatures\s*=" diffulex -S
printf '\n--- base/shift.py ---\n'
sed -n '1,140p' diffulex/sampler/base/shift.py
printf '\n--- llada2.py relevant sections ---\n'
sed -n '250,360p' diffulex/sampler/llada2.py
sed -n '520,590p' diffulex/sampler/llada2.py

Repository: SJTU-DENG-Lab/Diffulex

Length of output: 14590


Use strict=True in this zip loop.

zip(temperatures, reqs, split_logits) can silently drop requests if one sequence is shorter, which would produce partial sampling output. This codebase targets Python 3.11+, so strict=True is available here.

Suggested patch
-            for idx, (temperature, req, req_logits) in enumerate(zip(temperatures, reqs, split_logits)):
+            for idx, (temperature, req, req_logits) in enumerate(
+                zip(temperatures, reqs, split_logits, strict=True)
+            ):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

            for idx, (temperature, req, req_logits) in enumerate(
                zip(temperatures, reqs, split_logits, strict=True)
            ):
🧰 Tools
🪛 Ruff (0.15.18)

[warning] 72-72: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@diffulex/sampler/base/no_shift.py` at line 72, The zip loop in the sampling
path is vulnerable to silently truncating mismatched inputs; update the loop in
the no_shift sampler’s request iteration to use zip with strict checking so
temperature, request, and logits lengths must match. Make this change in the
enumerate(zip(...)) loop inside the relevant sampling function in no_shift.py,
ensuring any mismatch fails fast instead of producing partial output.

Source: Linters/SAST tools

@drewjin

drewjin commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

lgtm

@drewjin
drewjin merged commit f19bc2c into SJTU-DENG-Lab:main Jun 26, 2026
1 check passed
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