Add conditional full-attention KV comparison

#8
README.md CHANGED
@@ -41,6 +41,8 @@ Explore two different architectures with real Q8_0 tensor captures: Flint's spar
41
 
42
  [**Explore the layer map**](https://alphabell-model-observatory.static.hf.space/architecture/index.html) to compare Nanbeige4.3-33B's 52 full-attention layers with the Qwen3.8-27B text decoder's 48 recurrent linear and 16 full-attention layers. This comparison uses published configurations; the measured captures in this Space are Flint and Qwen.
43
 
 
 
44
  [Read the architecture explainer](https://huggingface.co/blog/alphabell/nanbeige-qwen-layer-map) for pinned sources and a matched runtime test that could measure how context length affects each model.
45
 
46
  ## Five measured experiments
 
41
 
42
  [**Explore the layer map**](https://alphabell-model-observatory.static.hf.space/architecture/index.html) to compare Nanbeige4.3-33B's 52 full-attention layers with the Qwen3.8-27B text decoder's 48 recurrent linear and 16 full-attention layers. This comparison uses published configurations; the measured captures in this Space are Flint and Qwen.
43
 
44
+ The page also calculates raw f16 full-attention K/V storage across context lengths if a runtime enables caching. Nanbeige's released config sets `use_cache: false`, so this is a conditional calculation rather than measured default memory use.
45
+
46
  [Read the architecture explainer](https://huggingface.co/blog/alphabell/nanbeige-qwen-layer-map) for pinned sources and a matched runtime test that could measure how context length affects each model.
47
 
48
  ## Five measured experiments
architecture/comparison.css CHANGED
@@ -24,7 +24,7 @@ h1 em{font-style:normal;color:var(--green)}
24
  .scope{display:flex;align-items:flex-start;gap:10px;max-width:830px;padding:14px 17px;background:#edf3ec;border:1px solid #d8e6d9;border-radius:9px;color:#4b6658;font-size:12px;line-height:1.5}
25
  .scope strong{white-space:nowrap;color:var(--green)}
26
  .scope-dot{flex:none;margin-top:5px;width:7px;height:7px;border-radius:50%;background:var(--green)}
27
- section.overview,section.layers,section.source{border-top:1px solid var(--line);padding-top:38px;margin-top:18px}
28
  .section-heading{margin-bottom:24px}
29
  .section-heading .eyebrow{margin-bottom:8px}
30
  .section-heading h2{font-size:clamp(27px,3.2vw,37px);letter-spacing:-.045em;margin-bottom:10px}
@@ -49,6 +49,25 @@ section.overview,section.layers,section.source{border-top:1px solid var(--line);
49
  .spec-table thead th{color:var(--muted);font-size:10px;letter-spacing:.1em;text-transform:uppercase}
50
  .spec-table tbody th{font-weight:550;width:42%}
51
  .spec-table tbody td{font-variant-numeric:tabular-nums;font-weight:650}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  .legend{display:flex;gap:24px;flex-wrap:wrap;font-size:12px;font-weight:650;margin-bottom:16px}
53
  .legend span{display:inline-flex;align-items:center;gap:8px}
54
  .swatch{width:12px;height:12px;border-radius:3px;display:inline-block}
@@ -90,7 +109,7 @@ footer a{color:var(--green);font-weight:650;text-decoration:none}
90
  .scope{display:grid;grid-template-columns:8px 1fr;gap:3px 10px}
91
  .scope span:last-child{grid-column:2}
92
  .scope-dot{margin-top:5px}
93
- .overview-grid,.map-pair{grid-template-columns:1fr}
94
  .model-card{min-height:220px}
95
  .inspect dl{grid-template-columns:repeat(2,minmax(0,1fr))}
96
  }
@@ -113,6 +132,10 @@ footer a{color:var(--green);font-weight:650;text-decoration:none}
113
  .inspect dl{gap:7px}
114
  .inspect dl>div{padding:10px}
115
  .inspect dd{font-size:13px}
 
 
 
 
116
  footer{display:block;padding-left:16px;padding-right:16px}
117
  footer a{display:block;margin-top:8px}
118
  }
 
24
  .scope{display:flex;align-items:flex-start;gap:10px;max-width:830px;padding:14px 17px;background:#edf3ec;border:1px solid #d8e6d9;border-radius:9px;color:#4b6658;font-size:12px;line-height:1.5}
25
  .scope strong{white-space:nowrap;color:var(--green)}
26
  .scope-dot{flex:none;margin-top:5px;width:7px;height:7px;border-radius:50%;background:var(--green)}
27
+ section.overview,section.kv,section.layers,section.source{border-top:1px solid var(--line);padding-top:38px;margin-top:18px}
28
  .section-heading{margin-bottom:24px}
29
  .section-heading .eyebrow{margin-bottom:8px}
30
  .section-heading h2{font-size:clamp(27px,3.2vw,37px);letter-spacing:-.045em;margin-bottom:10px}
 
49
  .spec-table thead th{color:var(--muted);font-size:10px;letter-spacing:.1em;text-transform:uppercase}
50
  .spec-table tbody th{font-weight:550;width:42%}
51
  .spec-table tbody td{font-variant-numeric:tabular-nums;font-weight:650}
52
+ .kv-panel{background:#fff;border:1px solid var(--line);border-radius:12px;padding:24px}
53
+ .kv-control-heading{display:flex;justify-content:space-between;align-items:baseline;gap:12px;margin-bottom:14px}
54
+ .kv-control-heading label{font-weight:700;font-size:14px}
55
+ .kv-control-heading output{font-weight:750;font-size:18px;font-variant-numeric:tabular-nums;color:var(--green)}
56
+ .kv-control input{display:block;width:100%;accent-color:var(--green);cursor:pointer}
57
+ .kv-endpoints{display:flex;justify-content:space-between;color:var(--muted);font-size:11px;margin-top:6px}
58
+ .kv-share{font-size:11px;color:var(--muted);margin:8px 0 0;text-align:right}
59
+ .kv-results{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:28px}
60
+ .kv-result{min-width:0;border:1px solid var(--line);border-top:4px solid var(--green);border-radius:10px;background:#fafcf9;padding:19px 21px;display:flex;flex-direction:column;gap:4px}
61
+ .kv-result.qwen-kv{border-top-color:var(--blue);background:#f9fcfd}
62
+ .kv-result p{font-size:12px;font-weight:700;color:var(--muted);margin:0 0 5px}
63
+ .kv-result strong{font-size:clamp(31px,4vw,45px);letter-spacing:-.055em;line-height:1.1;font-variant-numeric:tabular-nums}
64
+ .kv-result span{font-size:15px;font-weight:650;font-variant-numeric:tabular-nums}
65
+ .kv-result small{font-size:11px;color:var(--muted);font-variant-numeric:tabular-nums;margin-top:7px}
66
+ .kv-ratio{font-size:13px;font-weight:750;color:var(--green);margin:19px 0 0}
67
+ .kv-formula{font-size:12px;font-weight:600;color:#435e53;margin:13px 0 0;line-height:1.5}
68
+ .kv-scope{font-size:12px;line-height:1.65;color:#526a5f;margin:12px 0 0;max-width:880px}
69
+ .kv-scope code{background:#e9efe7;padding:2px 4px;border-radius:3px}
70
+ .kv-scope a{color:var(--green);font-weight:700}
71
  .legend{display:flex;gap:24px;flex-wrap:wrap;font-size:12px;font-weight:650;margin-bottom:16px}
72
  .legend span{display:inline-flex;align-items:center;gap:8px}
73
  .swatch{width:12px;height:12px;border-radius:3px;display:inline-block}
 
109
  .scope{display:grid;grid-template-columns:8px 1fr;gap:3px 10px}
110
  .scope span:last-child{grid-column:2}
111
  .scope-dot{margin-top:5px}
112
+ .overview-grid,.map-pair,.kv-results{grid-template-columns:1fr}
113
  .model-card{min-height:220px}
114
  .inspect dl{grid-template-columns:repeat(2,minmax(0,1fr))}
115
  }
 
132
  .inspect dl{gap:7px}
133
  .inspect dl>div{padding:10px}
134
  .inspect dd{font-size:13px}
135
+ .kv-panel{padding:16px}
136
+ .kv-control-heading{display:block}
137
+ .kv-control-heading output{display:block;margin-top:4px}
138
+ .kv-result{padding:16px}
139
  footer{display:block;padding-left:16px;padding-right:16px}
140
  footer a{display:block;margin-top:8px}
141
  }
architecture/comparison.js CHANGED
@@ -9,6 +9,7 @@ const fmt = value => new Intl.NumberFormat('en-US').format(value);
9
  const byId = id => document.getElementById(id);
10
  const configs = {};
11
  let selected = { model: 'qwen', layer: 3 };
 
12
 
13
  function attentionType(model, layer) {
14
  if (model === 'nanbeige') return 'full';
@@ -35,7 +36,7 @@ function renderSpecs() {
35
  byId('qwen-layer-count').textContent = `${linear} + ${full}`;
36
  const rows = [
37
  ['Decoder layers', fmt(n.num_hidden_layers), fmt(q.num_hidden_layers)],
38
- ['Attention pattern', '52 full', `${linear} recurrent linear / ${full} full`],
39
  ['Hidden width', fmt(n.hidden_size), fmt(q.hidden_size)],
40
  ['Dense feed-forward width', fmt(n.intermediate_size), fmt(q.intermediate_size)],
41
  ['Full-attention query heads', fmt(n.num_attention_heads), fmt(q.num_attention_heads)],
@@ -46,6 +47,42 @@ function renderSpecs() {
46
  byId('spec-body').replaceChildren(...rows.map(row => specRow(...row)));
47
  }
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  function fact(label, value) {
50
  const group = document.createElement('div');
51
  const dt = document.createElement('dt');
@@ -110,12 +147,14 @@ async function init() {
110
  for (const [model, config] of entries) configs[model] = config;
111
  if (configs.nanbeige.num_hidden_layers !== 52 || configs.qwen.layer_types.length !== configs.qwen.num_hidden_layers) throw new Error('Unexpected decoder configuration');
112
  renderSpecs();
 
113
  renderMap('nanbeige');
114
  renderMap('qwen');
115
  renderSelection();
116
  } catch (error) {
117
  byId('inspect-title').textContent = 'The comparison could not open';
118
  byId('inspect-description').textContent = 'Reload this page to try the model configurations again.';
 
119
  console.error('Architecture comparison failed', error);
120
  }
121
  }
 
9
  const byId = id => document.getElementById(id);
10
  const configs = {};
11
  let selected = { model: 'qwen', layer: 3 };
12
+ let kvLengths = [];
13
 
14
  function attentionType(model, layer) {
15
  if (model === 'nanbeige') return 'full';
 
36
  byId('qwen-layer-count').textContent = `${linear} + ${full}`;
37
  const rows = [
38
  ['Decoder layers', fmt(n.num_hidden_layers), fmt(q.num_hidden_layers)],
39
+ ['Attention pattern', `${n.num_hidden_layers} full`, `${linear} recurrent linear / ${full} full`],
40
  ['Hidden width', fmt(n.hidden_size), fmt(q.hidden_size)],
41
  ['Dense feed-forward width', fmt(n.intermediate_size), fmt(q.intermediate_size)],
42
  ['Full-attention query heads', fmt(n.num_attention_heads), fmt(q.num_attention_heads)],
 
47
  byId('spec-body').replaceChildren(...rows.map(row => specRow(...row)));
48
  }
49
 
50
+ function renderKv() {
51
+ const control = byId('kv-step');
52
+ const tokens = kvLengths[Number(control.value)];
53
+ control.setAttribute('aria-valuetext', `${fmt(tokens)} tokens`);
54
+ byId('kv-context-label').textContent = `${fmt(tokens)} tokens`;
55
+ const values = {};
56
+ for (const [model, prefix] of [['nanbeige', 'nan'], ['qwen', 'qwen']]) {
57
+ const bytes = window.kvCache.fullAttentionKvBytes(configs[model], tokens);
58
+ const perToken = window.kvCache.fullAttentionKvBytes(configs[model], 1);
59
+ values[model] = bytes;
60
+ byId(`kv-${prefix}-gb`).textContent = `${(bytes / 1e9).toFixed(2)} GB`;
61
+ byId(`kv-${prefix}-gib`).textContent = `${(bytes / 2 ** 30).toFixed(2)} GiB`;
62
+ byId(`kv-${prefix}-per-token`).textContent = `${fmt(perToken / 1024)} KiB per token`;
63
+ }
64
+ byId('kv-ratio').textContent = `${(values.nanbeige / values.qwen).toFixed(2)}× Nanbeige-to-Qwen ratio for this calculated component`;
65
+ }
66
+
67
+ function initKv() {
68
+ if (!window.kvCache || configs.nanbeige.use_cache !== false) throw new Error('Unexpected KV source configuration');
69
+ const max = Math.min(configs.nanbeige.max_position_embeddings, configs.qwen.max_position_embeddings);
70
+ kvLengths = [2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144].filter(tokens => tokens <= max);
71
+ if (!kvLengths.length) throw new Error('No common context length');
72
+ const control = byId('kv-step');
73
+ control.max = String(kvLengths.length - 1);
74
+ const requested = Number(new URLSearchParams(window.location.search).get('tokens'));
75
+ const requestedIndex = kvLengths.indexOf(requested);
76
+ control.value = String(requestedIndex >= 0 ? requestedIndex : Math.min(4, kvLengths.length - 1));
77
+ control.addEventListener('input', () => {
78
+ renderKv();
79
+ const url = new URL(window.location.href);
80
+ url.searchParams.set('tokens', String(kvLengths[Number(control.value)]));
81
+ window.history.replaceState(null, '', url);
82
+ });
83
+ renderKv();
84
+ }
85
+
86
  function fact(label, value) {
87
  const group = document.createElement('div');
88
  const dt = document.createElement('dt');
 
147
  for (const [model, config] of entries) configs[model] = config;
148
  if (configs.nanbeige.num_hidden_layers !== 52 || configs.qwen.layer_types.length !== configs.qwen.num_hidden_layers) throw new Error('Unexpected decoder configuration');
149
  renderSpecs();
150
+ initKv();
151
  renderMap('nanbeige');
152
  renderMap('qwen');
153
  renderSelection();
154
  } catch (error) {
155
  byId('inspect-title').textContent = 'The comparison could not open';
156
  byId('inspect-description').textContent = 'Reload this page to try the model configurations again.';
157
+ byId('kv-context-label').textContent = 'Calculation unavailable';
158
  console.error('Architecture comparison failed', error);
159
  }
160
  }
architecture/index.html CHANGED
@@ -8,6 +8,7 @@
8
  <title>Nanbeige and Qwen architecture | alphaBell</title>
9
  <link rel="icon" href="../favicon.svg">
10
  <link rel="stylesheet" href="comparison.css">
 
11
  <script src="comparison.js" defer></script>
12
  </head>
13
  <body>
@@ -17,7 +18,7 @@
17
  </header>
18
  <main>
19
  <section class="intro" aria-labelledby="page-title">
20
- <p class="eyebrow">Architecture comparison <span>01 / 02</span></p>
21
  <h1 id="page-title">Two dense decoders.<br><em>Different paths through a token.</em></h1>
22
  <p class="lead">Nanbeige repeats full attention in 52 layers. Qwen alternates recurrent linear attention with full attention across 64 text-decoder layers. Explore the published configuration, one layer at a time.</p>
23
  <div class="scope"><span class="scope-dot" aria-hidden="true"></span><strong>Configuration view</strong><span>Layer types and dimensions come from pinned model configs. Nanbeige weights, activations and routing were not measured here.</span></div>
@@ -40,8 +41,22 @@
40
  <div class="spec-wrap"><table class="spec-table"><caption>Published decoder configuration</caption><thead><tr><th scope="col">Configuration</th><th scope="col">Nanbeige</th><th scope="col">Qwen text decoder</th></tr></thead><tbody id="spec-body"></tbody></table></div>
41
  </section>
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  <section class="layers" aria-labelledby="layers-title">
44
- <div class="section-heading"><p class="eyebrow">02 / Layer map</p><h2 id="layers-title">Follow the stack</h2><p>Each square is one configured decoder layer. Select a layer to inspect its attention and feed-forward dimensions.</p></div>
45
  <div class="legend" aria-label="Layer colors"><span><i class="swatch full"></i>Full attention</span><span><i class="swatch linear"></i>Recurrent linear attention</span></div>
46
  <div class="map-pair">
47
  <div class="map-card"><div class="map-title"><h3>Nanbeige</h3><span>52 layers</span></div><div class="layer-grid" id="nanbeige-grid" role="group" aria-label="Nanbeige layers"></div></div>
 
8
  <title>Nanbeige and Qwen architecture | alphaBell</title>
9
  <link rel="icon" href="../favicon.svg">
10
  <link rel="stylesheet" href="comparison.css">
11
+ <script src="kv-cache.js" defer></script>
12
  <script src="comparison.js" defer></script>
13
  </head>
14
  <body>
 
18
  </header>
19
  <main>
20
  <section class="intro" aria-labelledby="page-title">
21
+ <p class="eyebrow">Architecture comparison <span>01 / 03</span></p>
22
  <h1 id="page-title">Two dense decoders.<br><em>Different paths through a token.</em></h1>
23
  <p class="lead">Nanbeige repeats full attention in 52 layers. Qwen alternates recurrent linear attention with full attention across 64 text-decoder layers. Explore the published configuration, one layer at a time.</p>
24
  <div class="scope"><span class="scope-dot" aria-hidden="true"></span><strong>Configuration view</strong><span>Layer types and dimensions come from pinned model configs. Nanbeige weights, activations and routing were not measured here.</span></div>
 
41
  <div class="spec-wrap"><table class="spec-table"><caption>Published decoder configuration</caption><thead><tr><th scope="col">Configuration</th><th scope="col">Nanbeige</th><th scope="col">Qwen text decoder</th></tr></thead><tbody id="spec-body"></tbody></table></div>
42
  </section>
43
 
44
+ <section class="kv" aria-labelledby="kv-title">
45
+ <div class="section-heading"><p class="eyebrow">02 / Cache lens</p><h2 id="kv-title">How full-attention K/V would scale</h2><p>Choose a context length to calculate raw f16 key and value tensor storage for one sequence if an inference runtime enables caching.</p></div>
46
+ <div class="kv-panel">
47
+ <div class="kv-control"><div class="kv-control-heading"><label for="kv-step">Context length</label><output id="kv-context-label" for="kv-step" aria-live="polite">32,768 tokens</output></div><input id="kv-step" type="range" min="0" max="7" step="1" value="4" aria-describedby="kv-scope"><div class="kv-endpoints"><span>2K tokens</span><span>256K tokens</span></div><p class="kv-share">Copy the page URL to share this context.</p></div>
48
+ <div class="kv-results">
49
+ <article class="kv-result nanbeige-kv"><p>Nanbeige4.3-33B</p><strong id="kv-nan-gb" aria-live="polite"></strong><span id="kv-nan-gib"></span><small id="kv-nan-per-token"></small></article>
50
+ <article class="kv-result qwen-kv"><p>Qwen3.8-27B text decoder</p><strong id="kv-qwen-gb" aria-live="polite"></strong><span id="kv-qwen-gib"></span><small id="kv-qwen-per-token"></small></article>
51
+ </div>
52
+ <p class="kv-ratio" id="kv-ratio"></p>
53
+ <p class="kv-formula">Formula: tokens × full-attention layers × 2 (K and V) × KV heads × head dimension × 2 bytes.</p>
54
+ <p class="kv-scope" id="kv-scope">Calculation scope: one sequence, f16 K and V for full-attention layers. Nanbeige's released model and <a href="https://huggingface.co/Nanbeige/Nanbeige4.3-33B/blob/c1093c501cc6460427c0ac634db7223d0450c0ee/generation_config.json" target="_blank" rel="noopener noreferrer">generation config</a> set <code>use_cache: false</code>, so its number applies when a runtime enables caching. Qwen's recurrent layers maintain separate state; weights and runtime overhead are separate.</p>
55
+ </div>
56
+ </section>
57
+
58
  <section class="layers" aria-labelledby="layers-title">
59
+ <div class="section-heading"><p class="eyebrow">03 / Layer map</p><h2 id="layers-title">Follow the stack</h2><p>Each square is one configured decoder layer. Select a layer to inspect its attention and feed-forward dimensions.</p></div>
60
  <div class="legend" aria-label="Layer colors"><span><i class="swatch full"></i>Full attention</span><span><i class="swatch linear"></i>Recurrent linear attention</span></div>
61
  <div class="map-pair">
62
  <div class="map-card"><div class="map-title"><h3>Nanbeige</h3><span>52 layers</span></div><div class="layer-grid" id="nanbeige-grid" role="group" aria-label="Nanbeige layers"></div></div>
architecture/kv-cache.js ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ function fullAttentionLayers(config) {
4
+ if (Array.isArray(config.layer_types)) {
5
+ return config.layer_types.filter(type => type === 'full_attention').length;
6
+ }
7
+ return config.num_hidden_layers;
8
+ }
9
+
10
+ function fullAttentionKvBytes(config, tokens, bytesPerValue = 2) {
11
+ const layers = fullAttentionLayers(config);
12
+ for (const value of [layers, config.num_key_value_heads, config.head_dim, tokens, bytesPerValue]) {
13
+ if (!Number.isSafeInteger(value) || value < 1) throw new RangeError('Invalid KV calculation input');
14
+ }
15
+ const bytes = tokens * layers * 2 * config.num_key_value_heads * config.head_dim * bytesPerValue;
16
+ if (!Number.isSafeInteger(bytes)) throw new RangeError('KV calculation exceeds safe integer range');
17
+ return bytes;
18
+ }
19
+
20
+ const kvCache = { fullAttentionLayers, fullAttentionKvBytes };
21
+ if (typeof module !== 'undefined' && module.exports) module.exports = kvCache;
22
+ else window.kvCache = kvCache;
tests/architecture.test.cjs CHANGED
@@ -4,6 +4,7 @@ const assert = require('node:assert/strict');
4
  const fs = require('node:fs');
5
  const path = require('node:path');
6
  const crypto = require('node:crypto');
 
7
  const root = path.resolve(__dirname, '..');
8
  const read = file => fs.readFileSync(path.join(root, file), 'utf8');
9
  const n = JSON.parse(read('models/nanbeige.json'));
@@ -19,9 +20,22 @@ test('the pinned Nanbeige configuration supports the displayed comparison', () =
19
  assert.equal(n.num_key_value_heads, 8);
20
  assert.equal(n.head_dim, 128);
21
  assert.equal(n.max_position_embeddings, 524288);
 
22
  assert.equal(n.num_experts, undefined);
23
  });
24
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  test('the pinned Qwen text decoder has three linear layers for each full layer', () => {
26
  assert.equal(q.model_type, 'qwen3_5_text');
27
  assert.equal(q.layer_types.length, q.num_hidden_layers);
@@ -38,14 +52,17 @@ test('the architecture route is linked and ships its local assets', () => {
38
  const home = read('index.html');
39
  const html = read('architecture/index.html');
40
  assert.match(home, /href="architecture\/index\.html"/);
41
- for (const file of ['architecture/comparison.css', 'architecture/comparison.js', 'models/nanbeige.json', 'models/qwen.json']) {
42
  assert.ok(fs.statSync(path.join(root, file)).size > 0, file);
43
  }
44
  assert.match(html, /Nanbeige.*?config/s);
45
  assert.match(html, /c1093c501cc6460427c0ac634db7223d0450c0ee/);
46
  assert.match(html, /1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0/);
47
  assert.match(html, /Nanbeige weights, activations and routing were not measured here/);
48
- for (const text of [html, read('architecture/comparison.js'), read('architecture/comparison.css')]) {
 
 
 
49
  assert.doesNotMatch(text, /\u2014|&mdash;|&#8212;|&#x2014;/i);
50
  }
51
  });
 
4
  const fs = require('node:fs');
5
  const path = require('node:path');
6
  const crypto = require('node:crypto');
7
+ const { fullAttentionLayers, fullAttentionKvBytes } = require('../architecture/kv-cache.js');
8
  const root = path.resolve(__dirname, '..');
9
  const read = file => fs.readFileSync(path.join(root, file), 'utf8');
10
  const n = JSON.parse(read('models/nanbeige.json'));
 
20
  assert.equal(n.num_key_value_heads, 8);
21
  assert.equal(n.head_dim, 128);
22
  assert.equal(n.max_position_embeddings, 524288);
23
+ assert.equal(n.use_cache, false);
24
  assert.equal(n.num_experts, undefined);
25
  });
26
 
27
+ test('conditional f16 full-attention KV sizes use the pinned layer and head dimensions', () => {
28
+ assert.equal(fullAttentionLayers(n), 52);
29
+ assert.equal(fullAttentionLayers(q), 16);
30
+ assert.equal(fullAttentionKvBytes(n, 1), 212992);
31
+ assert.equal(fullAttentionKvBytes(q, 1), 65536);
32
+ assert.equal(fullAttentionKvBytes(n, 32768), 6979321856);
33
+ assert.equal(fullAttentionKvBytes(q, 32768), 2147483648);
34
+ assert.equal(fullAttentionKvBytes(n, 262144), 55834574848);
35
+ assert.equal(fullAttentionKvBytes(q, 262144), 17179869184);
36
+ assert.throws(() => fullAttentionKvBytes(n, 0), RangeError);
37
+ });
38
+
39
  test('the pinned Qwen text decoder has three linear layers for each full layer', () => {
40
  assert.equal(q.model_type, 'qwen3_5_text');
41
  assert.equal(q.layer_types.length, q.num_hidden_layers);
 
52
  const home = read('index.html');
53
  const html = read('architecture/index.html');
54
  assert.match(home, /href="architecture\/index\.html"/);
55
+ for (const file of ['architecture/comparison.css', 'architecture/comparison.js', 'architecture/kv-cache.js', 'models/nanbeige.json', 'models/qwen.json']) {
56
  assert.ok(fs.statSync(path.join(root, file)).size > 0, file);
57
  }
58
  assert.match(html, /Nanbeige.*?config/s);
59
  assert.match(html, /c1093c501cc6460427c0ac634db7223d0450c0ee/);
60
  assert.match(html, /1d4bf0f2ff6012fd82039f2fa52739d0dd7c60c0/);
61
  assert.match(html, /Nanbeige weights, activations and routing were not measured here/);
62
+ assert.match(html, /use_cache: false/);
63
+ assert.match(html, /generation_config\.json/);
64
+ assert.match(html, /if an inference runtime enables caching/);
65
+ for (const text of [html, read('architecture/comparison.js'), read('architecture/comparison.css'), read('architecture/kv-cache.js')]) {
66
  assert.doesNotMatch(text, /\u2014|&mdash;|&#8212;|&#x2014;/i);
67
  }
68
  });