Skip to content

[DSpark] Export DSpark drafts with a vanilla Markov head [part of MiniCPM-V 4.7 support] - #2062

Open
imargulis wants to merge 6 commits into
huggingface:mainfrom
imargulis:dspark-preview
Open

imargulis wants to merge 6 commits into
huggingface:mainfrom
imargulis:dspark-preview

Conversation

@imargulis

@imargulis imargulis commented Oct 13, 2026 •

Copy link
Copy Markdown

What does this PR do?

Adds export of DSpark drafts, DFlash drafts with a Markov head that adjusts each drafted token's logits based on the previous token. Until now, drafts with a Markov head (markov_rank > 0) were rejected.

The draft backbone is exported as for DFlash. The Markov head is exported next to it as one model, openvino_markov_head_model.xml, that holds two computations over the same weights: a dense Markov step and a Top-K candidate scorer. OpenVINO GenAI selects one of them when it loads the draft, so the export has no DSpark-specific options. Only the vanilla Markov head is supported for now. It is never weight-compressed, so it keeps its original precision unless --weight-format fp16/fp32 is given.

Runtime support: openvinotoolkit/openvino.genai#4660. The draft backbone follows the current DFlash draft contract, which is planned to change with continuous batching support for DFlash-family drafts. The Markov head model is built for compatibility with continuous batching already: the candidate scorer takes the draft blocks of several requests packed back to back.

Also:

  • MiniCPM-V 4.7 language models get the hidden-state metadata needed to serve as targets for DFlash-family drafts.
  • DFlash and DSpark drafts are exported without a tokenizer, since they run with the target's tokenizer.

How to test

Environment (Python 3.11+). Until openvinotoolkit/openvino.genai#4660 is merged, OpenVINO GenAI is built from its branch, which needs CMake 3.26+ and a C++ compiler:

python -m venv dspark-env
source dspark-env/bin/activate  # Windows: dspark-env\Scripts\activate
python -m pip install --upgrade pip

# optimum-intel from this PR. Loading the draft through remote code needs diffusers and einops.
MiniCPM-V 4.7 needs transformers>=5.18
pip install "optimum-intel[openvino] @ git+https://github.com/imargulis/optimum-intel.git@dspark-preview"
pip install "transformers>=5.18"

# OpenVINO and OpenVINO Tokenizers nightly, and OpenVINO GenAI with DSpark support
pip install --pre -U openvino openvino-tokenizers --extra-index-url https://storage.openvinotoolkit.org/simple/wheels/nightly
pip install --pre "openvino-genai @ git+https://github.com/imargulis/openvino.genai.git@dspark-preview" --extra-index-url https://storage.openvinotoolkit.org/simple/wheels/nightly

Export the target and the DSpark draft:

optimum-cli export openvino --model openbmb/MiniCPM-V-4.7-1B --task image-text-to-text --weight-format int8 minicpm-v-4.7-1b-int8-ov
optimum-cli export openvino --model openbmb/MiniCPM-V-4.7-1B-DSpark --task text-generation-with-past --trust-remote-code minicpm-v-4.7-1b-dspark-ov

Once #1983 is merged, the draft export needs neither option for a Hub model ID: DFlash and DSpark drafts are then loaded directly instead of through remote code, and their task is inferred. A local checkpoint folder still needs --task text-generation-with-past, because the task cannot be inferred from a local folder.

optimum-cli export openvino --model openbmb/MiniCPM-V-4.7-1B-DSpark minicpm-v-4.7-1b-dspark-ov

Run with OpenVINO GenAI:

import openvino_genai as ov_genai

# MiniCPM-V 4.7 has linear attention: the scheduler needs at least num_assistant_tokens + 2 linear-attention blocks.
scheduler_config = ov_genai.SchedulerConfig()
scheduler_config.num_linear_attention_blocks = 17

# Top-64 candidate scorer by default; markov_head_top_k=16 changes K, markov_head_top_k=0 selects the dense Markov walk.
draft = ov_genai.draft_model("minicpm-v-4.7-1b-dspark-ov", "GPU")
pipe = ov_genai.VLMPipeline("minicpm-v-4.7-1b-int8-ov", "GPU", draft_model=draft, scheduler_config=scheduler_config)

config = ov_genai.GenerationConfig()
config.max_new_tokens = 256
config.num_assistant_tokens = 15

result = pipe.generate("Write a Python function that checks whether a number is prime.", generation_config=config)
print(result.texts[0])
print("accepted tokens:", result.extended_perf_metrics.get_num_accepted_tokens())

On GPU, the Top-K candidate scorer needs openvinotoolkit/openvino#38845 to be fast. With the current nightly, its TopK over the vocabulary falls back to a generic GPU kernel, and the draft is slower than the target alone.

Tests

