Auto picks DFlash2 for Unsloth's quantizations of Qwen3.8-Flash-Next

DF2-METAL found that SUPPORTED_FAMILIES and the model-name pattern left Unsloth out, so UD-Q4_K_XL and UD-IQ4_XS (the same Qwen3.8-Flash-Next weights, and the case the
DFlash2 numbers on Strix, R9700 and M5 Max were measured on) started the MTP everywhere.
- strata_control/drafting.py: "unsloth" is a supported family; the name pattern also matches qwen3.8-flash-next-unsloth-<model> (what setup.py and metal/setup_mac.py write:
  f"{family name}-{model.lower()}"). Swift (UkisAI's fine-tune), Coder (ISTA-DASLab's coding version) and NVFP4 stay on the MTP. The stale "Unsloth" in the "another model"
  comment is gone.
- The app's two copies of the rule follow (apps/host/server/native-settings.mjs, src/lib/drafting.ts): the Drafting dropdown and the memory estimate treat Unsloth like Qwen.
- Tests: Unsloth names on cuda, hip, metal and sycl resolve to DFlash2 alone with the vocabulary; Swift, Coder and NVFP4 names stay MTP with their arguments untouched; the names
  are read from setup.py and metal/setup_mac.py; the terminal flow gets the Drafting step for Unsloth; tests/drafting.test.mjs.
This commit is contained in:
Niko1221 committed 2026-11-02 11:28:06 +01:00
1 parent 3342e06444
commit d55138e95a
6 files changed
+122 -18

No files matched your search

