sparkle/native/ext/pool/demand.cpp
fiser_jun 4745f264b2
2026-08-04 14:34:00 +08:00

332 lines
15 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// [6] Phase 2 方案B真实区槽状态 C++ 全接管1 次同步版)。
// 复刻 Python ResidentExpertPool 的 _slot_of/_free/_freq + _choose_victim/_alloc_slot 语义:
// - free 优先(front popfree 初始 [0,cap))free 空 → LFU 驱逐,受害者槽直接复用(不回 free)。
// - _choose_victimcandidates=插入序中 ∉current 者victim=min(freq, 候选下标),驱逐不删 freq。
// - dual 语义:真实区命中不移动插入序;仅新放入 miss 追加序尾。
#include "demand.h"
#include "side_region.h"
#include "../io/bg_reader.h"
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <unordered_map>
#include <unordered_set>
struct RealLayer {
std::vector<int> order; // 插入序(LRU tie-break),与 e2r 同步维护
std::unordered_map<int, int> e2r; // expert -> slot [0,cap)
std::vector<int> free_rows; // 空闲槽(front pop仿 free.pop(0))
std::unordered_map<int, uint32_t> freq; // LFU 频次(驱逐不删,与 Python 一致)
std::unordered_set<int> pinned; // AUTOPIN 钉死集:驱逐永不选(见 real_pin)
int cap = 0;
long access = 0; // 累计访问(decay 用)
bool inited = false;
};
static std::mutex g_real_mutex;
static std::map<int, RealLayer> g_real;
// demand 统计:累计 + 本次(供 Python 更新 rp.hits/misses/gpu_fastpath/gpu_fallback)。
static std::mutex g_dstat_mutex;
// 本次 [hitpos, misspos, loads, fallback01, unplaced]
static long g_d_last[5] = {0, 0, 0, 0, 0};
static std::atomic<long> g_demand_ticket{1000000000}; // demand 并行 pread 用的独立 ticket 段
// 诊断计时(DEMAND_TIMING=1):累计各段主线程微秒,供定位结构性开销。默认关。
static bool g_dt_on = false;
static double g_dt[6] = {0, 0, 0, 0, 0, 0}; // [inds_eval, pool_eval, side_snap, real_lock, core, build]
static inline double dt_now_us() {
return std::chrono::duration<double, std::micro>(
std::chrono::steady_clock::now().time_since_epoch()).count();
}
std::vector<double> demand_timings() { return {g_dt[0], g_dt[1], g_dt[2], g_dt[3], g_dt[4], g_dt[5]}; }
void demand_timing_enable(bool on) {
g_dt_on = on;
for (int i = 0; i < 6; ++i) g_dt[i] = 0;
}
static void real_ensure_locked(RealLayer& c, int cap) {
if (c.inited) return;
c.cap = cap;
c.free_rows.clear();
for (int r = 0; r < cap; ++r) c.free_rows.push_back(r); // free 初始 [0,cap)
c.inited = true;
}
void real_init(int layer, int cap) {
std::lock_guard<std::mutex> lk(g_real_mutex);
real_ensure_locked(g_real[layer], cap);
}
std::vector<int> real_region_contents(int layer) {
std::lock_guard<std::mutex> lk(g_real_mutex);
std::vector<int> out;
auto it = g_real.find(layer);
if (it != g_real.end())
for (auto& p : it->second.e2r) { out.push_back(p.first); out.push_back(p.second); }
return out;
}
int real_region_count(int layer) {
std::lock_guard<std::mutex> lk(g_real_mutex);
auto it = g_real.find(layer);
return it == g_real.end() ? 0 : static_cast<int>(it->second.e2r.size());
}
int real_pinned_count(int layer) {
std::lock_guard<std::mutex> lk(g_real_mutex);
auto it = g_real.find(layer);
return it == g_real.end() ? 0 : static_cast<int>(it->second.pinned.size());
}
void real_reset() {
std::lock_guard<std::mutex> lk(g_real_mutex);
g_real.clear();
}
// 复刻 _choose_victim遍历插入序(order)选 ∉current 且 freq 最小者,并列取最早(候选下标最小)。
// pinned(AUTOPIN 钉死)与 current 同样永不选。返回 expert id-1 表示无可驱逐。调用方须持 g_real_mutex。
static int choose_victim_locked(RealLayer& c, const std::unordered_set<int>& current) {
int victim = -1;
uint32_t best = 0;
for (int e : c.order) {
if (!c.e2r.count(e) || current.count(e) || c.pinned.count(e)) continue;
uint32_t f = c.freq.count(e) ? c.freq[e] : 0;
if (victim < 0 || f < best) { victim = e; best = f; } // 并列不更新 → 保留更早者
}
return victim;
}
// AUTOPIN 预热钉死:把历史热专家注册进真实区并标记 pinned(此后驱逐永不选它)。
// 已驻 → 仅补 pinned 标记、返回原槽;未驻 → free 头取槽登记(池行字节由调用方负责写入)。
// 返回与 experts 平行的槽位;空闲耗尽分不到槽给 -1(调用方跳过该专家)。幂等。
// 前置:调用方须先 real_init(否则 free_rows 为空、全返 -1)。
std::vector<int> real_pin(int layer, const std::vector<int>& experts) {
std::lock_guard<std::mutex> lk(g_real_mutex);
RealLayer& c = g_real[layer];
std::vector<int> out;
out.reserve(experts.size());
for (int e : experts) {
auto it = c.e2r.find(e);
if (it != c.e2r.end()) { c.pinned.insert(e); out.push_back(it->second); continue; }
if (c.free_rows.empty()) { out.push_back(-1); continue; }
int slot = c.free_rows.front();
c.free_rows.erase(c.free_rows.begin()); // free.pop(0)
c.e2r[e] = slot;
c.order.push_back(e);
c.pinned.insert(e);
out.push_back(slot);
}
return out;
}
// AUTOPIN 热度持久化:导出各层 LFU 累计频次,扁平 [layer, expert, count, ...]。
// 仅 lfu 策略下 demand 路径计频;驱逐不删 freq故即该进程全生命周期的路由热度
// 由 Python 侧按增量差分合并进 usage 文件(dual decode 无 Python 物化点,这是唯一计频来源)。
std::vector<long> real_freq_dump() {
std::lock_guard<std::mutex> lk(g_real_mutex);
std::vector<long> out;
for (auto& kv : g_real)
for (auto& p : kv.second.freq) {
out.push_back(static_cast<long>(kv.first));
out.push_back(static_cast<long>(p.first));
out.push_back(static_cast<long>(p.second));
}
return out;
}
// 复刻 _alloc_slote 为 miss不会已在 e2rfree 优先,否则 LFU 驱逐复用受害者槽。
// 返回 slot-1 表示无可驱逐(超容量,不该在 dual 发生)。调用方须持 g_real_mutex。
static int alloc_slot_locked(RealLayer& c, int e, const std::unordered_set<int>& current) {
auto it = c.e2r.find(e);
if (it != c.e2r.end()) return it->second;
int slot;
if (!c.free_rows.empty()) {
slot = c.free_rows.front();
c.free_rows.erase(c.free_rows.begin()); // free.pop(0)
} else {
int victim = choose_victim_locked(c, current);
if (victim < 0) return -1;
slot = c.e2r[victim];
c.e2r.erase(victim);
for (auto oit = c.order.begin(); oit != c.order.end(); ++oit)
if (*oit == victim) { c.order.erase(oit); break; }
}
c.e2r[e] = slot;
c.order.push_back(e);
return slot;
}
// LFU 频次 bumpcanonical本次全部唯一专家各 +1+ decay。调用方须持 g_real_mutex。
static void note_access_locked(RealLayer& c, const std::vector<int>& uniq_access,
bool lfu, int decay_interval) {
if (!lfu) return;
for (int e : uniq_access) c.freq[e] += 1;
c.access += static_cast<long>(uniq_access.size());
if (decay_interval > 0 && c.access >= decay_interval) {
for (auto it = c.freq.begin(); it != c.freq.end();) {
it->second /= 2;
if (it->second == 0) it = c.freq.erase(it);
else ++it;
}
c.access = 0;
}
}
// 核心状态机(纯 CPU、无 I/O给定 host inds(ip[n]) + 侧区快照(side),算 local、分配 miss 槽,
// 把需要落盘的新放入 (expert, slot) 追加到 placements字节由调用方在锁外并行 pread 落池)。
// 返回 local(int32 vector)。stats: [hitpos, misspos, loads(=placements 数), unplaced]。
static std::vector<int32_t> demand_core_locked(
RealLayer& c, const uint32_t* ip, size_t n, const std::unordered_map<int, int>& side,
bool lfu, int decay_interval, std::vector<std::pair<int, int>>& placements, long stats[4]) {
std::vector<int32_t> local(n, -1);
std::vector<int> uniq_miss, access_order;
std::unordered_set<int> miss_seen, access_seen;
int hitpos = 0;
// pass1算命中(侧区覆盖真实区)、收集 miss(首见序)、收集唯一访问(freq)。
for (size_t i = 0; i < n; ++i) {
int e = static_cast<int>(ip[i]);
if (access_seen.insert(e).second) access_order.push_back(e);
auto sit = side.find(e);
if (sit != side.end()) { local[i] = sit->second; ++hitpos; continue; }
auto rit = c.e2r.find(e);
if (rit != c.e2r.end()) { local[i] = rit->second; ++hitpos; continue; }
if (miss_seen.insert(e).second) uniq_miss.push_back(e);
}
note_access_locked(c, access_order, lfu, decay_interval);
// pass2miss 分配槽不落盘。current = 本前向全部唯一路由专家(命中+miss):绝不驱逐本前向要读的
// 任何专家的槽(否则真实区命中专家的槽被 miss 复写 → 脏字节。比 Python 仅护 miss 更严格、更正确)。
std::unordered_map<int, int> new_slot;
long unplaced = 0;
for (int e : uniq_miss) {
int slot = alloc_slot_locked(c, e, access_seen);
if (slot < 0) {
// 超容量:可驱逐槽(= cap pinned 本前向命中数)不够安放全部 miss。只能落 0 号槽,
// 即拿别的专家的权重参与计算 → 该前向逐位错算。调用方须靠 stats[3] 拦截并回退到
// host/fetch 路径;这里只保证「绝不静默」。
new_slot[e] = 0;
++unplaced;
continue;
}
new_slot[e] = slot;
placements.emplace_back(e, slot);
}
// pass3回填 miss 位置。
int misspos = 0;
for (size_t i = 0; i < n; ++i) {
if (local[i] < 0) {
auto it = new_slot.find(static_cast<int>(ip[i]));
local[i] = (it != new_slot.end()) ? it->second : 0;
++misspos;
}
}
stats[0] = hitpos; stats[1] = misspos; stats[2] = static_cast<long>(placements.size());
stats[3] = unplaced;
return local;
}
// demand 全接管inds 惰性(内部 eval 一次=1 次同步)side_gen 指定侧区代pool_list 为 _segs 顺序
// 的 per-key 池数组(已 eval、指针稳定)。返回 local(int32, inds.shape)。
mx::array demand_dual(
const mx::array& inds, const std::vector<mx::array>& pool_list,
const std::vector<int>& seg_nbytes, int layer, int side_gen, const std::string& path,
int stride, int cap, bool lfu, int decay_interval, mx::StreamOrDevice s = {}) {
if (seg_nbytes.size() != pool_list.size())
throw std::invalid_argument("demand_dual: seg_nbytes.size() != pool_list.size()");
double t0 = g_dt_on ? dt_now_us() : 0;
// 关键:inds 常是 argpartition(...)[..., -k:] 切片 → 非连续 strided 视图(每行按父数组 stride E
// 偏移)。若直接按连续读 data(),仅首行(token0)正确,后续 token 读到错位内存 → 装错专家、seq≥2 全错
// (投机 verify 全体受害)。contiguous() 强制物化为连续,读取才逐位正确。
mx::array ids = mx::contiguous(inds);
ids.eval(); // 唯一同步
size_t n = ids.size();
const uint32_t* ip = ids.data<uint32_t>();
double t1 = g_dt_on ? dt_now_us() : 0;
std::vector<uint8_t*> ptrs;
ptrs.reserve(pool_list.size());
for (auto& a0 : pool_list) { mx::array a = a0; a.eval(); ptrs.push_back(a.data<uint8_t>()); }
double t2 = g_dt_on ? dt_now_us() : 0;
std::unordered_map<int, int> side = sideregion_snapshot(layer, side_gen); // 侧区快照(该代)
double ta = g_dt_on ? dt_now_us() : 0;
long stats[4];
std::vector<int32_t> local;
std::vector<std::pair<int, int>> placements; // (expert, slot):锁外并行落盘
double tb;
{
std::lock_guard<std::mutex> lk(g_real_mutex);
tb = g_dt_on ? dt_now_us() : 0;
RealLayer& c = g_real[layer];
real_ensure_locked(c, cap);
local = demand_core_locked(c, ip, n, side, lfu, decay_interval, placements, stats);
}
// 锁外:把 miss 专家的字节 pread 落真实区槽。复刻基线并发模型——多 miss 派给 BgReader worker
// 并行 pread高优队列、直写池段行无主线程 tmp/memcpy本层等待其完成并行 → 远快于串行)。
static const bool kSkipIO = []() {
const char* e = std::getenv("DEMAND_SKIP_IO");
return e && e[0] == '1';
}();
if (!placements.empty() && !kSkipIO) {
std::vector<long> seg_off(seg_nbytes.size()), seg_nb(seg_nbytes.size());
long acc = 0;
for (size_t k = 0; k < seg_nbytes.size(); ++k) {
seg_off[k] = acc; seg_nb[k] = seg_nbytes[k]; acc += seg_nbytes[k];
}
std::vector<long> tickets;
tickets.reserve(placements.size());
for (auto& pr : placements) {
long tk = g_demand_ticket.fetch_add(1);
bg_pread_into_pool(pool_list, seg_off, seg_nb, pr.second, pr.first, path,
static_cast<long>(stride), tk, /*prio=*/1,
/*nocache=*/false); // demand route 读走 cache段偏移非页对齐
tickets.push_back(tk);
}
for (long tk : tickets) bg_reader_wait(tk); // 并行 pread 完成 → 池槽字节就绪
}
double t3 = g_dt_on ? dt_now_us() : 0;
{
std::lock_guard<std::mutex> lk(g_dstat_mutex);
g_d_last[0] = stats[0]; g_d_last[1] = stats[1]; g_d_last[2] = stats[2];
g_d_last[3] = (stats[1] == 0) ? 0 : 1;
g_d_last[4] = stats[3];
}
if (stats[3] > 0) {
static std::atomic<long> warned{0};
long k = warned.fetch_add(1);
if (k < 8 || k % 1000 == 0)
fprintf(stderr,
"[DEMAND_DUAL] 错算layer=%d 有 %ld 个专家分不到槽(落 0 号槽)。"
"inds=%zu cap=%d pinned=%d —— 该前向输出不可信,请调小 PREFILL_CHUNK "
"或调大 EXPERT_SLOTS / 调小 AUTOPIN_BUDGET_FRAC。\n",
layer, stats[3], n, cap, real_pinned_count(layer));
}
mx::array out = mx::array(local.data(), ids.shape(), mx::int32);
if (g_dt_on) {
double t4 = dt_now_us();
g_dt[0] += t1 - t0; g_dt[1] += t2 - t1; g_dt[2] += ta - t2;
g_dt[3] += tb - ta; g_dt[4] += t3 - tb; g_dt[5] += t4 - t3;
}
return out;
}
// 取本次 demand 统计 [hitpos, misspos, loads, fallback01, unplaced](主线程串行,安全)。
std::vector<long> demand_last_stats() {
std::lock_guard<std::mutex> lk(g_dstat_mutex);
return {g_d_last[0], g_d_last[1], g_d_last[2], g_d_last[3], g_d_last[4]};
}
// 测试壳:纯状态推进(不 pread/不侧区),把 experts_flat 当一次 demand 的 inds返回 local 槽位。
// 供 LFU 驱逐语义逐步等价单测(与 Python 参考对拍)。
std::vector<int> real_debug_place(int layer, const std::vector<int>& experts_flat, int cap,
bool lfu, int decay_interval) {
std::vector<uint32_t> u(experts_flat.begin(), experts_flat.end());
long stats[4];
std::vector<std::pair<int, int>> placements;
std::lock_guard<std::mutex> lk(g_real_mutex);
RealLayer& c = g_real[layer];
real_ensure_locked(c, cap);
std::unordered_map<int, int> empty_side;
auto local = demand_core_locked(c, u.data(), u.size(), empty_side, lfu, decay_interval,
placements, stats);
return std::vector<int>(local.begin(), local.end());
}