Validated end to end with OpenVINO GenAI on MiniCPM-V-4.7-1B and its DSpark draft: with both computations, the outputs and accepted tokens are identical to those of the previous revision's separate Markov head models, which were also validated with MiniCPM-V-4.7-35B-A3B-DSpark. Unit tests use a tiny generated DSpark checkpoint and cover numerical parity of both computations, packed draft blocks of different lengths, changing K, metadata, precision, CLI export, and drafts without a Markov head still exporting as plain DFlash drafts. A GenAI test runs the tiny DSpark draft with a tiny Qwen3 target through the continuous batching pipeline and checks that greedy outputs match the target alone; the preview models workflow runs the DSpark tests against the GenAI nightly.

Before submitting

  • This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
  • Did you make sure to update the documentation with your changes?
  • Did you write any new necessary tests?

@rkazants
rkazants requested review from echarlaix and rkazants and a balanced review from Copilot October 13, 2026 08:33
Comment thread .github/workflows/test_openvino_preview_models.yml
Comment thread optimum/commands/export/openvino.py Outdated
Comment thread optimum/exporters/openvino/__main__.py Outdated
Comment thread optimum/exporters/openvino/__main__.py

Copilot AI 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.

馃煛 Changes recommended

CLI dtype overrides break checkpoint-precision preservation, and the fixture cache can reuse stale artifacts.

2 open findings
What changed in this PR

Adds OpenVINO export support for DSpark drafts with vanilla Markov heads and MiniCPM-V 4.7 targets.

Changes:

  • Exports dense or Top-K Markov-head sidecar models.
  • Adds DSpark validation, metadata, CLI options, and precision handling.
  • Adds extensive export, parity, compression, and CI coverage.
File Description
optimum/鈥媏xporters/鈥媜penvino/鈥媎flash_utils.py Defines DSpark configuration validation and interfaces.
optimum/鈥媏xporters/鈥媜penvino/鈥媘odel_patcher.py Implements the DSpark backbone and Markov exporters.
optimum/鈥媏xporters/鈥媜penvino/鈥媘odel_configs.py Adds Markov model export contracts.
optimum/鈥媏xporters/鈥媜penvino/鈥媔nput_generators.py Generates Markov export inputs.
optimum/鈥媏xporters/鈥媜penvino/鈥媍onvert.py Splits and annotates DSpark exports.
optimum/鈥媏xporters/鈥媜penvino/鈥媌ase.py Adds precision-preservation configuration.
optimum/鈥媏xporters/鈥媜penvino/鈥媉_main__.py Integrates DSpark export and tokenizer handling.
optimum/鈥媍ommands/鈥媏xport/鈥媜penvino.py Adds --dspark-top-k.
tests/鈥媜penvino/鈥媢tils_tests.py Creates tiny DSpark fixtures.
tests/鈥媜penvino/鈥媡est_dflash.py Tests parity, metadata, and precision.
tests/鈥媜penvino/鈥媡est_exporters_cli.py Tests CLI packages and tokenizer omission.
docs/鈥媠ource/鈥媜penvino/鈥媘odels.mdx Documents supported DSpark checkpoints.
.github/鈥媤orkflows/鈥媡est_openvino_preview_models.yml Adds MiniCPM-V metadata validation.

馃 Review effort: Balanced


馃挕 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread optimum/exporters/openvino/model_configs.py Outdated
Comment thread tests/openvino/utils_tests.py Outdated
Comment thread optimum/exporters/openvino/convert.py Outdated

@rkazants rkazants left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please also provide proper PR descriprion with full standalone code snippets for models export, inference using Openvino GenAI. How to set up environment to test it

@rkazants rkazants left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

btw, did you cherry-pick my changes for CB support?

@imargulis

imargulis commented Oct 13, 2026 via email •

Copy link
Copy Markdown
Author

@imargulis

imargulis commented Oct 13, 2026 via email •

Copy link
Copy Markdown
Author

@imargulis
imargulis force-pushed the dspark-preview branch 2 times, most recently from ce0e7ec to 5daa20d Compare October 13, 2026 12:47
@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Comment thread optimum/commands/export/openvino.py Outdated
@imargulis imargulis changed the title [DSpark] Export DSpark drafts with a vanilla Markov head [DSpark] Export DSpark drafts with a vanilla Markov head [part of MiniCPM-V 4.7 support] Oct 17, 2026
@imargulis
imargulis requested a review from rkazants October 17, 2026 11:44
Comment thread .github/workflows/test_openvino_preview_models.yml Outdated
Comment thread .github/workflows/test_openvino_preview_models.yml Outdated
Comment thread optimum/exporters/openvino/convert.py Outdated
Comment thread optimum/exporters/openvino/convert.py Outdated
Comment thread optimum/exporters/openvino/convert.py
Comment thread optimum/exporters/openvino/input_generators.py Outdated
@imargulis

Copy link
Copy Markdown
Author

@rkazants

Two follow-ups for after this PR and #1983 merge.

1. Rename Eagle3DummyGenerator. It now generates the inputs of Eagle-3, DFlash and DSpark drafts, so the name is misleading. What these drafts share is that they are grafted onto the target model: they read its hidden states. I propose GraftedDraftDummyGenerator and GraftedDraftVLMDummyGenerator (for Eagle3VLMDummyGenerator), with the term defined in the docstring. It touches several model configs, so I'd keep it out of this PR.

