DeepSeek: 5 experts per word by default on a single RDNA4 card (R9700, RX 9000); --experts N still wins

One R9700 holds under a fifth of a 2.25 bpw set, so the CPU lane and the SSD set the speed, as on the 16 GiB cards and the multi-card RDNA4 rule that already take 5.
1 x R9700 on p620 (125 GB RAM), 2.25 bpw, frozen expert profile, 5 interleaved pairs, 5 against 6 experts: decode 15.24 against 13.08 tok/s (+16%, 5 of 5 pairs), a scripted agent loop -24% wall time (5 of 5), the long-read loop -12% (5 of 5). KL against the 6-expert run 0.030 (6 against 6: 0.008); against the original model 0.1106 against 0.1067-0.1071. quality027 quick: 18 of 19 with 5 experts and with 6, the same task flagged in both.
ds4_experts_rule.hpp: experts_per_word() takes single_rdna4 (the engine passes rdna4() with one device); DS4_DEFAULTS=026 gives the model's 6 back. The Terminal's Speed words (strata launch --plan and screens), the app's Speed label (arch added to RunCard) and docs/MODELS.md say the same. Several RDNA4 cards keep the existing rule (they must hold half of the experts); the screens still do not mirror that one.
This commit is contained in:
Niko1221 committed 2026-11-01 13:04:39 +01:00
1 parent 243fdb213c
commit 386d2e4416
11 files changed
+68 -13

No files matched your search

