Skip to content

Commit 19446c0

Browse files
howard0suCopilot
andcommitted
feat: port --freq and --collect-routing from StreamMoE
Add expert frequency tracking and binary routing data collection for training MLP expert predictors. New features: - --freq: prints per-layer expert frequency analysis at shutdown - --collect-routing <path>: writes binary routing data (hidden states + expert selections) for offline predictor training New files: - server/src/common/moe_routing_collector.{h,cpp}: thread-safe binary writer for per-token routing decisions - scripts/train_predictor.py: MLP/linear predictor training script (ported from StreamMoE, supports CUDA, mlp/linear/cross variants) Binary format per sample: [int32 layer_idx, int32 K, float32[n_embd] hidden, int32[K] experts] Hooks added in qwen35moe pipelined decode (cached + dynamic paths), qwen35moe prefill, laguna hybrid decode, and laguna hybrid prefill. Tested: 1K tokens → 38.5% top-4 hit rate (mlp-cross), 2x temporal baseline. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2012d89 commit 19446c0

15 files changed

Lines changed: 1020 additions & 0 deletions

scripts/train_predictor.py

Lines changed: 726 additions & 0 deletions
Large diffs are not rendered by default.

server/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ add_library(dflash_common STATIC
265265
src/common/moe_hybrid_stream.cpp
266266
src/common/cold_ffn_cpu.cpp
267267
src/common/moe_hybrid_swap_manager.cpp
268+
src/common/moe_routing_collector.cpp
268269
src/qwen35/layer_split_forward.cpp
269270
src/qwen35/qwen35_target_shard_ipc.cpp
270271
src/qwen35/qwen35_target_shard_ipc_daemon.cpp

server/src/common/model_backend.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,16 @@ struct ModelBackend {
323323
// the server before startup.
324324
virtual bool supports_remote_draft() const { return false; }
325325

326+
// ── Routing data collection ──────────────────────────────────────
327+
// Set an external routing collector that the backend will call for each
328+
// token/layer during decode (hidden state + expert IDs). Used by
329+
// --collect-routing for predictor training data.
330+
virtual void set_routing_collector(class MoeRoutingCollector *) {}
331+
332+
// Get the current routing stats (if tracked). Returns nullptr if the
333+
// backend does not support routing stats or they are not enabled.
334+
virtual const struct MoeHybridRoutingStats * get_routing_stats() const { return nullptr; }
335+
326336
// ── Cleanup ──────────────────────────────────────────────────────
327337
// Release all resources (weights, cache, snapshots, drafter).
328338
// Called by run_daemon() before returning.

server/src/common/moe_hybrid_routing_stats.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include <algorithm>
44
#include <cstdio>
55
#include <cstdlib>
6+
#include <cstring>
67
#include <fstream>
78
#include <numeric>
89

@@ -118,6 +119,69 @@ std::vector<int> MoeHybridRoutingStats::hot_experts(int layer_idx, int hot_count
118119
return ranked;
119120
}
120121

122+
void MoeHybridRoutingStats::print_freq_analysis() const {
123+
if (n_layer <= 0 || n_expert <= 0 || counts.empty()) return;
124+
125+
uint64_t total_all = 0;
126+
for (int il = 0; il < n_layer; ++il) total_all += layer_totals[(size_t)il];
127+
if (total_all == 0) return;
128+
129+
// Estimate total tokens: layer_totals[0] / n_expert_used
130+
uint64_t total_tokens = (n_expert_used > 0 && layer_totals[0] > 0)
131+
? layer_totals[0] / (uint64_t)n_expert_used : 0;
132+
double avg_k = (n_layer > 0 && total_tokens > 0)
133+
? (double)total_all / ((double)n_layer * (double)total_tokens) : 0.0;
134+
135+
std::fprintf(stderr, "\n=== Expert Frequency Analysis ===\n");
136+
std::fprintf(stderr, "Tokens tracked: %llu, avg-K=%.2f\n\n",
137+
(unsigned long long)total_tokens, avg_k);
138+
139+
int experts_for_80_total = 0;
140+
std::vector<uint64_t> sorted((size_t)n_expert);
141+
142+
for (int il = 0; il < n_layer; ++il) {
143+
uint64_t tact = layer_totals[(size_t)il];
144+
if (tact == 0) {
145+
std::fprintf(stderr, "Layer %2d: 0 unique\n", il);
146+
continue;
147+
}
148+
for (int ie = 0; ie < n_expert; ++ie)
149+
sorted[(size_t)ie] = counts[index_of(il, ie)];
150+
std::sort(sorted.begin(), sorted.end(), std::greater<uint64_t>());
151+
152+
int unique = 0;
153+
for (int ie = 0; ie < n_expert; ++ie) if (sorted[(size_t)ie] > 0) unique++;
154+
155+
uint64_t cum = 0;
156+
uint64_t top10 = 0, top30 = 0, top60 = 0;
157+
int n50 = 0, n80 = 0, n90 = 0;
158+
for (int ie = 0; ie < n_expert; ++ie) {
159+
cum += sorted[(size_t)ie];
160+
if (ie == 9) top10 = cum;
161+
if (ie == 29) top30 = cum;
162+
if (ie == 59) top60 = cum;
163+
if (!n50 && cum * 100 >= tact * 50) n50 = ie + 1;
164+
if (!n80 && cum * 100 >= tact * 80) n80 = ie + 1;
165+
if (!n90 && cum * 100 >= tact * 90) n90 = ie + 1;
166+
}
167+
std::fprintf(stderr, "Layer %2d: %3d unique, top-10 %.0f%%, top-30 %.0f%%, top-60 %.0f%% "
168+
"(50%%@%d, 80%%@%d, 90%%@%d)\n",
169+
il, unique,
170+
100.0 * (double)top10 / (double)tact,
171+
100.0 * (double)top30 / (double)tact,
172+
100.0 * (double)top60 / (double)tact,
173+
n50, n80, n90);
174+
experts_for_80_total += n80;
175+
}
176+
177+
double avg80 = (n_layer > 0) ? (double)experts_for_80_total / n_layer : 0.0;
178+
std::fprintf(stderr, "\n--- Overall Summary ---\n");
179+
std::fprintf(stderr, "80%% hit rate: %d experts pinned (avg %.0f/layer)\n",
180+
experts_for_80_total, avg80);
181+
std::fprintf(stderr, "Model: %d layers x %d experts, top-%d routing\n",
182+
n_layer, n_expert, n_expert_used);
183+
}
184+
121185
bool MoeHybridRoutingStats::save_csv(const std::string & path, std::string * err) const {
122186
if (n_layer <= 0 || n_expert <= 0 || counts.size() != (size_t)n_layer * (size_t)n_expert) {
123187
if (err) *err = "routing stats not initialized";

server/src/common/moe_hybrid_routing_stats.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ struct MoeHybridRoutingStats {
3838
std::vector<int> ranked_experts(int layer_idx) const;
3939
std::vector<int> hot_experts(int layer_idx, int hot_count) const;
4040

41+
// Print StreamMoE-style frequency analysis to stderr.
42+
void print_freq_analysis() const;
43+
4144
bool save_csv(const std::string & path, std::string * err = nullptr) const;
4245
static bool load_csv(const std::string & path,
4346
MoeHybridRoutingStats & out,
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#include "moe_routing_collector.h"
2+
3+
#include <cstdio>
4+
#include <cstring>
5+
6+
namespace dflash::common {
7+
8+
bool MoeRoutingCollector::open(const std::string & path) {
9+
std::lock_guard<std::mutex> lock(mu_);
10+
if (fd_) {
11+
std::fclose(fd_);
12+
fd_ = nullptr;
13+
}
14+
fd_ = std::fopen(path.c_str(), "wb");
15+
if (!fd_) {
16+
std::fprintf(stderr, "[routing-collector] failed to open '%s': %s\n",
17+
path.c_str(), std::strerror(errno));
18+
return false;
19+
}
20+
samples_ = 0;
21+
std::fprintf(stderr, "[routing-collector] collecting routing data to '%s'\n", path.c_str());
22+
return true;
23+
}
24+
25+
void MoeRoutingCollector::record(int layer_idx, const float * hidden, int n_embd,
26+
const int32_t * expert_ids, int K) {
27+
std::lock_guard<std::mutex> lock(mu_);
28+
if (!fd_) return;
29+
30+
int32_t li = layer_idx;
31+
int32_t ki = K;
32+
std::fwrite(&li, sizeof(int32_t), 1, fd_);
33+
std::fwrite(&ki, sizeof(int32_t), 1, fd_);
34+
std::fwrite(hidden, sizeof(float), (size_t)n_embd, fd_);
35+
std::fwrite(expert_ids, sizeof(int32_t), (size_t)K, fd_);
36+
samples_++;
37+
}
38+
39+
void MoeRoutingCollector::close() {
40+
std::lock_guard<std::mutex> lock(mu_);
41+
if (!fd_) return;
42+
std::fclose(fd_);
43+
fd_ = nullptr;
44+
std::fprintf(stderr, "[routing-collector] closed, %d samples written\n", samples_);
45+
}
46+
47+
} // namespace dflash::common
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// MoE routing data collector — writes per-token binary routing data for
2+
// predictor training (gate input hidden states + expert selections).
3+
//
4+
// Binary format per sample (matches StreamMoE train_predictor.py format):
5+
// int32 layer_idx
6+
// int32 K (number of selected experts)
7+
// float32[n_embd] hidden_state (gate input / post-norm hidden for this layer)
8+
// int32[K] expert_indices (actual routing result)
9+
//
10+
// Enable via --collect-routing <path> on the server CLI or via env var
11+
// DFLASH_COLLECT_ROUTING=<path>.
12+
13+
#pragma once
14+
15+
#include <cstdint>
16+
#include <cstdio>
17+
#include <mutex>
18+
#include <string>
19+
20+
namespace dflash::common {
21+
22+
class MoeRoutingCollector {
23+
public:
24+
MoeRoutingCollector() = default;
25+
~MoeRoutingCollector() { close(); }
26+
27+
MoeRoutingCollector(const MoeRoutingCollector &) = delete;
28+
MoeRoutingCollector & operator=(const MoeRoutingCollector &) = delete;
29+
30+
// Open the output file. Returns false on failure.
31+
bool open(const std::string & path);
32+
33+
// Record one routing sample: gate input + expert selection for one token at one layer.
34+
// hidden must point to n_embd floats; expert_ids to K int32s.
35+
void record(int layer_idx, const float * hidden, int n_embd,
36+
const int32_t * expert_ids, int K);
37+
38+
// Close and flush the output file. Prints summary to stderr.
39+
void close();
40+
41+
bool is_open() const { return fd_ != nullptr; }
42+
int sample_count() const { return samples_; }
43+
44+
private:
45+
std::FILE * fd_ = nullptr;
46+
int samples_ = 0;
47+
std::mutex mu_;
48+
};
49+
50+
} // namespace dflash::common

server/src/laguna/laguna_backend.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "../common/moe_hybrid_storage.h"
2020
#include "../common/moe_hybrid_routing_stats.h"
2121
#include "../common/moe_hybrid_swap_manager.h"
22+
#include "../common/moe_routing_collector.h"
2223
#include "common/step_graph.h"
2324

2425
#include "ggml-cuda.h"
@@ -1426,6 +1427,15 @@ bool LagunaBackend::hybrid_forward_one_token(int32_t tok, int kv_pos,
14261427
}
14271428
}
14281429

1430+
// Collect routing data for predictor training
1431+
if (routing_collector_) {
1432+
std::vector<float> gate_input((size_t)w_.n_embd);
1433+
ggml_backend_tensor_get(layer_sg.ffn_post, gate_input.data(), 0,
1434+
sizeof(float) * (size_t)w_.n_embd);
1435+
routing_collector_->record(il, gate_input.data(), w_.n_embd,
1436+
selected.data(), (int)selected.size());
1437+
}
1438+
14291439
// Hybrid FFN: hot on GPU, cold on CPU, combine on GPU
14301440
auto & storage = moe_hybrid_->layers[(size_t)il];
14311441
{ int _lc=0; for (int _k=0;_k<(int)selected.size();++_k){ int _g=selected[(size_t)_k];
@@ -1661,6 +1671,18 @@ GenerateResult LagunaBackend::generate_hybrid(const GenerateRequest & req,
16611671
}
16621672
}
16631673

1674+
// Collect routing data for predictor training (prefill path)
1675+
if (routing_collector_) {
1676+
for (int i = 0; i < chunk_len; ++i) {
1677+
routing_collector_->record(
1678+
il,
1679+
chunk_post.data() + (size_t)i * (size_t)w_.n_embd,
1680+
w_.n_embd,
1681+
chunk_selected.data() + (size_t)i * (size_t)n_expert_used,
1682+
n_expert_used);
1683+
}
1684+
}
1685+
16641686
// Batched hybrid FFN evaluation
16651687
auto & storage = moe_hybrid_->layers[(size_t)il];
16661688
MoeHybridConfig chunk_cfg = make_moe_hybrid_config(w_);

server/src/laguna/laguna_backend.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "../common/moe_hybrid_routing_stats.h"
1818
#include "../common/moe_hybrid_swap_manager.h"
1919
#include "../common/moe_hybrid_stream.h"
20+
#include "../common/moe_routing_collector.h"
2021

2122
#include "ggml.h"
2223
#include "ggml-backend.h"
@@ -73,6 +74,9 @@ class LagunaBackend : public ModelBackend {
7374

7475
void shutdown() override;
7576

77+
void set_routing_collector(MoeRoutingCollector * c) override { routing_collector_ = c; }
78+
const MoeHybridRoutingStats * get_routing_stats() const override { return routing_stats_.get(); }
79+
7680
private:
7781
LagunaBackendArgs args_;
7882
ggml_backend_t backend_ = nullptr;
@@ -98,6 +102,7 @@ class LagunaBackend : public ModelBackend {
98102
MoeHybridSwapPolicy swap_policy_;
99103
bool hybrid_telemetry_ = false;
100104
MoeHybridStreamEngine stream_engine_;
105+
MoeRoutingCollector * routing_collector_ = nullptr;
101106

102107
bool ensure_slot(int slot);
103108

server/src/qwen35moe/qwen35moe_backend.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ bool Qwen35MoeBackend::ensure_pipe_state(int kv_start) {
394394
pipe_state_.reset();
395395
return false;
396396
}
397+
pipe_state_->routing_collector = routing_collector_;
397398
return true;
398399
}
399400

@@ -867,6 +868,18 @@ GenerateResult Qwen35MoeBackend::generate_impl(const GenerateRequest & req,
867868
}
868869
}
869870

871+
// Collect routing data for predictor training (prefill path)
872+
if (routing_collector_) {
873+
for (int i = 0; i < chunk_len; ++i) {
874+
routing_collector_->record(
875+
il,
876+
chunk_post.data() + (size_t)i * (size_t)hidden,
877+
hidden,
878+
chunk_selected.data() + (size_t)i * (size_t)n_expert_used,
879+
n_expert_used);
880+
}
881+
}
882+
870883
// Hybrid FFN — skip batched path when cold experts exist (CUDA mul_mat_id bug on sm_75)
871884
MoeHybridConfig chunk_cfg = make_moe_hybrid_config(target_weights());
872885
MoeLayerDesc chunk_desc = make_moe_layer_desc(target_weights().layers[(size_t)il]);

0 commit comments

Comments
 (0)