2. DSpark drafts with a pruned vocabulary. Several published DSpark drafts (which is also true for some DFlash drafts) score only part of the target vocabulary, as Eagle-3 drafts do:

Draft Draft vocabulary Target vocabulary
RedHatAI/Qwen3.6-35B-A3B-speculator.dspark 32,000 248,320
novita/kimi-k2.6-dspark 32,000 163,840
mpashkovskii/dspark-qwen3-8b 32,000 151,936
Qwen3.6-27B-DSpark-49k 49,167 248,320

This PR doesn't support them. It supports the DeepSpec format of the MiniCPM-V 4.7 drafts, which reuse the target's lm_head and whose Markov tables cover the full vocabulary (their draft_vocab_size equals vocab_size). The first three drafts above are also in the speculators format, which differs beyond the vocabulary:

  • The config has no top-level model_type. Its auto_map points to a config.py that imports the speculators package, the Qwen3 settings are nested in transformer_layer_config, and the target layers are listed in aux_hidden_state_layer_ids. The architecture is DSparkDraftModel (novita, mpashkovskii) or Qwen3DSparkModel (RedHat, the same name as the DeepSpec format). All these can be normalized.
  • The weights include the draft's own lm_head [32000, H], d2t [32000] (draft id to target id, as in Eagle-3), t2d [V], a full copy of embed_tokens [V, H], and a confidence head [1, H + 256]. The Markov head is split between the two vocabularies: markov_w1 [V, 256] is looked up by the previous target token, and markov_w2 [32000, 256] gives a bias over the draft vocabulary.

With this PR, these checkpoints fail when loaded (missing model_type, unknown architecture, or a markov_w2 shape mismatch) rather than producing a draft that proposes wrong tokens.

Supporting them changes the draft contract with GenAI: the draft applies its own lm_head, the Markov head's Top-K runs over the draft vocabulary, and the ids it returns are mapped to target ids through d2t. Eagle-3 already does this mapping in both repositories: the export outputs d2t, and GenAI maps the draft's ids. It is worth doing: for Qwen3.6 the output layer and the Top-K shrink by about 7.75x. speculators' DSpark config extends its DFlash config, so speculators-format DFlash drafts probably share this layout, and the two are best designed together with continuous batching support.

Not sure who's the decision maker whether the above should be supported.

@imargulis
imargulis requested a review from rkazants October 19, 2026 21:32
imargulis and others added 5 commits October 24, 2026 09:41
Export the Markov head next to the DFlash backbone, as the dense Markov step or, with --dspark-top-k, as a Top-K candidate scorer. The head is not weight-compressed, and markov_rank=0 drafts export as plain DFlash drafts.

Co-authored-by: Cursor <cursoragent@cursor.com>
The drafts run with the target's tokenizer; without a tokenizer in the checkpoint, Transformers 5 builds and saves a placeholder one.

Co-authored-by: Cursor <cursoragent@cursor.com>
Export the Markov head as openvino_markov_head_model.xml with two computations over the same weights: the dense Markov step and the Top-K lattice, whose K (64 by default) is held by a single TopK so the runtime can change it. The lattice takes the rows of several draft blocks packed back to back and delimited by subsequence_begins. MarkovHeadForExport defines this first-order Markov head contract, and the DSpark vanilla head implements its transition scores. The runtime selects the computation, so --dspark-top-k is removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
The tiny DSpark draft runs with the tiny Qwen3 target through the continuous batching pipeline, like the DFlash cases. GenAI without DSpark support runs it as a plain DFlash draft, so the case needs no new version gate. The preview workflow runs the DSpark tests in test_dflash.py and test_genai.py against the nightly GenAI.

Co-authored-by: Cursor <cursoragent@cursor.com>
The custom draft loader pointed config.auto_map at model_patcher.py and loaded it as remote code, so transformers required every package that file imports, diffusers among them. Exporting a DFlash or DSpark draft then failed without the diffusers extra, which the DFlash CI step does not install. Load the DFlash and DSpark export classes directly, as in huggingface#1983; Eagle3 drafts keep the auto_map loader.

Co-authored-by: Ofir Zafrir <ofir.zafrir@intel.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Qwen3OpenVINOConfig declares the Markov head of a DSpark draft as a behavior
  (SUPPORTED_BEHAVIORS, with_behavior, get_model_for_behavior), and
  _get_submodels_and_export_configs adds declared models with a generic loop.
- The head's rt_info is written by _add_dflash_mode_to_rt_info through its
  existing call; _add_dflash_interface_to_rt_info is removed, and so is the
  redundant hidden-state condition in _save_model.
- Eagle3DummyGenerator generates the Markov head inputs, replacing
  DSparkMarkovDummyGenerator.
- The generic CLI export tests cover qwen3_dspark, and the preview CI step,
  renamed to GenAI speculative decoding validation, runs test_exporters_cli.py.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

4 participants