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

218 lines
9.8 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.

// [2] 轻量预取(无 staging仅预热 page cache) + [3] staging 版 miss→hit + STAGING_HPROF 探针。
#include "prefetch.h"
#include "../io/bg_reader.h"
#include <chrono>
#include <functional>
#include <map>
#include <tuple>
#include <unordered_set>
#include <fcntl.h>
#include <unistd.h>
// ====== [2] 轻量预取(无 staging 模式)GPU 完成回调里读 inds、pread 预热 page cache ======
// 命门验证:回调在 command buffer 完成后触发,此时 inds 已算完,读到的是正确值。
class PrefetchOnCompletePrimitive : public mx::Primitive {
public:
PrefetchOnCompletePrimitive(mx::Stream s, std::string path, size_t stride, bool do_read)
: Primitive(s), path_(std::move(path)), stride_(stride), do_read_(do_read) {}
const char* name() const override { return "PrefetchOnCompletePrimitive"; }
void eval_cpu(const std::vector<mx::array>& inputs, std::vector<mx::array>& outputs) override {
outputs[0].set_data(mx::allocator::malloc(outputs[0].nbytes()));
mx::array ids = inputs[0];
ids.eval();
record_and_read(ids.data<uint32_t>(), ids.size(), path_, stride_, do_read_);
}
void eval_gpu(const std::vector<mx::array>& inputs, std::vector<mx::array>& outputs) override {
outputs[0].set_data(mx::allocator::malloc(outputs[0].nbytes()));
mx::array ids = inputs[0]; // by-value保活到回调结束
const uint32_t* ptr = ids.data<uint32_t>(); // 指针此刻有效buffer 已分配),值待 GPU 算完
size_t n = ids.size();
std::string path = path_;
size_t stride = stride_;
bool do_read = do_read_;
auto& enc = mx::metal::get_command_encoder(stream());
MTL::CommandBuffer* cb = enc.get_command_buffer();
cb->addCompletedHandler([ids, ptr, n, path, stride, do_read](MTL::CommandBuffer*) {
// 此时 buffer 已完成 → ptr 指向已算好的 inds 值。ids 按值捕获保证 buffer 不被释放。
record_and_read(ptr, n, path, stride, do_read);
});
}
private:
static void record_and_read(const uint32_t* p, size_t n, const std::string& path,
size_t stride, bool do_read) {
int fd = (do_read && !path.empty()) ? ::open(path.c_str(), O_RDONLY) : -1;
static thread_local std::vector<uint8_t> buf;
if (do_read && buf.size() < stride) buf.resize(stride);
for (size_t i = 0; i < n; ++i) {
int e = static_cast<int>(p[i]);
if (fd >= 0) ::pread(fd, buf.data(), stride, static_cast<off_t>(static_cast<size_t>(e) * stride));
}
if (fd >= 0) ::close(fd);
}
std::string path_;
size_t stride_;
bool do_read_;
};
// 返回一个 dummy 输出挂进图里eval 时触发上面的回调)。
mx::array prefetch_on_complete(
const mx::array& expert_ids,
const std::string& path,
int stride,
bool do_read = true,
mx::StreamOrDevice s = {}) {
return mx::array(
mx::Shape{1},
mx::uint8,
std::make_shared<PrefetchOnCompletePrimitive>(
mx::to_stream(s), path, static_cast<size_t>(stride), do_read),
std::vector<mx::array>{expert_ids});
}
// ---- handler 触发时刻探针(STAGING_HPROF):记录每次 staging 完成回调被 Metal 触发的时刻 ----
// 用于实测"完成回调到底扎堆在 eval 尾、还是逐层铺开"。仅诊断用,默认关。
static std::mutex g_hprof_mutex;
static std::vector<std::tuple<long, int, double>> g_hprof_log; // (gen, layer, t_fire_seconds)
static bool g_hprof_on = false;
static inline double hprof_steady_now() {
return std::chrono::duration<double>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
// ====== [3] staging 版 miss→hithandler pread 进 per-layer staging + 记录 (expert→row) ======
static std::mutex g_stg_mutex;
// layer -> (gen, [(expert, row)])handler 原子写 gen+映射,主线程按 gen 匹配 buffer 后 take。
static std::map<int, std::pair<long, std::vector<std::pair<int, int>>>> g_stg_ready;
class PrefetchStagingPrimitive : public mx::Primitive {
public:
// 方案Bexpert_ids 是"预测宽集合"(top-N按门控分降序)resident 是目标层当前常驻专家。
// handler 在回调里过滤掉常驻、按降序取前 cap 个缺口 pread 进 stagingcap=buffer 行数)。
// 这样预测可以很宽(高 recall),而 staging 内存只按 cap 预留(小,覆盖缺口分布即可)。
PrefetchStagingPrimitive(mx::Stream s, int layer, long gen, std::string path, size_t stride,
std::vector<int> resident, int cap, bool parallel)
: Primitive(s), layer_(layer), gen_(gen), path_(std::move(path)), stride_(stride),
resident_(std::move(resident)), cap_(cap), parallel_(parallel) {}
const char* name() const override { return "PrefetchStagingPrimitive"; }
void eval_cpu(const std::vector<mx::array>& inputs, std::vector<mx::array>& outputs) override {
outputs[0].set_data(mx::allocator::malloc(outputs[0].nbytes()));
mx::array ids = inputs[0]; ids.eval();
mx::array stg = inputs[1]; stg.eval();
fill(ids.data<uint32_t>(), ids.size(), stg.data<uint8_t>(), layer_, gen_, path_, stride_,
resident_, cap_);
}
void eval_gpu(const std::vector<mx::array>& inputs, std::vector<mx::array>& outputs) override {
outputs[0].set_data(mx::allocator::malloc(outputs[0].nbytes()));
mx::array ids = inputs[0];
mx::array stg = inputs[1];
const uint32_t* idp = ids.data<uint32_t>();
uint8_t* sp = stg.data<uint8_t>();
size_t n = ids.size();
int layer = layer_; long gen = gen_; std::string path = path_; size_t stride = stride_;
std::vector<int> resident = resident_; int cap = cap_; bool parallel = parallel_;
auto& enc = mx::metal::get_command_encoder(stream());
MTL::CommandBuffer* cb = enc.get_command_buffer();
cb->addCompletedHandler(
[ids, stg, idp, sp, n, layer, gen, path, stride, resident, cap, parallel](MTL::CommandBuffer*) {
// 探针:记录本回调被 Metal 触发的时刻(= 该层 pread 能开始跑的时刻)。
if (g_hprof_on) {
double t = hprof_steady_now();
std::lock_guard<std::mutex> lk(g_hprof_mutex);
g_hprof_log.emplace_back(gen, layer, t);
}
// ids/stg 按值捕获保活 buffer派后台线程时再拷一份保活到 fill 跑完。
if (parallel) {
bg_submit_task([ids, stg, idp, sp, n, layer, gen, path, stride, resident, cap]() {
fill(idp, n, sp, layer, gen, path, stride, resident, cap);
});
} else {
fill(idp, n, sp, layer, gen, path, stride, resident, cap);
}
});
}
private:
static void fill(const uint32_t* idp, size_t n, uint8_t* stg,
int layer, long gen, const std::string& path, size_t stride,
const std::vector<int>& resident, int cap) {
int fd = ::open(path.c_str(), O_RDONLY);
if (fd < 0) return;
std::unordered_set<int> res(resident.begin(), resident.end());
std::unordered_set<int> done; // 去重:同一缺口只 pread 一次
std::vector<std::pair<int, int>> ready;
ready.reserve(static_cast<size_t>(cap));
int row = 0; // 写入的 staging 行(≤ cap
for (size_t i = 0; i < n && row < cap; ++i) {
int e = static_cast<int>(idp[i]);
if (res.count(e) || !done.insert(e).second) continue; // 已常驻/已取过 → 跳过
if (::pread(fd, stg + static_cast<size_t>(row) * stride, stride,
static_cast<off_t>(static_cast<size_t>(e) * stride))
== static_cast<ssize_t>(stride)) {
ready.emplace_back(e, row);
++row;
}
}
::close(fd);
std::lock_guard<std::mutex> lk(g_stg_mutex);
g_stg_ready[layer] = {gen, std::move(ready)}; // 原子gen 与映射一起写
}
int layer_;
long gen_;
std::string path_;
size_t stride_;
std::vector<int> resident_; // 目标层提交时刻的常驻专家快照(过滤用)
int cap_; // staging buffer 行数上限
bool parallel_; // true: fill 派后台线程池并行false: 回调线程同步(旧行为)
};
mx::array prefetch_into_staging(
const mx::array& staging, const mx::array& expert_ids, int layer, long gen,
const std::string& path, int stride, const std::vector<int>& resident, int cap,
bool parallel, mx::StreamOrDevice s = {}) {
return mx::array(
mx::Shape{1}, mx::uint8,
std::make_shared<PrefetchStagingPrimitive>(
mx::to_stream(s), layer, gen, path, static_cast<size_t>(stride), resident, cap, parallel),
std::vector<mx::array>{expert_ids, staging});
}
// 取走某层就绪记录:[gen, e0,r0,e1,r1,...](首元素是 generation空表示无就绪。
std::vector<long> prefetch_staging_take(int layer) {
std::lock_guard<std::mutex> lk(g_stg_mutex);
std::vector<long> out;
auto it = g_stg_ready.find(layer);
if (it != g_stg_ready.end()) {
out.push_back(it->second.first); // gen
for (auto& p : it->second.second) { out.push_back(p.first); out.push_back(p.second); }
g_stg_ready.erase(it);
}
return out;
}
// ---- handler 触发时刻探针接口 ----
void staging_hprof_enable(bool on) {
std::lock_guard<std::mutex> lk(g_hprof_mutex);
g_hprof_on = on;
g_hprof_log.clear(); // 开启即清零,便于每次采集干净
}
double staging_hprof_now() { return hprof_steady_now(); } // 与日志同一时钟,供 Python 标 eval 边界
// 扁平返回 [gen0,layer0,t0, gen1,layer1,t1, ...](避开 nanobind tuple caster
std::vector<double> staging_hprof_get() {
std::lock_guard<std::mutex> lk(g_hprof_mutex);
std::vector<double> out;
out.reserve(g_hprof_log.size() * 3);
for (auto& r : g_hprof_log) {
out.push_back(static_cast<double>(std::get<0>(r)));
out.push_back(static_cast<double>(std::get<1>(r)));
out.push_back(std::get<2>(r));
}
return out;
}