Merge f-dsv41-setup 86ba33845 into f-dsv41-rel (serve: DeepSeek turn without </think> returns its text as content; pack recipe noise token/markov rank; exec bits in packages; console-safe smoke)

This commit is contained in:
Niko1221 committed 2026-10-23 23:00:32 +02:00
commit 0be62ccad7
9 files changed
+124 -16

No files matched your search

+3 -1
View File
@@ -139,7 +139,9 @@ def build(src: Path, backend: str, version: str, target: str, out: Path, *, entr
manifest["notes"] = notes
(staging / ec.MANIFEST).write_text(json.dumps(manifest, indent=1) + "\n", encoding="utf-8")
if os.name != "nt":
(staging / entrypoint).chmod(0o755)
for exe_name in (entrypoint, DSV41_EXE, "strata-vision", "strata-vision-hip"): # the programs setup starts: executable in the tar
if (staging / exe_name).is_file():
(staging / exe_name).chmod(0o755)
ec.verify(ec.read_manifest(staging), full=True)
staging.rename(folder)
except BaseException:
+24 -4
View File
@@ -22,7 +22,7 @@ from __future__ import annotations
import re
from serve.frontend import OutputParser
from serve.frontend import Event, OutputParser
DSML = "|DSML|"
_OPEN = "<" + DSML
@@ -168,6 +168,19 @@ class DsmlOutputParser(OutputParser):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.dsml = DsmlTranslator(self._on_param)
# The 2-bit sets often write the whole answer after <think> and stop (EOS) without </think>: everything would be reasoning and
# the reply empty (P2003 install test, 4 of 4 prompts). The reasoning text is kept here; finish() hands it over as the answer
# when the turn ended on a stop with no answer and no call.
self._reasoning: list[str] = []
self._answered = False
def _watch(self, evs):
for ev in evs:
if ev.kind == "reasoning":
self._reasoning.append(ev.text)
elif ev.kind in ("tool_start", "tool_call") or (ev.kind == "content" and ev.text.strip()):
self._answered = True
return evs
def _on_param(self, tool, name, is_string) -> None:
"""string="true": the value is a string whatever it looks like ("123", "true"), as the template wrote it."""
@@ -186,12 +199,19 @@ class DsmlOutputParser(OutputParser):
self.sdeclared[name] = "string"
def feed(self, delta: str):
return super().feed(self.dsml.feed(delta))
return self._watch(super().feed(self.dsml.feed(delta)))
def finish(self, reason: str | None = None):
rest = self.dsml.flush()
out = super().feed(rest) if rest else []
return out + super().finish(reason)
out = self._watch(super().feed(rest) if rest else [])
out += self._watch(super().finish(reason))
text = "".join(self._reasoning).strip()
if reason in (None, "stop") and text and not self._answered:
# no </think> and no answer: the text IS the answer. A streaming client has already seen it as reasoning and now gets
# it as content; the non-streaming collectors (server.py) drop the identical reasoning.
self._answered = True
out.append(Event("content", text))
return out
@property
def clean(self) -> bool:
+7
View File
@@ -5557,6 +5557,8 @@ def openai_collect(chunks) -> dict:
if last["choices"][0]["finish_reason"] != "tool_calls":
calls = [c for c in calls if _is_json(c["function"]["arguments"])] # a call the output ended inside (#211)
msg = {"role": "assistant", "content": "".join(content) or None}
if "".join(reasoning).strip() and "".join(reasoning).strip() == "".join(content).strip():
reasoning = [] # a turn with no </think> (serve/dsml.py): the text is the answer; it is not also reasoning
if "".join(reasoning):
msg["reasoning_content"] = "".join(reasoning)
if calls:
@@ -5695,6 +5697,11 @@ def anthropic_collect(events) -> dict:
msg["stop_reason"] = e["delta"]["stop_reason"]
msg["stop_sequence"] = e["delta"].get("stop_sequence")
msg["usage"].update(e["usage"])
for i in range(len(blocks) - 1): # a turn with no </think> (serve/dsml.py): the text is the answer; it is not also a thinking block
a, b = blocks[i], blocks[i + 1]
if a.get("type") == "thinking" and b.get("type") == "text" and a.get("thinking", "").strip() == b.get("text", "").strip():
del blocks[i]
break
msg["content"] = blocks
return msg
+50
View File
@@ -345,6 +345,56 @@ class Api(unittest.TestCase):
self.assertEqual(msg["content"].strip(), "Hello!")
self.assertEqual(r["choices"][0]["finish_reason"], "stop")
ANSWER = "17 x 23 = 391. The answer is 391."
def test_no_closing_think_tag_makes_the_text_the_answer(self):
"""The 2-bit sets write the answer after <think> and stop without </think>: it must not arrive as reasoning only (P2003)."""
for effort in (None, "low", "medium", "high"):
with self.subTest(effort=effort, api="openai"):
self.start(self.ANSWER)
status, r = self.chat(**({"reasoning_effort": effort} if effort else {}))
self.assertEqual(status, 200, r)
msg = r["choices"][0]["message"]
self.assertEqual(msg["content"].strip(), self.ANSWER)
self.assertFalse(msg.get("reasoning_content"))
self.assertEqual(r["choices"][0]["finish_reason"], "stop")
with self.subTest(effort=effort, api="anthropic"):
self.start(self.ANSWER)
status, r = self.post("/v1/messages", {"model": "x", "max_tokens": 400,
"messages": [{"role": "user", "content": "hi"}],
**({"output_config": {"effort": effort}} if effort else {})})
self.assertEqual(status, 200, r)
self.assertEqual([b["type"] for b in r["content"]], ["text"])
self.assertEqual(r["content"][0]["text"].strip(), self.ANSWER)
def test_no_closing_think_tag_streaming_ends_with_the_answer_as_content(self):
self.start(self.ANSWER)
status, raw = self.post("/v1/chat/completions", {"messages": [{"role": "user", "content": "hi"}], "max_tokens": 400,
"stream": True}, raw=True)
self.assertEqual(status, 200)
chunks = [json.loads(l[6:]) for l in raw.splitlines() if l.startswith("data: ") and l != "data: [DONE]"]
content = "".join(c["choices"][0]["delta"].get("content") or "" for c in chunks if c["choices"])
self.assertEqual(content.strip(), self.ANSWER)
status, raw = self.post("/v1/messages", {"model": "x", "max_tokens": 400, "stream": True,
"messages": [{"role": "user", "content": "hi"}]}, raw=True)
self.assertEqual(status, 200)
self.assertIn('"text_delta"', raw)
self.assertIn("391", raw)
def test_a_turn_cut_by_max_tokens_is_not_turned_into_an_answer(self):
self.start(self.ANSWER * 30)
status, r = self.chat(max_tokens=8)
msg = r["choices"][0]["message"]
self.assertEqual(r["choices"][0]["finish_reason"], "length")
self.assertFalse(msg.get("content"))
self.assertTrue(msg.get("reasoning_content"))
def test_a_normal_closed_think_is_unchanged(self):
self.start("Reasoning here.</think>The answer.")
status, r = self.chat()
msg = r["choices"][0]["message"]
self.assertEqual((msg["reasoning_content"], msg["content"].strip()), ("Reasoning here.", "The answer."))
def test_thinking_off(self):
self.start("Hello!")
for extra in ({"chat_template_kwargs": {"enable_thinking": False}}, {"reasoning_effort": "none"}):
+24 -8
View File
@@ -706,7 +706,7 @@ DSV41_CONTEXT = 8192 # the context the engine was checked at (4096 and
MULTI_FILE_FAMILIES = {
"deepseek41": {"title": "DeepSeek-V4.1-Flash", "by": "DeepSeek; the 2-bit GGUFs by IST-DAS, UD-Q8_K_XL by Unsloth", "arch": "deepseek41",
"about": "a big model that runs on a PC with a 12 GB card and 64 GB of RAM (the two official 2-bit versions: 257 or "
"274 GB of disk, about 5-7 words a second) or, in Unsloth's 508 GB version, on several graphics cards",
"274 GB of disk, about 5-7 tokens a second) or, in Unsloth's 508 GB version, on several graphics cards",
"hf": hf(DSV41_REPO) + "UD-Q8_K_XL/", "tag": "deepseek-v4.1-flash-", "name": "deepseek-v4.1-flash",
"license": "DeepSeek's model license: https://huggingface.co/unsloth/DeepSeek-V4.1-Flash-1017-GGUF",
"models": {"UD-Q8_K_XL": {"about": "Unsloth Dynamic 8-bit: the experts in MXFP4 as DeepSeek made them, the rest "
@@ -720,7 +720,7 @@ MULTI_FILE_FAMILIES["deepseek41"]["models"] = {
"IST-DAS-2.00bpw": {
"label": "IST-DAS 2.00 bpw",
"about": "the smaller official version: 138 GB of model + 111 GB n-gram table + 8 GB drafter (257 GB of disk). Runs on a PC with "
"a 12 GB card and 40-64 GB of RAM at about 5-7 words a second; quality about 16% above the original model's "
"a 12 GB card and 40-64 GB of RAM at about 5-7 tokens a second; quality about 16% above the original model's "
"perplexity",
"ram_gb": 40, "recipe": "ista", "subdir": "2.00bpw", "profile": "profile-2.00.bin", "hf": hf(DSV41_ISTA_REPO) + "2.00bpw/",
"shards": [DSV41_ISTA_SHARD.format(bpw="2.00bpw", i=i) for i in range(1, 5)],
@@ -4729,7 +4729,7 @@ def get_prebuilt(url_base, gpu, vision, updating=False, toolkit=13) -> Path | No
if not (eng / EXE).exists():
fail("the ready-made engine archive has no " + EXE)
if not WIN:
for x in (EXE, VEXE):
for x in (EXE, VEXE, "strata-dsv41"):
if (eng / x).exists():
(eng / x).chmod(0o755)
ok(f"ready-made engine {meta.get('version', '')} for {', '.join('sm_' + str(a) for a in archs)} (CUDA "
@@ -5638,7 +5638,7 @@ def data_folder(requested: str | None, move: bool = False, yes: bool = True, set
["keep", "move"], "keep", False) == "move"
if held and not move:
for folder in held:
say(f" your model files stay in {folder}; {dest} is used for new downloads")
say(f" Models you already have in {folder} stay there; new downloads go to {dest}.")
if set_default:
save_settings({**settings, "data_dir": str(dest)})
return dest, held
@@ -7288,6 +7288,11 @@ def dsv41_exe(eng: Path) -> Path | None:
if own:
return Path(own) if Path(own).is_file() else None
exe = Path(eng) / ("strata-dsv41.exe" if WIN else "strata-dsv41")
if exe.is_file() and not WIN and not os.access(exe, os.X_OK): # a package copied without its modes (scp / tar from Windows): fix it, do not stop
try:
exe.chmod(0o755)
except OSError:
pass
return exe if exe.is_file() else None
@@ -7338,10 +7343,12 @@ def dsv41_find_shared(a, name: str, folder: Path, shared: Path) -> Path:
def dsv41_pack_cmd(folder: Path, shards: list, engram: Path, meta: Path, dspark: Path, pack: Path) -> list:
"""The pack recipe for an official IST-DAS set - this and nothing else: another one (--skip-tensors / --dense-override + engram_fold)
packs without an error and gives garbage text (P2003, 2026-10-23). --shards names the four files, so an Engram or another
packs without an error and gives garbage text (P2003, 2026-10-23). The drafter's noise token (128799) and markov rank (256) are stated:
the IST-DAS GGUFs do not carry them, and a pack with noise id 0 drafts deep less often (decode ~10% slower with DSpark). --shards names the four files, so an Engram or another
set in the same folder is not read as part of the model."""
return [sys.executable, str(ROOT / "tools" / "dsv41" / "pack_gguf.py"), "--model", str(folder), "--shards", *[Path(n).name for n in shards],
"--engram", str(engram), "--engram-meta", str(meta), "--dspark-gguf", str(dspark), "--out", str(pack)]
"--engram", str(engram), "--engram-meta", str(meta), "--dspark-gguf", str(dspark),
"--dspark-noise-token", "128799", "--dspark-markov-rank", "256", "--out", str(pack)]
def dsv41_engine_env(lib_dirs) -> dict:
@@ -7375,7 +7382,8 @@ def dsv41_smoke(exe: Path, pack: Path, lib_dirs, gpus=None, timeout: int = 5400)
except (OSError, subprocess.TimeoutExpired) as e:
return False, f"the test run did not finish: {e}"
text = ((r.stdout or "") + (r.stderr or "")).strip()
return r.returncode == 0, text[-1500:]
enc = getattr(sys.stdout, "encoding", None) or "utf-8" # the reply is printed by say()/fail(): a Windows console code page cannot show DeepSeek's special tokens
return r.returncode == 0, text[-1500:].encode(enc, "replace").decode(enc, "replace")
def dsv41_cards(gpu: dict, all_cards: list | None, asked: bool) -> list:
@@ -7436,6 +7444,14 @@ def install_dsv41(a, family, gpu, hip, amd, cuda_tk, multi, port, ram, adopted,
if left:
say(f" Download: {left:.0f} GB to fetch (checked against pinned sizes and SHA-256; it resumes if it stops). At 50 MB/s that "
f"is {left * 1e9 / 50e6 / 3600:.1f} hours. Files you already have: --gguf-dir DIR.")
else:
say(" Nothing to download: the files are already here.")
unchecked = [f for f in [*shards, *shared] if f.exists() and f"sha256 {fam['sha256'][f.name][1]}" not in
(f.with_name(f.name + ".done").read_text(encoding="utf-8", errors="replace") if done(f) else "")]
if unchecked:
gb = sum(fam["sha256"][f.name][0] for f in unchecked) / 1e9
say(f" The files already here ({gb:.0f} GB) are checked against their SHA-256 once, now: about {max(1, round(gb / 55))} minutes on a fast "
"drive (4-5 minutes for the 257 GB of 2.00), longer on a slow one. The next runs skip it.")
for w in plan["warnings"]:
if "free disk space" in w:
continue # the disk is a hard limit, said below
@@ -8259,7 +8275,7 @@ def main() -> int:
for mname in ("IST-DAS-2.00bpw", "IST-DAS-2.25bpw", "UD-Q8_K_XL"):
ds = dsv41_fit.plan(cards_ok, ram, None, model=mname)
tail = {"fits": f"fits ({ds['experts_in_vram']:,} of {ds['experts_total']:,} experts in graphics memory, the rest on the CPU)",
"tight": ("runs, with the experts RAM cannot hold read from the SSD (about 5-7 words a second measured)"
"tight": ("runs, with the experts RAM cannot hold read from the SSD (about 5-7 tokens a second measured)"
if ds.get("variant") else "fits, but RAM is tight"),
"spills": (f"works but slow: less RAM than it was measured with (46-64 GB)" if ds.get("variant")
else f"about {ds['spill_gb']:.0f} GB of its experts would be read from the SSD (slow)"),
+1 -1
View File
@@ -227,7 +227,7 @@ def _plan_ista(model: str, cards: list[dict] | None, ram_gb: float | None, disk_
elif float(ram_gb) >= v["ram_ok_gb"]:
out["verdict"] = "tight"
lines.append(f"This computer has {ram_gb:.0f} GB of RAM: Strata keeps the experts you use most in RAM (it sizes this itself) and "
"reads the rest from the SSD. Measured: about 4.7 words a second on 64 GB of RAM and a 12 GB card, about 7 on "
"reads the rest from the SSD. Measured: about 4.7 tokens a second on 64 GB of RAM and a 12 GB card, about 7 on "
"46 GB with a faster drive. More RAM or a faster NVMe drive makes it quicker.")
else:
out["verdict"] = "spills"
+12
View File
@@ -90,6 +90,18 @@ class Manifest(unittest.TestCase):
folder = self.package()
self.assertNotIn("link.so", {f[0] for f in ec.read_manifest(folder).files})
def test_linux_programs_are_executable_in_the_tar_even_from_a_windows_packer(self):
import feed_tool
folder = self.package()
elf = bytes([0x7f]) + b"ELF" + bytes(60)
for name in ("strata-dsv41", "strata-vision"): # ELF programs without execute bits (copied over from Windows)
(folder / name).write_bytes(elf)
os.chmod(folder / name, 0o644)
(folder / "libx.so").write_bytes(elf)
if WINDOWS: # a Linux package packed on Windows: the ELF header decides
self.assertEqual(feed_tool._mode(folder / "strata-dsv41", "strata-dsv41", "strata"), 0o755)
self.assertEqual(feed_tool._mode(folder / "strata-vision", "strata-vision", "strata"), 0o755)
def test_strata_dsv41_travels_beside_the_engine_and_can_be_required(self):
dsv = "strata-dsv41.exe" if WINDOWS else "strata-dsv41"
with self.assertRaises(SystemExit):
+1 -1
View File
@@ -1247,7 +1247,7 @@ class Strata:
return (f"slow here: this computer has {pl['ram_gb']:.0f} GB of RAM and the set was measured with 46-64 GB, so most of its "
"experts are read from the SSD; it can still be installed")
if v == "tight":
return ("tight: runs, with the experts RAM cannot hold read from the SSD (about 5-7 words a second measured on 46-64 GB "
return ("tight: runs, with the experts RAM cannot hold read from the SSD (about 5-7 tokens a second measured on 46-64 GB "
"of RAM and a 12 GB card)")
if v == "fits":
return (f"fits: {pl['experts_in_vram']:,} of {pl['experts_total']:,} experts in graphics memory, the rest cached in RAM "
+2 -1
View File
@@ -360,7 +360,8 @@ class OfficialSets(unittest.TestCase):
cmd = setup.dsv41_pack_cmd(folder, names, Path("/s/engram-IQ4_NL.gguf"), Path("/s/meta.gguf"), Path("/s/dspark.gguf"), pack)
self.assertTrue(cmd[1].endswith("pack_gguf.py"))
self.assertEqual(cmd[2:], ["--model", str(folder), "--shards", *names, "--engram", str(Path("/s/engram-IQ4_NL.gguf")),
"--engram-meta", str(Path("/s/meta.gguf")), "--dspark-gguf", str(Path("/s/dspark.gguf")), "--out", str(pack)])
"--engram-meta", str(Path("/s/meta.gguf")), "--dspark-gguf", str(Path("/s/dspark.gguf")),
"--dspark-noise-token", "128799", "--dspark-markov-rank", "256", "--out", str(pack)])
for bad in ("--skip-tensors", "--dense-override", "--override-experts", "--tq-experts"): # these give garbage text on the official sets
self.assertNotIn(bad, cmd)