【分享】原地升天!R9700x2 从 Qwen3.8-27B-MXFP4 到 Qwen3.8-Flash-Next IQ4_R
-
-
这是我自己用的测试方法 有兴趣的话跑一跑
decode 测试
5 个固定 prompt,t=0,max_tokens 512,每项 3 轮。decode = (输出 token − 1) ÷ (总时间 − TTFT)。
设置 代码生成 英文技术 中文对话 中文散文 双路代码:同 prompt / 不同 prompt 关思考( --think off)168.5 147.4 126.3 117.8 178.0 / 207.2 默认(开思考) 149.2 140.5 121.7 120.2 147.7 / 169.5 #!/usr/bin/env python3 """统一基准 v2(基于 "SGLang DFLASH 统一基准 v1")— 双方同 prompt、同口径对测,只用 Python 标准库。 与 v1 相同:5 个 prompt、t=0、max_tokens 512、每项 3 轮、 decode = (completion_tokens - 1) / (总时间 - TTFT),TTFT = 第一个 content 或 reasoning 片段到达的时间, 并发 aggregate = 两路 (completion_tokens - 1) 之和 / 两路中较长的 (总时间 - TTFT)。 v2 新增: --think default|off off = 发送 chat_template_kwargs.enable_thinking=false(vLLM / SGLang / 多数 Qwen 服务) --extra-body JSON 其它引擎专用字段,例如 '{"reasoning_effort":"low"}' --api-key KEY 需要鉴权的服务(例如 radiance 设了 --api-key-env) --warmup N 正式计时前的预热请求数(默认 1,不计入结果) 每项输出:平均 decode、最低-最高、平均输出 token 数、思考占比(reasoning 片段 / 全部片段)、TTFT、tok/chunk 思考占比用来确认 --think off 是否真的生效(off 时应为 0 %);开着思考时显示实际有多少是思考内容,不同引擎和模型差别很大。 tok/chunk 取决于服务端怎么分片推流,只能在同一引擎内比较。 用法: python3 uni_bench_v2.py --model <模型名> --base-url http://127.0.0.1:8000 --think off python3 uni_bench_v2.py --model <模型名> --base-url http://127.0.0.1:8000 --think default 请把完整输出贴出来(开头一行包含时间、模型和设置)。""" import argparse, concurrent.futures, json, time, urllib.request P = { "code": "You are an expert Python engineer. Write a complete implementation of a thread-safe LRU cache class with TTL expiration support, including unit tests. Provide only code, no explanation.", "code_b": "You are an expert systems engineer. Write a complete implementation of a lock-free SPSC ring buffer in C++17 with acquire/release memory ordering annotations. Provide only code, no explanation.", "en_tech": "Write a detailed technical explanation of how speculative decoding with a draft model works, covering the draft-verify loop, acceptance criteria, and why acceptance rate varies by domain. Aim for depth and precision.", "cn_chat": "请用中文详细分析一下,为什么家用路由器长期不重启会越来越慢?从内存碎片、连接跟踪表、NAT 状态老化三个角度展开,给出可操作的解决建议。", "cn_essay": "请写一篇关于秋天的散文,描写北平的秋色,要求文笔优美,有细节描写,不少于五百字。", } SCENES = [("代码生成", "code"), ("英文技术论述", "en_tech"), ("中文常规对话", "cn_chat"), ("中文散文", "cn_essay")] def gen(a, prompt): payload = {"model": a.model, "messages": [{"role": "user", "content": prompt}], "max_tokens": a.max_tokens, "temperature": a.temperature, "stream": True, "stream_options": {"include_usage": True}} if a.think == "off": payload["chat_template_kwargs"] = {"enable_thinking": False} payload.update(a.extra) headers = {"Content-Type": "application/json"} if a.api_key: headers["Authorization"] = "Bearer " + a.api_key req = urllib.request.Request(a.base_url.rstrip("/") + "/v1/chat/completions", data=json.dumps(payload).encode(), headers=headers) t0 = time.time(); ttft = None; n_content = 0; n_reason = 0; usage = None try: with urllib.request.urlopen(req, timeout=900) as r: for raw in r: line = raw.decode("utf-8", "replace").strip() if not line.startswith("data:"): continue d = line[5:].strip() if d == "[DONE]": break try: j = json.loads(d) except Exception: continue if j.get("usage"): usage = j["usage"] ch = (j.get("choices") or [{}])[0].get("delta", {}) or {} c = ch.get("content") or "" rc = ch.get("reasoning_content") or ch.get("reasoning") or "" if c or rc: if ttft is None: ttft = time.time() - t0 if rc: n_reason += 1 if c: n_content += 1 except Exception as e: return {"err": str(e)[:120]} total = time.time() - t0 comp = usage.get("completion_tokens", 0) if usage else 0 nch = n_content + n_reason if ttft is None or comp <= 1 or total <= ttft: return {"err": "no tokens" if usage else "no usage in stream (server ignores stream_options.include_usage?)"} return {"comp": comp, "ttft": ttft, "total": total, "dec": (comp - 1) / (total - ttft), "tpc": comp / nch if nch else 0.0, "think": n_reason / nch if nch else 0.0} def avg(xs): return sum(xs) / len(xs) def main(): ap = argparse.ArgumentParser() ap.add_argument("--base-url", default="http://127.0.0.1:8000") ap.add_argument("--model", required=True) ap.add_argument("--rounds", type=int, default=3) ap.add_argument("--max-tokens", type=int, default=512) ap.add_argument("--temperature", type=float, default=0.0) ap.add_argument("--think", choices=["default", "off"], default="default") ap.add_argument("--extra-body", default="{}") ap.add_argument("--api-key", default="") ap.add_argument("--warmup", type=int, default=1) a = ap.parse_args() a.extra = json.loads(a.extra_body) print(f"# 统一基准 v2: {a.model} @ {a.base_url} {time.strftime('%Y-%m-%d %H:%M')} rounds={a.rounds} " f"max_tokens={a.max_tokens} t={a.temperature} think={a.think} extra={json.dumps(a.extra, ensure_ascii=False)}", flush=True) for _ in range(a.warmup): gen(a, "Say hello in one short sentence.") print("\n## 单路(四场景)", flush=True) for name, key in SCENES: rs = [gen(a, P[key]) for _ in range(a.rounds)] ok = [r for r in rs if "err" not in r] errs = [r["err"] for r in rs if "err" in r] if not ok: print(f" {name}: 失败 ({errs[0] if errs else '?'})", flush=True) continue d = [r["dec"] for r in ok] print(f" {name}: decode {avg(d):.1f} t/s ({min(d):.1f}-{max(d):.1f}) | 输出 {avg([r['comp'] for r in ok]):.0f} tok | " f"思考占比 {100 * avg([r['think'] for r in ok]):.0f}% | TTFT {avg([r['ttft'] for r in ok]):.3f}s | " f"tok/chunk {avg([r['tpc'] for r in ok]):.2f}" + (f" | 失败 {len(errs)}" if errs else ""), flush=True) print("\n## 并发 2 路(代码场景)", flush=True) for label, ps in [("同 prompt(前缀缓存命中)", [P["code"]] * 2), ("不同 prompt", [P["code"], P["code_b"]])]: aggs = [] for _ in range(a.rounds): with concurrent.futures.ThreadPoolExecutor(2) as ex: rs = list(ex.map(lambda p: gen(a, p), ps)) rs = [r for r in rs if "err" not in r] if len(rs) < 2: continue aggs.append(sum(r["comp"] - 1 for r in rs) / max(r["total"] - r["ttft"] for r in rs)) if aggs: print(f" {label}: aggregate decode {avg(aggs):.1f} t/s ({min(aggs):.1f}-{max(aggs):.1f}, n={len(aggs)})", flush=True) else: print(f" {label}: 失败", flush=True) if __name__ == "__main__": main()prefill 测试
上下文 4K 16K 32K 64K 128K 192K 冷 prefill(耗时) 1,748(2.3 s) 4,188(4.0 s) 4,559(7.1 s) 4,865(13.3 s) 5,079(25.4 s) 5,102(37.9 s) 增量 2000 token(耗时) 992(2.03 s) 951(2.11 s) 922(2.18 s) 907(2.22 s) 806(2.47 s) 752(2.67 s) #!/usr/bin/env python3 """统一 prefill 基准 v1(uni_bench_v2 的配套脚本)— 双方同文本、同口径对测,只用 Python 标准库。 冷 prefill:每个长度发一条全新的长 prompt(开头带随机 nonce,避免命中前缀缓存),max_tokens=1, 冷 prefill = 服务端返回的 prompt_tokens ÷ 请求总耗时(1 个输出 token,耗时≈TTFT)。 增量 prefill(多轮对话口径,默认开):冷请求之后接一轮新对话 [user: 长上下文] [assistant: 第一轮的回复] [user: 新增约 2000 token + 问题],max_tokens=1, 增量 prefill = (第二轮 prompt_tokens − 第一轮 prompt_tokens) ÷ 第二轮耗时。 这一项同时测了引擎的多轮缓存:如果没命中缓存,会接近重新读整个上下文的耗时。 文本:默认下载 Project Gutenberg 的 Sherlock Holmes(和 llama-benchy 同一本,缓存到本地文件); 下载不了就用 --text-file 指定任意 UTF-8 文本,对测双方要用同一个文件。 长度不够时从不同位置循环取文本;每次请求的起点不同。 用法: python3 uni_prefill.py --model <模型名> --base-url http://127.0.0.1:8000 python3 uni_prefill.py --model <模型名> --lengths 4096 16384 65536 131072 --rounds 2 --followup 2000 请把完整输出贴出来。prompt_tokens 以各自服务端的计数为准(不同 tokenizer 同一段文本的 token 数会略有不同)。""" import argparse, json, os, random, time, urllib.request BOOK_URL = "https://www.gutenberg.org/files/1661/1661-0.txt" QUESTION = "\n\n请用一句话概括上面的内容。" def post(a, messages): payload = {"model": a.model, "messages": messages, "max_tokens": 1, "temperature": 0.0, "stream": False} if a.think == "off": payload["chat_template_kwargs"] = {"enable_thinking": False} payload.update(a.extra) headers = {"Content-Type": "application/json"} if a.api_key: headers["Authorization"] = "Bearer " + a.api_key req = urllib.request.Request(a.base_url.rstrip("/") + "/v1/chat/completions", data=json.dumps(payload).encode(), headers=headers) t0 = time.time() d = json.loads(urllib.request.urlopen(req, timeout=3600).read()) dt = time.time() - t0 u = d.get("usage") or {} msg = (d.get("choices") or [{}])[0].get("message") or {} cached = (u.get("prompt_tokens_details") or {}).get("cached_tokens") return u.get("prompt_tokens", 0), dt, (msg.get("content") or "").strip(), cached def load_text(a): if a.text_file: return open(a.text_file, encoding="utf-8").read() cache = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uni_prefill_book.txt") if not os.path.exists(cache): print(f"# 下载 {BOOK_URL} ...", flush=True) data = urllib.request.urlopen(BOOK_URL, timeout=120).read().decode("utf-8", "replace") open(cache, "w", encoding="utf-8").write(data) return open(cache, encoding="utf-8").read() def piece(text, start, nchars): out = [] while nchars > 0: s = start % len(text) chunk = text[s:s + nchars] out.append(chunk); nchars -= len(chunk); start = 0 return "".join(out) def main(): ap = argparse.ArgumentParser() ap.add_argument("--base-url", default="http://127.0.0.1:8000") ap.add_argument("--model", required=True) ap.add_argument("--lengths", type=int, nargs="+", default=[4096, 16384, 32768, 65536, 131072]) ap.add_argument("--rounds", type=int, default=2) ap.add_argument("--followup", type=int, default=2000, help="增量 prefill 的新增 token 数,0 = 不测") ap.add_argument("--think", choices=["default", "off"], default="default") ap.add_argument("--extra-body", default="{}") ap.add_argument("--api-key", default="") ap.add_argument("--text-file", default="") a = ap.parse_args() a.extra = json.loads(a.extra_body) text = load_text(a) rnd = random.Random() # 校准:这个服务端的 tokenizer 每个 token 大约几个字符 sample = piece(text, 5000, 40000) pt0, _, _, _ = post(a, [{"role": "user", "content": sample + QUESTION}]) cpt = len(sample) / max(pt0 - 20, 1) print(f"# 统一 prefill v1: {a.model} @ {a.base_url} {time.strftime('%Y-%m-%d %H:%M')} rounds={a.rounds} " f"followup={a.followup} think={a.think} extra={json.dumps(a.extra, ensure_ascii=False)} " f"文本 {'--text-file ' + a.text_file if a.text_file else 'Sherlock Holmes'} ({cpt:.2f} 字符/token)", flush=True) print(f"\n{'长度':>8} | {'冷 prompt_tok':>13} {'耗时':>8} {'冷 prefill':>11} | {'增量 tok':>8} {'耗时':>7} {'增量 prefill':>12} {'cached':>8}", flush=True) for L in a.lengths: cold, inc = [], [] for r in range(a.rounds): nonce = "[%016x] " % rnd.getrandbits(64) ctx = nonce + piece(text, rnd.randrange(len(text)), int(L * cpt)) m1 = [{"role": "user", "content": ctx + QUESTION}] p1, t1, reply, _ = post(a, m1) cold.append(p1 / t1) line = f"{L:>8} | {p1:>13} {t1:>7.2f}s {p1 / t1:>8.0f} t/s |" if a.followup > 0: new = piece(text, rnd.randrange(len(text)), int(a.followup * cpt)) m2 = m1 + [{"role": "assistant", "content": reply or "好"}, {"role": "user", "content": new + QUESTION}] p2, t2, _, cached = post(a, m2) inc.append((p2 - p1) / t2) line += f" {p2 - p1:>8} {t2:>6.2f}s {(p2 - p1) / t2:>8.0f} t/s {cached if cached is not None else '-':>8}" print(line, flush=True) s = f"{'平均':>8} | {'':>13} {'':>8} {sum(cold) / len(cold):>8.0f} t/s |" if inc: s += f" {'':>8} {'':>7} {sum(inc) / len(inc):>8.0f} t/s" print(s, flush=True) if __name__ == "__main__": main() -
-
@linkdesu 测一测512k context context offload 去ram strata 唯一的好处就是长上下文几乎没衰减
radiance用qfn也幾乎沒衰減,27B就衰減的很快比不上 sly vllm和paiton vllm,現在的radiance不算是vllm,已經是獨立的引擎和框架了,優點就是啟動不用一分鐘,vllm都是3分鐘起步。
-
-
@paul-hou 双7900XTX 性能上不知道提升怎么样
-
最近用RADIANCE都是用這個模型
https://huggingface.co/frank8465/Swift-1.5-Qwen3.8-Flash-Next-GPTQ-W4A8-Radiance-AMD-R9700
比原廠慢一點,但品質好。@paul-hou 请问未审查的模型有推荐的吗?我现在用的是作者转的https://huggingface.co/StillDeadcode/qwen3.8-next-flash-uncensored-fp8-iq4r-moe
-
-
-
@terry 已经从vllm 分出来了
之前是vllm 的fork
现在完全脱离出来 用c++重写 -
@paul-hou 请问未审查的模型有推荐的吗?我现在用的是作者转的https://huggingface.co/StillDeadcode/qwen3.8-next-flash-uncensored-fp8-iq4r-moe
-
-
最近用RADIANCE都是用這個模型
https://huggingface.co/frank8465/Swift-1.5-Qwen3.8-Flash-Next-GPTQ-W4A8-Radiance-AMD-R9700
比原廠慢一點,但品質好。 -
@linkdesu 测一测512k context context offload 去ram strata 唯一的好处就是长上下文几乎没衰减
radiance用qfn也幾乎沒衰減,27B就衰減的很快比不上 sly vllm和paiton vllm,現在的radiance不算是vllm,已經是獨立的引擎和框架了,優點就是啟動不用一分鐘,vllm都是3分鐘起步。
说的我都心动了,现在是2块7900XTX 在跑QFN 。另两块9700 闲置。
已经采购电源模块准备折腾


-
-
-
-
-
-


本帖参与讨论人数已达 10+,入选热议,作者 +3 积分奖励!