+6 -2
View File
@@ -2,18 +2,22 @@ import type { RunCard } from './native-fit';
/**
* DeepSeek's Speed default, said the way the engine decides it (`--experts auto`, src/dsv41/ds4_experts_rule.hpp): Faster (5 experts a word) when
* a card in use is an integrated / unified-memory GPU (Strix Halo, gfx1151: the model shares the PC's memory), or when the cards in use total
* 16 GiB or less; otherwise Full quality (6). The app never decides: the engine does; this only names the same default beside the choice.
* a card in use is an integrated / unified-memory GPU (Strix Halo, gfx1151: the model shares the PC's memory), when the one card in use is an
* RDNA4 Radeon (gfx1200 / gfx1201: the CPU lane does most of the experts), or when the cards in use total 16 GiB or less; otherwise Full quality (6). The app never decides: the engine does; this only names the same default beside the choice.
* The total is compared in whole MiB (a 16 GB card reports 16376 MiB = 15.99 GiB and counts as 16 GiB or less).
*/
export const FASTER_MIB = 16 * 1024;
export const isIntegrated = (card: RunCard) => !!(card.uma || card.integrated || (card.unified_gb ?? 0) > 0);
/** One RDNA4 Radeon (gfx1200 / gfx1201: Radeon AI PRO R9700, RX 9070 / 9060). */
export const isRdna4 = (card: RunCard) => (card.vendor ?? '').toLowerCase() === 'amd' && /^gfx120[01]/i.test(card.arch ?? '');
export function fasterByDefault(cards: readonly RunCard[] | null | undefined): boolean {
const used = (cards ?? []).filter(card => card.usable !== false);
if (!used.length) return false;
if (used.some(isIntegrated)) return true;
if (used.length === 1 && isRdna4(used[0])) return true;
const mib = used.reduce((sum, card) => sum + Math.round((card.vram_gb ?? 0) * 1024), 0);
return mib > 0 && mib <= FASTER_MIB;
}
+1 -1
View File
@@ -102,7 +102,7 @@ export function recommendedVersions<V extends { sourceFamily: string; model: str
}
/** A card as the installer's hardware report lists it. */
export interface RunCard { index: number; name: string; vram_gb: number; usable?: boolean; vendor?: string; uma?: boolean; integrated?: boolean; unified_gb?: number }
export interface RunCard { index: number; name: string; vram_gb: number; usable?: boolean; vendor?: string; arch?: string; uma?: boolean; integrated?: boolean; unified_gb?: number }
/**
* The cards a model can be placed on: the ones this engine can drive (strata_control/gpu_fit.py `usable_cards` and `for_vendor`, same rule).
+12
View File
@@ -28,6 +28,18 @@ test('discrete cards: Faster at 16 GiB or less in total, Full quality above', ()
assert.equal(fasterByDefault(null), false);
});
test('one RDNA4 Radeon (1 x R9700, 32 GB) defaults to Faster: the CPU lane does most of the experts', () => {
const r9700 = (extra = {}) => card(32, { vendor: 'amd', arch: 'gfx1201', ...extra });
assert.equal(fasterByDefault([r9700()]), true);
assert.equal(fasterByDefault([card(16, { vendor: 'amd', arch: 'gfx1200' })]), true);
assert.equal(fasterByDefault([r9700({ arch: 'gfx1100' })]), false, 'an RX 7000 card with 32 GB is not RDNA4');
assert.equal(fasterByDefault([r9700({ vendor: 'nvidia' })]), false);
assert.equal(fasterByDefault([card(32)]), false, 'no arch in the report: the old rule');
assert.equal(fasterByDefault([r9700(), r9700({ index: 1 })]), false, 'two cards are the other engine rule (they must hold half of the experts)');
assert.equal(fasterByDefault([r9700(), r9700({ index: 1, usable: false })]), true, 'a card the engine cannot use does not count');
assert.deepEqual(speedOptions([r9700()]).map(o => o.label), ['Default (Faster on this PC)', 'Faster (default on this PC)', 'Full quality']);
});
test('the labels say "(default on this PC)" beside the choice that is the default, once', () => {
const faster = speedOptions([card(12)]).map(o => o.label);
assert.deepEqual(faster, ['Default (Faster on this PC)', 'Faster (default on this PC)', 'Full quality']);
+1 -1
View File
@@ -93,7 +93,7 @@ strata launch --family deepseek41 --model ist-das-2.00bpw
**Speed: Faster or Full quality.** The app asks, and the Terminal takes `--speed faster|full` (or `--full-quality`). Faster uses 5 of the
model's 6 experts per word, a small quality change that is usually not noticeable. It is the default on a PC with 16 GB of graphics
memory or less. Full quality uses all 6. It needs DeepSeek engine 0.1.50 or newer, which the app and `strata launch` fetch.
memory or less, on a Ryzen AI Max, and on a single Radeon RX 9000 / AI PRO R9700 card (and on several of them when they hold half of the experts). Full quality uses all 6. It needs DeepSeek engine 0.1.50 or newer, which the app and `strata launch` fetch.
**Measured on:** IST-DAS 2.00 bpw, Strata 0.2.4 (before the Speed choice).
+8 -4
View File
@@ -2,7 +2,7 @@
//
// The model routes every word to its top 6 experts. On a PC with a small graphics card the CPU lane and the SSD set the speed, and
// each expert dropped saves their bytes: the router's top N are kept and their weights renormalised (the same selection the full
// model makes, a shorter list). The default is 5 where the graphics memory is 16 GiB or less, or the GPU shares the PC's RAM, the model's own count elsewhere;
// model makes, a shorter list). The default is 5 where the graphics memory is 16 GiB or less, or the GPU shares the PC's RAM, or the cards are RDNA4 (one card, or two or more holding half of the experts), the model's own count elsewhere;
// --experts N says it outright (--experts 6: full quality), DS4_TOPK=N is the test override over everything.
#pragma once
@@ -36,12 +36,16 @@ constexpr int kFasterModeExperts = 5;
// multi_rdna4: two or more RDNA4 cards (gfx1200 / gfx1201: R9700, RX 9000) that together hold at least half of the routed experts - the 4 x R9700 box
// the Marlin stream measured: 5 against 6 experts, 2.25 bpw, 8K prompt 1339 -> 1419 ppts (+6%) and decode 78.1 -> 81.2 tok/s (+4%), reference KL
// 0.10987 against 0.10833 and top-1 87.59% against 87.53% (the gate is 0.006). Faster mode there too, by default.
// single_rdna4: ONE RDNA4 card (a 32 GB R9700 holds under a fifth of a 2.25 bpw set, the CPU lane does the rest: 29-65% of the experts, 12-18 tok/s).
// 1 x R9700 on p620 (125 GB RAM), 2.25 bpw, frozen profile, 5 interleaved pairs, 5 against 6 experts: decode 15.24 against 13.08 tok/s (+16%, 5 of 5
// pairs), a scripted agent loop -24% wall time (5 of 5), the long-read loop -12% (5 of 5); KL against the 6-expert run 0.030 (6 against 6: 0.008),
// against the original model 0.1106 against 0.1067-0.1071. The multi-card rule above needs the cards to hold half of the experts; this one does not.
inline int experts_per_word(int requested, uint64_t vram_bytes, int model_k, bool integrated = false, bool multi_rdna4 = false,
bool v026 = false) {
bool v026 = false, bool single_rdna4 = false) {
if (model_k <= 1) return model_k;
if (v026) multi_rdna4 = false; // 0.2.6: Faster by default on a small card or an integrated GPU only
if (v026) multi_rdna4 = single_rdna4 = false; // 0.2.6: Faster by default on a small card or an integrated GPU only
int n = requested;
if (n < 1) n = integrated || multi_rdna4 || (vram_bytes != 0 && vram_bytes <= kFasterModeVramMax) ? kFasterModeExperts : model_k;
if (n < 1) n = integrated || multi_rdna4 || single_rdna4 || (vram_bytes != 0 && vram_bytes <= kFasterModeVramMax) ? kFasterModeExperts : model_k;
if (n > model_k) n = model_k;
return n < 1 ? 1 : n;
}
+4 -2
View File
@@ -2840,11 +2840,13 @@ struct Model::Impl {
}
// two or more RDNA4 cards holding at least half of the experts (the 4 x R9700 class): Faster by default too
const bool multi_rdna4 = rdna4() && devs.size() >= 2 && gpu_expert_share() >= 0.5;
const int n = experts_test_override(experts_per_word(o.experts, vram_all, l.k_exp, integrated, multi_rdna4, defaults_026()), l.k_exp);
const bool single_rdna4 = rdna4() && devs.size() == 1; // one R9700 / RX 9000 card: the CPU lane does most of the experts
const int n = experts_test_override(experts_per_word(o.experts, vram_all, l.k_exp, integrated, multi_rdna4, defaults_026(), single_rdna4), l.k_exp);
experts_used = n;
if (n < l.k_exp)
std::fprintf(stderr, "ds4: experts per word: %d (%s; full quality: --experts %d)\n", n,
o.experts >= 1 ? "as asked" : multi_rdna4 ? "faster mode: the cards hold most of the experts" : "faster mode", l.k_exp);
o.experts >= 1 ? "as asked" : multi_rdna4 ? "faster mode: the cards hold most of the experts" :
single_rdna4 && !integrated && vram_all > kFasterModeVramMax ? "faster mode: one RDNA4 card, the CPU lane does most of the experts" : "faster mode", l.k_exp);
else if (o.experts >= 1)
std::fprintf(stderr, "ds4: experts per word: %d (full quality)\n", n);
}
+1 -1
View File
@@ -107,7 +107,7 @@ def add_parsers(sp: argparse._SubParsersAction) -> list[argparse.ArgumentParser]
"(not enough graphics memory, or a model it is not for) the MTP is used and the start says why")
g.add_argument("--speed", type=_choice("a speed", S.SPEED_MODES), metavar="faster|full",
help="DeepSeek: faster uses 5 of 6 experts per word (a small quality change, usually not noticeable; the default on a PC with 16 GB "
"of graphics memory or less), full uses all 6")
"of graphics memory or less, and on a single Radeon RX 9000 / AI PRO R9700 card), full uses all 6")
g.add_argument("--full-quality", action="store_true", help="DeepSeek: all 6 experts per word (the same as --speed full), for scripts")
g.add_argument("--gpu", metavar="LIST", help="graphics card numbers to use, e.g. 0 or 0,1 (default: the engine's choice)")
g.add_argument("--gpu-roles", metavar="LIST", help="a role for each graphics card: main, helper or off, e.g. 1=main,0=helper (cards you do not name are off). "
+2 -2
View File
@@ -475,8 +475,8 @@ def uses_integrated_gpu(computer, s: "S.LaunchSettings") -> bool:
def faster_by_default(computer, s: "S.LaunchSettings") -> bool:
"""The engine's `--experts auto` rule: Faster (5 experts) on an integrated GPU, or when the cards used total 16 GiB or less."""
return uses_integrated_gpu(computer, s) or S.faster_default(used_vram_gb(computer, s))
"""The engine's `--experts auto` rule: Faster (5 experts) on an integrated GPU, on a single RDNA4 card, or when the cards used total 16 GiB or less."""
return uses_integrated_gpu(computer, s) or S.single_rdna4(used_cards(computer, s)) or S.faster_default(used_vram_gb(computer, s))
EXPERTS_LINE = re.compile(r"experts per word: (\d+)")
+11
View File
@@ -41,6 +41,17 @@ def faster_default(total_vram_gb: float | None) -> bool:
return bool(total_vram_gb) and round(float(total_vram_gb) * 1024) <= round(FASTER_VRAM_GIB * 1024)
RDNA4_ARCHS = ("gfx1200", "gfx1201") # Radeon AI PRO R9700, RX 9070 / 9060 (the engine's rdna4())
def single_rdna4(cards) -> bool:
"""Mirrors the engine's single-RDNA4-card rule (src/dsv41/ds4_experts_rule.hpp): exactly one card is used and it is an RDNA4 Radeon, so the CPU lane
does most of the experts and `--experts auto` is 5 whatever the card's memory (1 x R9700, 2.25 bpw: decode +16%, agent loop -24%, KL against the
original model +0.004). Two or more RDNA4 cards are the engine's other rule (the cards hold half of the experts), which the screens do not mirror."""
cards = list(cards or [])
return len(cards) == 1 and str(cards[0].get("vendor") or "").lower() == "amd" and str(cards[0].get("arch") or "").lower().startswith(RDNA4_ARCHS)
def speed_words(speed: str, default: bool) -> str:
"""The plain words for the chosen speed, for --plan and the summary."""
if speed == "faster":
+7
View File
@@ -44,6 +44,13 @@ int main() {
CHECK_EQ(experts_per_word(-1, 3 * 32 * GiB, 6, false, true), 5);
CHECK_EQ(experts_per_word(-1, 4 * 32 * GiB, 6, false, false), 6); // the same cards when they hold under half (or are not RDNA4): the model's 6
CHECK_EQ(experts_per_word(6, 4 * 32 * GiB, 6, false, true), 6); // --experts 6 (the app's / Terminal's "Full quality") wins
// one RDNA4 card (1 x R9700, 32 GB): Faster by default, whatever the card's size; not on another make or on two NVIDIA cards
CHECK_EQ(experts_per_word(-1, 32 * GiB, 6, false, false, false, true), 5);
CHECK_EQ(experts_per_word(-1, 24 * GiB, 6, false, false, false, true), 5);
CHECK_EQ(experts_per_word(-1, 32 * GiB, 6, false, false, false, false), 6); // the same card size when it is not a single RDNA4 card
CHECK_EQ(experts_per_word(6, 32 * GiB, 6, false, false, false, true), 6); // --experts 6 still wins
CHECK_EQ(experts_per_word(4, 32 * GiB, 6, false, false, false, true), 4); // and so does any other --experts N
CHECK_EQ(experts_per_word(-1, 32 * GiB, 6, false, false, true, true), 6); // DS4_DEFAULTS=026: back to the model's 6
CHECK_EQ(experts_per_word(4, 4 * 32 * GiB, 6, false, true), 4); // so does any other --experts N
CHECK_EQ(experts_per_word(5, 4 * 32 * GiB, 6, false, false), 5);
CHECK_EQ(experts_per_word(-1, 4 * 32 * GiB, 1, false, true), 1); // a model of one expert a word has nothing to drop
+15
View File
@@ -125,6 +125,21 @@ class Resolve(unittest.TestCase):
def test_a_choice_wins_over_the_default_in_the_words(self):
self.assertEqual(self.runner(12, speed="full").resolved_speed(), ("full", False))
def test_one_rdna4_card_is_faster_by_default_whatever_its_memory(self): # 1 x R9700 (32 GB): the CPU lane does most of the experts, the engine says 5
r9700 = {"index": 0, "vram_gb": 32, "vendor": "amd", "arch": "gfx1201", "name": "Radeon AI PRO R9700"}
s = S.LaunchSettings()
self.assertTrue(S.single_rdna4([r9700]))
self.assertTrue(faster_by_default(mock.Mock(usable_gpus=[r9700]), s))
self.assertTrue(faster_by_default(mock.Mock(usable_gpus=[{**r9700, "arch": "gfx1200", "vram_gb": 16}]), s))
self.assertEqual(self.runner(32, speed="full").experts_flag(), 6) # the choice still wins
# not a single RDNA4 card: an older Radeon, another make, an unknown arch, or two cards (the engine's other rule)
for other in ({**r9700, "arch": "gfx1100"}, {**r9700, "vendor": "nvidia"}, {k: v for k, v in r9700.items() if k != "arch"}):
self.assertFalse(S.single_rdna4([other]), other)
self.assertFalse(faster_by_default(mock.Mock(usable_gpus=[other]), s), other)
two = mock.Mock(usable_gpus=[r9700, {**r9700, "index": 1}])
self.assertFalse(faster_by_default(two, s))
self.assertTrue(faster_by_default(two, S.LaunchSettings(gpus=[1]))) # one of the two named: a single card
def test_only_the_cards_the_launch_uses_are_summed(self):
comp = mock.Mock(usable_gpus=[{"index": 0, "vram_gb": 12}, {"index": 1, "vram_gb": 24}])
self.assertEqual(used_vram_gb(comp, S.LaunchSettings()), 36)