Ember 2: the plan's section 6b (the clocks, the hill-climb, the goal, the price, the curve, the fleet prior, what is owed); Settings: the goal, the electricity price, the climb switch; the card detail: £ a day for the tuned point and the measured curve; UI test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 15:53:43 +00:00
parent eda64d599f
commit 07f04243d5
4 changed files with 53 additions and 1 deletions

View file

@ -441,7 +441,9 @@ var TuneLine = (function () {
return { kind: 'idle', text: cd.sweep_note || 'tuning: not run yet (starts after 120 s of steady mining)' };
}
function rel(s) { s = Math.max(0, Math.floor(s)); return s < 60 ? s + ' s' : s < 3600 ? Math.floor(s / 60) + ' min' : s < 86400 ? Math.floor(s / 3600) + ' h' : Math.floor(s / 86400) + ' d'; }
return { model: model, point: point, rel: rel };
// Ember 2: £ a day for a draw at pence per kWh (watts × 24 / 1000 × price / 100)
function pounds(watts, pence) { var v = watts * 24 / 1000 * pence / 100; return '£' + v.toFixed(2); }
return { model: model, point: point, rel: rel, pounds: pounds };
})();
if (typeof module === 'object' && module && module.exports) { module.exports = Notices; module.exports.UpdateCard = UpdateCard; module.exports.View = View; module.exports.TuneLine = TuneLine; module.exports.tuneNotice = Notices.tuneNotice; }
@ -630,6 +632,9 @@ if (typeof document !== 'undefined') (function () {
$('s-login').addEventListener('change', function () { var on = this.checked; api('api/settings', { start_at_login: on }).then(function (r) { if (!r.ok) { toast(r.error || 'could not change'); $('s-login').checked = !on; } }); });
$('s-jobs-allow').addEventListener('change', function () { api('api/jobs/allow', { on: this.checked }); });
$('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Ember Tune on: once after install, then weekly' : 'Ember Tune off'); }); });
$('s-tune-goal').addEventListener('change', function () { var g = this.value; api('api/tune/goal', { goal: g }).then(function (r) { if (r.ok) toast('Goal: ' + g); }); });
$('s-power-price').addEventListener('change', function () { var p = parseFloat(this.value); if (isNaN(p)) return; api('api/tune/goal', { price_pence: p }).then(function (r) { if (r.ok) toast('Electricity: ' + p + ' p/kWh'); }); });
$('s-tune-climb').addEventListener('change', function () { var on = this.checked; api('api/tune/goal', { climb: on }).then(function (r) { if (r.ok) toast(on ? 'Ember 2: the clocks climb from the next tune' : 'The ladders run from the next tune'); }); });
$('s-power-control').addEventListener('change', function () { var on = this.checked; api('api/power/control', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Power control on: Windows asks for administrator rights once' : 'Power control off; nothing asks'); }); });
$('s-trust').addEventListener('change', function () { var on = this.checked; api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); else { toast(r.error || 'could not change'); $('s-trust').checked = !on; } }); });
$('s-live').addEventListener('click', function () { if (state && state.live_page) api('api/open', { url: state.live_page }); });
@ -1177,6 +1182,10 @@ if (typeof document !== 'undefined') (function () {
sw('s-jobs-allow', s.jobs && s.jobs.allowed); sw('s-sweep', s.settings.sweep); sw('s-trust', s.settings.proof_verify_trust); sw('s-power-control', s.settings.power_control);
setText('s-power-note', s.settings.tuning_off ? s.settings.tuning_note : (s.settings.power_note || (s.settings.power_control ? '' : 'Off: NVIDIA cards measure only; AMD cards need no rights.')));
setText('s-devfee-text', View.devFeeText(s));
if (document.activeElement !== $('s-tune-goal')) $('s-tune-goal').value = (s.settings.tune_goal || 'balanced');
if (document.activeElement !== $('s-power-price')) $('s-power-price').value = s.settings.power_price_pence > 0 ? s.settings.power_price_pence : '';
setText('s-power-price-note', s.settings.power_price_pence > 0 ? 'the card rows read in £ a day' : 'set it to see £ a day per card');
sw('s-tune-climb', s.settings.tune_climb);
if (document.activeElement !== $('s-name')) $('s-name').value = s.display_name || '';
setText('s-mid', (s.machine_id || '').slice(0, 8));
setText('s-node-dir', 'chain data: ' + s.node_dir);
@ -1200,6 +1209,7 @@ if (typeof document !== 'undefined') (function () {
}
// Ember Tune's line (TuneLine.model): running, tuned, measured, stopped or not run yet, for every card
h += tuneLineHtml(cd);
h += tuneCurveHtml(cd, state && state.settings ? state.settings.power_price_pence : 0);
h += '<div class="ctl"><span class="k">Identities</span><p class="help" style="grid-column:2">Each identity votes and is paid on its own. 8 suits a big card, 2 a small one, 1 an integrated GPU. Changing it restarts that card’s worker.</p><div class="ids"><button type="button" data-d="-1" aria-label="fewer identities">&minus;</button><input type="number" min="1" max="64" value="' + (cd.identities || 1) + '" aria-label="Identities on ' + esc(cd.name) + '"><button type="button" data-d="1" aria-label="more identities">+</button></div></div>';
return h + '</div>';
}
@ -1211,6 +1221,14 @@ if (typeof document !== 'undefined') (function () {
var btn = running ? '<button class="btn tiny" data-sweep-stop="1">Stop</button>' : '<button class="btn tiny" data-sweep-start="' + esc(cd.key) + '">Tune now</button>' + (cd.pinned ? ' <button class="btn tiny" data-sweep-pin="' + esc(cd.key) + '" data-pinned="0">Unpin</button>' : '');
return '<div class="line' + (running ? ' on' : '') + '">' + t + ' ' + btn + '</div>';
}
// Ember 2: £ a day for the tuned point and the measured curve (every row of the last plan)
function tuneCurveHtml(cd, price) {
var h = '';
if (price > 0 && cd.sweep_watts > 0) h += '<div class="line dim">' + esc(TuneLine.pounds(cd.sweep_watts, price)) + ' a day at this point' + (cd.power_w > 0 && Math.abs(cd.power_w - cd.sweep_watts) > 5 ? ' · ' + esc(TuneLine.pounds(cd.power_w, price)) + ' at the draw now' : '') + '</div>';
var c = cd.tune_curve || [];
if (c.length > 1) h += '<div class="line dim">curve: ' + c.map(function (r) { return esc((r.clock_mhz ? r.clock_mhz + ' MHz' : 'unlocked') + (r.mem_mhz ? ' mem ' + r.mem_mhz : '') + ' ' + r.power_pct + '%: ' + Number(r.mhs).toFixed(1) + ' MH/s ' + Math.round(r.watts) + ' W ' + Number(r.eff).toFixed(3) + (r.mark && r.mark !== 'ok' ? ' (' + r.mark + ')' : '')); }).join(' · ') + '</div>';
return h;
}
function renderSetCards(s) {
var cards = shownCards(s.mining.cards), box = $('s-cards');
if (box.querySelector('input[type=range]:active') || box.contains(document.activeElement)) return;

View file

@ -369,6 +369,9 @@
<div class="set-cards" id="s-cards"><div class="empty">No card yet.</div></div>
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span>Ember Tune: tune every card for hashes per watt</span></label>
<p class="help">Once after install, then weekly and after a driver or program change: the power limit steps from 100% down to 50%, then the core clock from its maximum down to 60%, 75 s a step on the live program; the memory clock is never touched. The card keeps the point with the most hashes per watt within 1% of its top rate. A step with a rejected hash, a hot GPU or a dragged memory clock is reverted. A card whose model the fleet already knows starts at that point and confirms it in two steps. Every result goes back to the fleet without anything that identifies you. A cap you set by hand is left alone.</p>
<div class="ctl"><span class="k">Goal</span><select id="s-tune-goal" aria-label="tuning goal"><option value="efficiency">Efficiency: the most hashes per watt, within 10% of the top rate</option><option value="balanced">Balanced: the most hashes per watt, within 1% of the top rate</option><option value="rate">Maximum rate</option></select></div>
<div class="ctl"><span class="k">Electricity</span><input type="number" id="s-power-price" min="0" max="500" step="0.1" placeholder="pence per kWh" aria-label="electricity price, pence per kWh"><span class="pv" id="s-power-price-note"></span></div>
<label class="switch"><input type="checkbox" id="s-tune-climb"><span class="track"></span><span>Ember 2: climb the clocks (memory up, core down) instead of the ladders</span></label>
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span>Power control: let the app set NVIDIA limits</span></label>
<p class="help">Windows asks for administrator rights once; the NVIDIA cap and the tune need them. Off, the app never asks and NVIDIA cards measure only. AMD cards need no rights. <span id="s-power-note"></span></p>
</div>

View file

@ -75,3 +75,8 @@ test('a card never shows 0 MH/s without a reason word: tuning, held and tuned ro
assert.equal(View.tuneEta(30), 'under a minute');
assert.equal(View.tuneEta(700), 'about 12 min');
});
test('Ember 2: £ a day from watts and pence per kWh', () => {
assert.equal(mod.exports.TuneLine.pounds(310, 28.5), '£2.12');
assert.equal(mod.exports.TuneLine.pounds(102.7, 28.5), '£0.70');
});

View file

@ -176,6 +176,32 @@ live MH/s and W, the seconds left and the plan; a POST of state, never a quit, p
reads `Tuned: <MH/s> at <W> W (<MH/W>)` with the point and the plan. Tests: `ui/tune-line.test.mjs` (tuning, held,
tuned rows; the button; the strip line).
## 6b. Ember 2: the clocks, the hill-climb, the goal (6 October 2026)
Why: run 5 measured the 5090's power ladder flat (310 to 314 W under every cap from 575 to 400 W, 0.41 MH/W), so
on this memory-latency-bound hash the power cap is a safety, not a lever; the pair that matters is the memory
clock UP and the core clock DOWN. Ember 2 adds both and replaces the fixed ladder with a hill-climb.
| Piece | What | Where |
|---|---|---|
| The memory knob | NVIDIA: `nvidia-smi -lmc <m>,<m>` locks the memory clock to one value, `-rmc` resets; the range is the driver's own (`clocks.mem` under load = the floor, `clocks.max.mem` = the ceiling; PC 1's 5090: 13,801 to 14,001 MHz); directly from an elevated engine or through the Power Helper's `lmc`/`rmc` verbs. AMD: ADLX on RDNA 4 exposes no memory setter through the helper's path, so an AMD card climbs on core and power only (the row says so). Linux NVIDIA: `-lmc` through pkexec as the cap is | `Point.mem_mhz`, `Limits.mem_default_mhz`, `Limits.mem_max_mhz`, `tune_apply`, `powertask::HelperCmd::MemClock`, `sweep::helper_script_*` |
| The hill-climb | from the start point (the fleet prior for the model, else the card's point): each probe moves memory up by 5% of the range above the default (at least 25 MHz) or core down by 5% of the maximum (at least 25 MHz); a probe that improves the goal's score keeps that direction, else the climb turns to the other knob, then to both; 5 probes of 60 s (10 s settle) converge in under 10 minutes | `Plan::climb`, `Climb`, `Plan::climb_next` |
| The stability guard | a rejected or mismatched hash during a probe marks it `faulted`; a faulted probe backs that knob off for good in this climb (memory never goes above, core never below, the last good point); the hash fingerprint is the worker's own self-test at every pack (a variant that is not bit-exact never serves), so a step that keeps mining with 0 mismatches is stable by the only test that matters | `Row::from_samples`, `climb_next`'s `refused_mem` / `refused_core`, docs/design/miner-tuning.md |
| The goal | Settings > goal: `efficiency` (the most MH/W within 10% of the top rate), `balanced` (within 1%, lever 3's rule), `rate` (the fastest point; MH/W breaks ties); one goal for every card | `Goal`, `Settings.tune_goal`, `POST /api/tune/goal {goal, price_pence, climb}` |
| The price | Settings > electricity, pence per kWh: the card's row reads the chosen point as £ a day (`watts × 24 / 1000 × price / 100`: 310 W at 28.5 p = £2.12) | `pounds_per_day`, `Settings.power_price_pence`, the UI |
| The curve | every row of the last plan on the card (`tune_curve`: point, MH/s, W, MH/W, clocks, hottest reading, mark) so a manual tuner sees the points the climb measured | `CardState.tune_curve`, the card detail |
| The fleet | the record's `steps` already carry the curve; the prior gains `mem_mhz` (the median memory clock, 10 MHz steps); a stranger's card of a known model starts its climb at the prior's point | `relay/lib/ember.mjs`, `ember::prior_of` |
| Re-check | weekly (the manifest's period), after a driver major or program-class change, and when the card's temperature band changes (owed: the band trigger) | `tick_sweep` |
Tests: `ember::tests::the_climb_walks_memory_up_and_core_down_and_converges_in_five_probes` (a synthetic
memory-bound card: every probe moves memory up or core down inside the range, the chosen point beats the start in
five probes), `a_refused_probe_backs_that_knob_off_for_good`, `each_goal_picks_its_point` (and the £/day formula),
`powertask::tests` (the `lmc`/`rmc` verbs reach nvidia-smi as `-lmc m,m` and `-rmc`), `sweep::tests` (the helper
scripts carry them), `relay/test/ember.test.mjs` (climb records aggregate into a prior with the memory clock).
Owed: the measured comparison on PC 1 (one climb per card against run 5's ladder rows, no prompt through the
Power Helper), the temperature-band trigger, NVAPI/nvidia-settings for cards whose driver refuses `-lmc`.
## 7a. One administrator approval, ever (0.3.13; the project lead, 6 October 2026, 11:50 UTC)
What 0.3.12 does: Power control on raises one prompt and sets every cap in that step; every later cap (an app start, a