+3 -3
View File
@@ -87,9 +87,9 @@ export function setSpeedSwitch(env,id,on){
export function speedSwitchRows(env,context){return SPEED_SWITCHES.filter(s=>speedSwitchApplies(s.id,context)).map(s=>({...s,on:speedSwitchIsOn(env,s.id)}));}
/** Settings the app itself applies when it starts a model (they are not read from the engine's source, so the engine's text never says it supports them). */
export const APP_OWNED_FIELDS=['mtp','batchMtp','drafting','ds_experts'];
/** The families that have a Drafting choice and the model names the DFlash2 drafter was made for (strata_control/drafting.py SUPPORTED_FAMILIES, _SUPPORTED_NAME). */
export const DRAFTING_FAMILIES=['qwen','gyro'];
const DFLASH2_MODEL=/^qwen3\.8-flash-next(-gyro-[a-z0-9._-]+|-(q|iq|tq)\d[a-z0-9_.]*)?$/i;
/** The families that have a Drafting choice and the model names the DFlash2 drafter was made for (strata_control/drafting.py SUPPORTED_FAMILIES, _SUPPORTED_NAME): Qwen3.8-Flash-Next, Gyro and Unsloth's quantizations of the same weights; not Swift, Coder or NVFP4. */
export const DRAFTING_FAMILIES=['qwen','gyro','unsloth'];
const DFLASH2_MODEL=/^qwen3\.8-flash-next(-gyro-[a-z0-9._-]+|-unsloth-[a-z0-9._-]+|-(q|iq|tq)\d[a-z0-9_.]*)?$/i;
/** The drafter a choice means (one per run; strata_control/drafting.py choice_of): Auto is DFlash2 where the engine has it (`served`, Control's word) and the model is one it was made for, else the MTP; off and mtp are what they say. `model` is a run config's model name or a family. */
export function resolveDrafter(choice,model,served){
const c=String(choice??'auto').toLowerCase();if(c==='off'||c==='mtp')return c;
+2 -2
View File
@@ -1,10 +1,10 @@
/**
* The Drafting choice the Models screen offers for a model. It exists only where there is something to choose: DFlash2 is for Qwen3.8-Flash-Next
* only (the qwen and gyro families), and only while this computer's engine can run it: strata_control/drafting.py hardware_support (Control reports it as
* only (the qwen, gyro and unsloth families: Qwen3.8-Flash-Next, its Gyro build and Unsloth's quantizations of the same weights), and only while this computer's engine can run it: strata_control/drafting.py hardware_support (Control reports it as
* `capabilities.dflash2`; Metal and Intel engines and PCs without a supported card have none). Every other model has no dropdown at all: DeepSeek has no
* MTP (its own DSpark drafter is chosen by the engine), and the other families have the MTP and nothing to pick against it (Settings > Draft layer turns it off).
*/
export const DRAFTING_FAMILIES = ['qwen', 'gyro'];
export const DRAFTING_FAMILIES = ['qwen', 'gyro', 'unsloth'];
/** The drafter a choice means for a family (one per run; strata_control/drafting.py choice_of): Auto is DFlash2 where the engine has it (`served`) and the family is one it was made for, else the MTP. */
export function resolveDrafter(choice: string | null | undefined, family: string | null | undefined, served: boolean | null | undefined): 'dflash2' | 'mtp' | 'off' {
+10 -4
View File
@@ -109,13 +109,13 @@ test('every sentence of the Drafting choice is in the catalog in all nine langua
}
});
test('the Drafting dropdown exists only where there is something to choose: Qwen3.8-Flash-Next (qwen, gyro) on a computer whose engine can run DFlash2', async () => {
test('the Drafting dropdown exists only where there is something to choose: Qwen3.8-Flash-Next (qwen, gyro, unsloth) on a computer whose engine can run DFlash2', async () => {
const { transform } = await import('esbuild');
const code = (await transform(read('../src/lib/drafting.ts'), { loader: 'ts', format: 'esm' })).code;
const { draftingChoices } = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
const all = ['auto', 'mtp', 'dflash2', 'off'];
// the engine here cannot run it (Metal, Intel, no card), or Control has not answered yet: no dropdown for anyone
for (const family of ['qwen', 'gyro', 'coder', 'swift', 'deepseek41', undefined]) {
for (const family of ['qwen', 'gyro', 'unsloth', 'coder', 'swift', 'deepseek41', undefined]) {
assert.equal(draftingChoices(family, false), null, `${family}, not served`);
assert.equal(draftingChoices(family, undefined), null, `${family}, Control has not answered`);
assert.equal(draftingChoices(family, null), null);
@@ -123,7 +123,8 @@ test('the Drafting dropdown exists only where there is something to choose: Qwen
// it can: only the models it is for. DeepSeek has no MTP (its DSpark drafter is chosen by the engine); the other families have the MTP and nothing to pick against it.
assert.deepEqual(draftingChoices('qwen', true), all);
assert.deepEqual(draftingChoices('gyro', true), all);
for (const family of ['coder', 'swift', 'unsloth', 'nvfp4', 'qwen36', 'deepseek41', undefined]) assert.equal(draftingChoices(family, true), null, `${family}, served`);
assert.deepEqual(draftingChoices('unsloth', true), all, "Unsloth's quantizations are the same weights (DFlash2 measured on UD-Q4_K_XL)");
for (const family of ['coder', 'swift', 'nvfp4', 'qwen36', 'deepseek41', undefined]) assert.equal(draftingChoices(family, true), null, `${family}, served`);
// the app learns it from Control, once, and false until Control answers
const host = read('../server/native-control.mjs');
assert.match(host, /dflash2:dflash2Served===true,dflash2Support\}/);
@@ -163,12 +164,17 @@ test('one drafter per run: Auto is DFlash2 where the engine has it, the same wor
const code = (await transform(read('../src/lib/drafting.ts'), { loader: 'ts', format: 'esm' })).code;
const ts = await import(`data:text/javascript;base64,${Buffer.from(code).toString('base64')}`);
const host = await import('../server/native-settings.mjs');
for (const [choice, family, served] of [['auto', 'qwen', true], [undefined, 'gyro', true], ['auto', 'qwen', false], ['auto', 'coder', true], ['mtp', 'qwen', true], ['dflash2', 'qwen', true], ['off', 'qwen', true], ['dflash2', 'coder', true]]) {
for (const [choice, family, served] of [['auto', 'qwen', true], [undefined, 'gyro', true], ['auto', 'unsloth', true], ['auto', 'swift', true], ['auto', 'nvfp4', true], ['auto', 'qwen', false], ['auto', 'coder', true], ['mtp', 'qwen', true], ['dflash2', 'qwen', true], ['off', 'qwen', true], ['dflash2', 'coder', true]]) {
assert.equal(ts.resolveDrafter(choice, family, served), host.resolveDrafter(choice, family, served), `${choice} ${family} ${served}`);
}
assert.equal(ts.resolveDrafter('auto', 'qwen', true), 'dflash2');
assert.equal(ts.resolveDrafter('auto', 'qwen', null), 'mtp', 'Control has not answered: the MTP');
assert.equal(host.resolveDrafter('auto', 'Qwen3.8-Flash-Next-iq3_s', true), 'dflash2', 'a run config model name works too');
// the names setup.py writes (f"{family name}-{model.lower()}"): Unsloth's quantizations are DFlash2's; Swift, Coder and NVFP4 are not
for (const name of ['qwen3.8-flash-next-unsloth-ud-q4_k_xl', 'qwen3.8-flash-next-unsloth-ud-iq4_xs', 'qwen3.8-flash-next-unsloth-q8_0', 'qwen3.8-flash-next-gyro-gyro-s']) assert.equal(host.resolveDrafter('auto', name, true), 'dflash2', name);
for (const name of ['qwen3.8-flash-next-coder-iq1_m', 'qwen3.8-flash-next-nvfp4-nvfp4', 'swift-1.5-iq2_xs', 'qwen3.8-flash-next-unsloth', 'qwen3.6-35b-a3b-ud-q4_k_xl']) assert.equal(host.resolveDrafter('auto', name, true), 'mtp', name);
assert.equal(ts.resolveDrafter('auto', 'unsloth', true), 'dflash2');
assert.equal(host.resolveDrafter('auto', 'unsloth', true), 'dflash2');
});
test('a new install fetches only the drafter it will use; the fit and the slot estimates use the drafter chosen', async () => {
+11 -6
View File
@@ -9,11 +9,11 @@ strata_control/draft_layer.py), so a setup run that rewrites the engine argument
``--draft dflash2``) starts with the one chosen.
dflash2 ``--draft dflash2 --draft-model <pack> --draft-fallback none --mtp-draft-vocab <file> --spec 6 --draft-conf 0.35`` and no ``--mtp``: the DFlash2
block drafter (incoai/Qwen3.8-Flash-Next-DFlash2, Q4_0 pack) for Qwen3.8-Flash-Next and Gyro, with the window and confidence that measured best
block drafter (incoai/Qwen3.8-Flash-Next-DFlash2, Q4_0 pack) for Qwen3.8-Flash-Next, Gyro and Unsloth's quantizations, with the window and confidence that measured best
(RESULTS-DRAFT.md section 4); a ``--spec`` or ``--draft-conf`` the person set by hand (``user_args``) stays
mtp the model's own draft layer (``--mtp <folder>``) and none of the DFlash2 flags
off no draft layer and no DFlash2 (the same as ``"mtp": false``: the engine still looks ahead in the text with ``--spec``)
auto DFlash2 wherever the engine has it (DFLASH2_BACKENDS) and the model is Qwen3.8-Flash-Next or Gyro, the MTP everywhere else
auto DFlash2 wherever the engine has it (DFLASH2_BACKENDS) and the model is Qwen3.8-Flash-Next, Gyro or an Unsloth quantization, the MTP everywhere else
Recommend, never force: a choice that cannot be applied (DFlash2 in a layer split or with several requests at once, no drafter file yet) does not stop the
start. The other drafter runs if it is on this computer, else none, and ``Decision.note`` says why in one short line. Nothing is loaded twice and the
@@ -227,9 +227,14 @@ SLOT_BASE_MIB = 3800 # dense weights and the engine's fixed buffers beyo
SLOT_RESERVE_MIB = 700 # the engine's default --vram-reserve-mib
DEFAULT_CONTEXT = 131072 # --max-context when the arguments name none (the 128K default)
#: model families (strata launch --family) the drafter was trained for and measured on: Qwen3.8-Flash-Next and its Gyro build.
SUPPORTED_FAMILIES = ("qwen", "gyro")
_SUPPORTED_NAME = re.compile(r"^qwen3\.8-flash-next(-gyro-[a-z0-9._-]+|-(q|iq|tq)\d[a-z0-9_.]*)?$", re.IGNORECASE)
#: model families (strata launch --family) the drafter was trained for and measured on: Qwen3.8-Flash-Next, its Gyro build and Unsloth's quantizations of the same weights
#: (UD-IQ4_XS, UD-Q4_K_XL...; DFlash2 numbers measured on UD-Q4_K_XL on Strix, R9700 and M5 Max). Not Swift (UkisAI's fine-tune), Coder (ISTA-DASLab's coding version) or NVFP4:
#: no DFlash2 acceptance numbers for them, they start the MTP.
SUPPORTED_FAMILIES = ("qwen", "gyro", "unsloth")
#: The model_name setup.py (and metal/setup_mac.py) write: f"{family name}-{model.lower()}": qwen3.8-flash-next-iq3_s, qwen3.8-flash-next-gyro-gyro-s,
#: qwen3.8-flash-next-unsloth-ud-q4_k_xl. (Coder is qwen3.8-flash-next-coder-iq1_m, NVFP4 qwen3.8-flash-next-nvfp4-nvfp4: neither matches.) Mirrored in
#: apps/host/server/native-settings.mjs; tests/drafting.test.mjs keeps the two the same.
_SUPPORTED_NAME = re.compile(r"^qwen3\.8-flash-next(-gyro-[a-z0-9._-]+|-unsloth-[a-z0-9._-]+|-(q|iq|tq)\d[a-z0-9_.]*)?$", re.IGNORECASE)
def supported_model(model_name: str | None = None, family: str | None = None) -> bool:
@@ -621,7 +626,7 @@ def apply(cfg: dict[str, Any], *, klass: str = "other", data_dir: str | Path | N
`engine` is the engine program that will start (None: not known, the usual flags): an engine that only takes `--mtp DIR` for the vocabulary gets that
(still without the MTP layer), and an engine without `--draft-fallback` gets no DFlash2 flag at all (the MTP, said in one line)."""
args = [str(a) for a in cfg.get("args") or []]
if not supported_model(cfg.get("model_name"), family): # another model: its drafting is not this choice's business (DeepSeek, Swift, Coder, Qwen3.6, Unsloth...)
if not supported_model(cfg.get("model_name"), family): # another model: its drafting is not this choice's business (DeepSeek, Swift, Coder, NVFP4, Qwen3.6...)
asked = normalize(cfg.get("drafting"))
if asked == "dflash2":
cfg["args"] = strip_draft_flags(args)
+94 -2
View File
@@ -43,11 +43,11 @@ class AutoAndTheBackends(unittest.TestCase):
"""Owner 2026-11-02: DFlash2 is the primary drafter on every system whose engine has it (CUDA, HIP, Metal, Intel SYCL since the DF2-SYCL engine)."""
self.assertEqual(drafting.DFLASH2_BACKENDS, ("cuda", "hip", "metal", "sycl"))
for backend in ("cuda", "hip", "metal", "sycl"):
for family in ("qwen", "gyro"):
for family in ("qwen", "gyro", "unsloth"):
self.assertEqual(drafting.choice_of("auto", family=family, backend=backend), "dflash2", (backend, family))
self.assertEqual(drafting.choice_of("auto", family="qwen", backend=None), "mtp", "an unknown computer is the MTP")
self.assertEqual(drafting.choice_of("auto", family="qwen", backend="cpu"), "mtp", "an engine without the code is the MTP")
for family in ("coder", "swift", "unsloth", "nvfp4", "deepseek41"):
for family in ("coder", "swift", "nvfp4", "deepseek41"):
self.assertEqual(drafting.choice_of("auto", family=family, backend="cuda"), "mtp", family)
def test_intel_runs_dflash2_and_a_build_without_the_code_does_not(self):
@@ -369,6 +369,98 @@ class EngineArguments(unittest.TestCase):
self.assertIn('o.draft_fallback != "mtp"', src)
class UnslothAndTheOtherFlashNextFiles(unittest.TestCase):
"""DF2-METAL found that Auto never picked DFlash2 for Unsloth's quantizations: they are the same Qwen3.8-Flash-Next weights and the DFlash2 numbers (Strix, R9700,
M5 Max) were measured on UD-Q4_K_XL. Swift (UkisAI's fine-tune) and Coder (ISTA-DASLab's coding version) are fine-tunes with no acceptance numbers: the MTP.
The names are the ones setup.py and metal/setup_mac.py write: f"{family name}-{model.lower()}"."""
UNSLOTH = ("qwen3.8-flash-next-unsloth-ud-q4_k_xl", "qwen3.8-flash-next-unsloth-ud-iq4_xs", "qwen3.8-flash-next-unsloth-ud-q5_k_xl",
"qwen3.8-flash-next-unsloth-ud-q6_k_xl", "qwen3.8-flash-next-unsloth-q8_0")
FLASH_NEXT = ("qwen3.8-flash-next", "qwen3.8-flash-next-q2_0", "qwen3.8-flash-next-iq2_xs", "qwen3.8-flash-next-iq3_xxs", "qwen3.8-flash-next-iq3_s",
"qwen3.8-flash-next-gyro-gyro-s", "qwen3.8-flash-next-gyro-gyro-m")
OTHERS = ("qwen3.8-flash-next-coder-iq1_m", "qwen3.8-flash-next-nvfp4-nvfp4", "qwen3.8-flash-next-nvfp4-nvfp4-nvidia", "qwen3.8-flash-next-nvfp4-nvfp4-qad",
"swift-1.5-iq2_xs", "swift-1.5-q4_0", "qwen3.6-35b-a3b-ud-q4_k_xl", "deepseek-v4.1-flash", "qwen3.8-flash-next-unsloth", "qwen3.8-flash-next-unslothx-q8_0")
HARDWARE = (("cuda", ["sm_120"]), ("hip", ["gfx1151"]), ("metal", None), ("sycl", ["8086"]))
def test_the_names_the_setups_write(self):
for name in self.UNSLOTH + self.FLASH_NEXT:
self.assertTrue(drafting.supported_model(name), name)
self.assertTrue(drafting.supported_model(name.upper()), name.upper())
for name in self.OTHERS:
self.assertFalse(drafting.supported_model(name), name)
self.assertTrue(drafting.supported_model(None, "unsloth"))
for family in ("swift", "coder", "nvfp4", "qwen36", "deepseek41"):
self.assertFalse(drafting.supported_model(None, family), family)
self.assertEqual(drafting.SUPPORTED_FAMILIES, ("qwen", "gyro", "unsloth"))
def test_the_names_are_the_ones_setup_writes(self):
"""Read from the code, not from memory: the family names and the way setup.py and metal/setup_mac.py make the run config's model_name."""
setup = (ROOT / "setup.py").read_text(encoding="utf-8")
mac = (ROOT / "metal" / "setup_mac.py").read_text(encoding="utf-8")
self.assertIn('"name": "qwen3.8-flash-next-unsloth"', setup)
self.assertIn('"name": "qwen3.8-flash-next-coder"', setup)
self.assertIn('"name": "qwen3.8-flash-next-nvfp4"', setup)
self.assertGreaterEqual(setup.count("\"model_name\": f\"{fam['name']}-{model.lower()}\""), 3)
self.assertIn("f\"{fam['name']}-{model.lower()}\"", mac)
for model in ("UD-IQ4_XS", "UD-Q4_K_XL", "UD-Q5_K_XL", "UD-Q6_K_XL", "Q8_0"):
self.assertIn(f'"{model}": {{"about"', setup)
self.assertTrue(drafting.supported_model(f"qwen3.8-flash-next-unsloth-{model.lower()}"), model)
def run_config(self, name, tmp, mode=None):
data = Path(tmp)
mtp = data / "mtp" / "rt"
mtp.mkdir(parents=True)
(mtp / "experts.bin").write_bytes(b"e")
(mtp / "draft_vocab.bin").write_bytes(b"v")
pack = drafting.pack_path(data)
pack.parent.mkdir(parents=True)
pack.write_bytes(b"x" * (2 << 20))
args = ["--pack", str(data / "packs" / "x"), "--native", "/d/m.gguf", "--mtp", str(mtp), "--spec", "4", "--spec-min-p", "0.5"]
cfg = {"model_name": name, "args": args, **({} if mode is None else {"drafting": mode})}
return cfg, data, mtp
def test_unsloth_resolves_to_dflash2_alone_with_the_vocabulary_on_every_backend(self):
for backend, archs in self.HARDWARE:
for name in self.UNSLOTH:
with tempfile.TemporaryDirectory() as tmp:
cfg, data, mtp = self.run_config(name, tmp)
decision = drafting.apply(cfg, data_dir=data, backend=backend, archs=archs)
args = cfg["args"]
self.assertEqual((decision.drafter, decision.note), ("dflash2", ""), (backend, name))
self.assertEqual(args[args.index("--draft") + 1], "dflash2")
self.assertEqual(args[args.index("--draft-fallback") + 1], "none")
self.assertEqual(args[args.index("--mtp-draft-vocab") + 1], str(mtp / "draft_vocab.bin"), "the vocabulary the engine needs")
for flag in ("--mtp", "--spec-min-p"):
self.assertNotIn(flag, args, (backend, name, flag))
self.assertEqual(args[args.index("--spec") + 1], "6")
def test_swift_coder_and_nvfp4_stay_on_the_mtp(self):
for backend, archs in self.HARDWARE:
for name in ("qwen3.8-flash-next-coder-iq1_m", "qwen3.8-flash-next-nvfp4-nvfp4", "swift-1.5-iq2_xs"):
for mode in (None, "auto", "dflash2"):
with tempfile.TemporaryDirectory() as tmp:
cfg, data, mtp = self.run_config(name, tmp, mode)
original = list(cfg["args"])
decision = drafting.apply(cfg, data_dir=data, backend=backend, archs=archs)
self.assertEqual(decision.drafter, "mtp", (backend, name, mode))
self.assertEqual(cfg["args"], original, "its own MTP arguments, untouched")
self.assertNotIn("--draft", cfg["args"])
if mode == "dflash2":
self.assertEqual(decision.note, drafting.NOT_SUPPORTED)
def test_by_family_the_terminal_and_the_app_agree(self):
for backend, _ in self.HARDWARE:
self.assertEqual(drafting.choice_of("auto", family="unsloth", backend=backend), "dflash2", backend)
for family in ("swift", "coder", "nvfp4"):
self.assertEqual(drafting.choice_of("auto", family=family, backend=backend), "mtp", (backend, family))
# the app's own copies of the rule (the host and the page) name the same families and the same names
host = (ROOT / "apps" / "host" / "server" / "native-settings.mjs").read_text(encoding="utf-8")
page = (ROOT / "apps" / "host" / "src" / "lib" / "drafting.ts").read_text(encoding="utf-8")
self.assertIn("DRAFTING_FAMILIES=['qwen','gyro','unsloth']", host)
self.assertIn("DRAFTING_FAMILIES = ['qwen', 'gyro', 'unsloth']", page)
self.assertIn(r"-unsloth-[a-z0-9._-]+", host)
class TheInstalledEngineDecidesTheFlags(unittest.TestCase):
"""GUI-FB3 item 2. Gyro-S with Auto sent `--mtp-draft-vocab FILE` and no `--mtp` to the engine 0.1.55.2 (f-0291-r5070-df2), which only takes `--mtp DIR` for the
vocabulary and ended with exit code 2. The app reads what the engine program takes (it is never started for this) and sends only that."""
+2 -1
View File
@@ -364,7 +364,8 @@ class TheTerminalStep(unittest.TestCase):
self.assertIn("drafting", flow)
self.assertEqual(flow.index("drafting"), flow.index("pictures") + 1)
self.assertIn("drafting", self.flow("gyro", ("hip", ["gfx1151"])))
for family in ("deepseek41", "swift", "coder", "unsloth", "nvfp4", "qwen36", None):
self.assertIn("drafting", self.flow("unsloth", ("cuda", ["sm_120"])))
for family in ("deepseek41", "swift", "coder", "nvfp4", "qwen36", None):
self.assertNotIn("drafting", self.flow(family, ("cuda", ["sm_120"])), family)
self.assertIn("drafting", self.flow("qwen", ("sycl", [])))
for hardware in (("cpu", []), (None, [])):