Qwen3.8 27b - #751
Open
michyu-amd wants to merge 10 commits into
Open
Qwen3.8 27b#751michyu-amd wants to merge 10 commits into
michyu-amd wants to merge 10 commits into
Conversation
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
qwen3-8mtp— the Qwen3.8-27B engine — as a new model family, and adds theruntime plumbing for speculative decoding driven by the model's MTP draft head.
Qwen3.8-27B is a hybrid stack: 64 decoder layers in the pattern
[linear, linear, linear, full] x 16(48 GatedDeltaNet + 16 full attention),plus a 1-layer MTP head used as a draft model. The engine dispatches to the
NPU2 bitstreams checked in under
src/xclbins/Qwen3.8-27B-NPU2/, with CPUreference kernels as the fallback path.
Model is registered at
qwen3-8mtp:27b→FastFlowLM/Qwen3.8-27B-NPU2(Q4_K, 17.1 GB footprint, 4096 default context, 256 max prefill).
What's in it
New engine
src/include/models/qwen3_8mtp/qwen3_8mtp_npu.hpp+src/lib/xrt/libqwen3_8mtp_npu.sosrc/common/AutoModel/modeling_qwen3_8mtp.cpp—AutoModelwrappersrc/common/AutoModel/modeling_qwen3_8mtp_image.cpp— vision tower host-sidepipeline (active only when the checkpoint ships
vision_weight.q4nx; imagesare rejected loudly on a text-only checkpoint rather than silently dropped)
src/test/qwen3_8mtp_npu/— standalone driver for scoring the engine againsta reference dump
all_models.hpp,automodel.hpp,CMakeLists.txt,model_list.jsonSpeculative decoding
causal_lm, all defaulted (not pure) so the other 16engines are unchanged in behaviour:
supports_speculation(),speculate(last_token, max_draft),last_speculation_prime_us().AutoModel::_shared_generate()takes the speculative path only when theengine supports it and the sampler is truly greedy —
top_k == 1and allthree penalties neutral. Acceptance is an exact argmax compare, so running it
under any other sampler would silently substitute greedy decoding for the
user's configured sampler. The output would still be fluent, so nothing
downstream would ever flag it; all four conditions are load-bearing.
into a single
consume()lambda shared by both paths, so the speculativebranch cannot drift into emitting text past a stop token.
cache over the prompt window. That is prefill-shaped work happening inside a
speculate()call with the decode clock running, and it scales with promptlength — leaving it there makes decode tok/s a function of prompt size. The
engine reports the split and the runtime moves it from
DECODING_TIMEtoPREFILL_TIME.Profiling
show_profile()re-enables the sampling / token-encode / token-decode rows(previously commented out) and adds an
Untimedremainder row.This was added because the gap was real and invisible: Qwen3.8's think
preamble spent ~13 s of a 24 s run in four
forward()calls that_shared_generate()'sDECODING_TIME.reset()then discarded.Caveat documented in-place: the narrow rows only compare against Total on a
single-turn run, since
DECODING_TIMEresets per turn whilePREFILL_TIMEand
TOTAL_TIMEaccumulate. A multi-turn session over-reportsUntimed, andthe row prints signed rather than clamping, so that case announces itself.
profiler::add_time(int64_t)for transferring an already-measured intervalbetween buckets (clamped at zero).
minja
is undefinedtest. Upstream minja (3e4c61c) implementsis definedbut notits negation, and throws on the test name before looking at the value, so
x is undefinedfails even whenxis set. Qwen3.8-27B's chat template openswith
{%- if enable_thinking is undefined or enable_thinking is true %}.Blocking before merge: stale engine vtables
Adding virtuals to
causal_lmis a lockstep change. Every engine.soemits its own vtable and
flmloads those prebuilt; appending to the base classrenumbers the vtable, so any
.sonot rebuilt keeps one that is short by theadded slots, and the first virtual call past the end segfaults — after a clean
prefill, with no diagnostic. A link check cannot catch this: the vtable symbol
still resolves, it is merely the wrong size.
This branch rebuilt only
libqwen3_8mtp_npu.so. The other 16 are stale:All engine
.sofiles undersrc/lib/xrt/need to be regenerated and re-copiedin this PR before it is safe to merge — otherwise every model except
Qwen3.8-27B crashes on its first decode step. Verify with the
readelfcommandabove rather than with "it compiled" or "it linked".
(Note: the ABI comment in
causal_lm.hppsays the change takes the vtable from120 to 136 bytes — it is 144, since three virtuals were added, not two. Worth
correcting so the number in the comment matches what
readelfprints.)Also needs a look before merge
prompt.txtat the repo root looks like a stray scratch file (an LLMprompt about dispatch-count profiling) rather than something intended to ship.
Probably wants deleting.
.claude/in.gitignore— fine if the project wants it, flagging onlybecause it is unrelated to the model.
Testing
src/test/qwen3_8mtp_npu/driver runs a text turn and (with-i) onemultimodal turn, scoring intermediates against a reference dump.
mean_accepted_length(),draft_hit_rate(),speculation_cycles(),speculation_timing(),speculation_phase_fraction()) because a broken draft path is otherwisesilent — verify overrides every rejected draft with the base model's own
argmax, so bad drafting still produces correct text and merely burns time.
speculation_phase_fraction()is deliberately normalised by measured cycletime, not by the sum of the parts, so a double-counted phase pushes it above
1.0 instead of hiding itself.
Still to confirm on hardware:
readelfvtable sizes agreeUntimedrow sanity-checked on a single-turn run