Skip to content

Commit cea8b50

Browse files
authored
Add a hashed perceptron branch predictor
1 parent 236c545 commit cea8b50

3 files changed

Lines changed: 321 additions & 1 deletion

File tree

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
strategy:
2222
matrix:
2323
os: [ubuntu-latest, macos-latest, windows-latest]
24-
predictor: ["bimodal<>", "bimodalN<>", "tage<>", "gshare<>", "gshareN<>", "never_taken", "perceptron<>"]
24+
predictor: ["bimodal<>", "bimodalN<>", "tage<>", "gshare<>", "gshareN<>", "never_taken", "perceptron<>", "hashed_perceptron<>"]
2525
runs-on: ${{ matrix.os }}
2626
steps:
2727
- name: Checkout code

branch_predictor.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "predictors/bimodalN.hpp"
88
#include "predictors/gshareN.hpp"
99
#include "predictors/gshareN_ahead.hpp"
10+
#include "predictors/hashed_perceptron.hpp"
1011
#include "predictors/tutorial/tutorial.hpp"
1112

1213
#ifdef PREDICTOR

predictors/hashed_perceptron.hpp

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
#include "../cbp.hpp"
2+
#include "../harcom.hpp"
3+
#include "common.hpp"
4+
5+
#include <array>
6+
7+
using namespace hcm;
8+
9+
10+
template<
11+
u64 LOGLB = 6, // 64B fetch block
12+
u64 NTABLES = 8, // number of tables
13+
u64 MAXHIST = 100, // maximum global history length
14+
u64 MINHIST = 2, // minimum global history length
15+
u64 WBITS = 4, // signed 4-bit weight (-8 to 7)
16+
u64 LOGTABLE = 13, // 32KB hashed perceptron for P2
17+
u64 LOGP1 = 14, // 4KB gshare for P1
18+
u64 GHIST1 = 6 // P1 gshare history length
19+
>
20+
struct hashed_perceptron : predictor {
21+
static_assert(LOGLB >= 2);
22+
static constexpr u64 LOGLINEINST = LOGLB - 2; // 4B instruction
23+
24+
static constexpr u64 LINEINST = 1ull << LOGLINEINST;
25+
static constexpr u64 NUMHIST = NTABLES - 1; // number of tables using history
26+
27+
static constexpr u64 YBITS = WBITS + std::bit_width(NTABLES-1);
28+
static constexpr u64 TCBITS = 4; // corresponds to SPEED = 16
29+
static constexpr u64 THETABITS = YBITS + TCBITS;
30+
static constexpr u64 PATHBITS = 6;
31+
32+
static_assert(LOGP1 >= LOGLINEINST);
33+
static constexpr u64 index1_bits = LOGP1 - LOGLINEINST;
34+
static_assert(LOGTABLE >= LOGLINEINST);
35+
static constexpr u64 index2_bits = LOGTABLE - LOGLINEINST;
36+
37+
geometric_folds<NUMHIST, MINHIST, MAXHIST, index2_bits> gfolds;
38+
reg<1> true_block = 1;
39+
40+
// ---- for P1 (gshare) ----
41+
reg<GHIST1> global_history1;
42+
reg<index1_bits> index1;
43+
arr<reg<1>, LINEINST> readp1;
44+
reg<LINEINST> p1;
45+
46+
// ---- for P2 (hashed perceptron) ----
47+
// based on ChampSim's implementation
48+
// https://github.com/ChampSim/ChampSim/blob/29b7b368f761fce1309103c17280c847ada44c1e/branch/hashed_perceptron.bpred
49+
arr<reg<index2_bits>, NTABLES> index2;
50+
arr<reg<WBITS, i64>, NTABLES> readw[LINEINST];
51+
arr<reg<YBITS, i64>, LINEINST> yout;
52+
reg<LINEINST> p2;
53+
// dynamic update threshold
54+
// packed counter representing (theta << TCBITS) + tc
55+
reg<THETABITS, i64> theta_and_tc = 10 << TCBITS;
56+
57+
// ---- simulation artifacts ----
58+
u64 num_branch = 0;
59+
u64 block_size = 0;
60+
arr<reg<LOGLINEINST>, LINEINST> branch_offset;
61+
arr<reg<1>, LINEINST> branch_dir;
62+
reg<LINEINST> block_entry;
63+
64+
// ---- RAMs ----
65+
// P2 weight tables
66+
ram<val<WBITS, i64>, (1 << index2_bits)> wtable[NTABLES][LINEINST] {{"P2 weight"}};
67+
68+
// P1 prediction bits
69+
ram<val<1>, (1 << index1_bits)> table1_pred[LINEINST] {"P1 pred"};
70+
// P1 hysteresis bits
71+
zone UPDATE_ONLY;
72+
ram<val<1>, (1 << index1_bits)> table1_hyst[LINEINST] {"P1 hyst"};
73+
74+
void new_block(val<64> inst_pc)
75+
{
76+
val<LOGLINEINST> offset = inst_pc.fo1() >> 2;
77+
block_entry = offset.fo1().decode().concat();
78+
block_entry.fanout(hard<4*LINEINST>{});
79+
block_size = 1;
80+
}
81+
82+
// P1 gshare
83+
val<1> predict1(val<64> inst_pc)
84+
{
85+
inst_pc.fanout(hard<2>{});
86+
new_block(inst_pc);
87+
88+
// index1 = PC ^ ghist1
89+
val<std::max(index1_bits, GHIST1)> lineaddr = inst_pc >> LOGLB;
90+
global_history1.fanout(hard<2>{});
91+
if constexpr (GHIST1 <= index1_bits) {
92+
index1 = lineaddr.fo1() ^ (val<index1_bits>{global_history1} << (index1_bits - GHIST1));
93+
} else {
94+
index1 = global_history1.make_array(val<index1_bits>{}).append(lineaddr.fo1()).fold_xor();
95+
}
96+
index1.fanout(hard<LINEINST+1>{});
97+
98+
// read prediction bits
99+
for (u64 offset=0; offset<LINEINST; offset++) {
100+
readp1[offset] = table1_pred[offset].read(index1);
101+
}
102+
readp1.fanout(hard<2>{});
103+
p1 = readp1.concat();
104+
p1.fanout(hard<LINEINST>{});
105+
106+
// prediction for the first instruction
107+
return (block_entry & p1) != hard<0>{};
108+
}
109+
110+
val<1> reuse_predict1([[maybe_unused]] val<64> inst_pc)
111+
{
112+
// prediction for subsequent instructions
113+
return ((block_entry << block_size) & p1) != hard<0>{};
114+
}
115+
116+
// P2 hashed perceptron
117+
val<1> predict2(val<64> inst_pc)
118+
{
119+
val<index2_bits> lineaddr = inst_pc.fo1() >> LOGLB;
120+
lineaddr.fanout(hard<NTABLES>{});
121+
gfolds.fanout(hard<2>{});
122+
123+
// index2 = PC ^ folded_history
124+
for (u64 i=0; i<NTABLES; i++) {
125+
if (i == 0) {
126+
index2[i] = lineaddr;
127+
} else {
128+
index2[i] = lineaddr ^ gfolds.template get<0>(i-1);
129+
}
130+
}
131+
index2.fanout(hard<2>{});
132+
133+
// read weights, then summed up
134+
for (u64 i=0; i<NTABLES; i++) {
135+
auto dindex2 = index2[i].distribute(wtable[i]);
136+
for (u64 offset=0; offset<LINEINST; offset++) {
137+
readw[offset][i] = wtable[i][offset].read(dindex2[offset].fo1());
138+
}
139+
}
140+
for (u64 offset=0; offset<LINEINST; offset++) {
141+
readw[offset].fanout(hard<2>{});
142+
yout[offset] = readw[offset].fold_add();
143+
}
144+
yout.fanout(hard<2>{});
145+
146+
// per-offset predictions
147+
arr<val<1>, LINEINST> p2bits = [&](u64 offset) {
148+
auto [sign_bit, rest] = split<1, YBITS-1>(yout[offset]);
149+
return sign_bit.fo1();
150+
};
151+
p2 = p2bits.fo1().concat();
152+
p2.fanout(hard<LINEINST>{});
153+
154+
// prediction for the first instruction
155+
val<1> taken = (block_entry & p2) != hard<0>{};
156+
157+
// determine block termination
158+
reuse_prediction(~val<1>{block_entry >> (LINEINST - 1)});
159+
return taken.fo1();
160+
}
161+
162+
val<1> reuse_predict2([[maybe_unused]] val<64> inst_pc)
163+
{
164+
// prediction for subsequent instructions
165+
val<1> taken = ((block_entry << block_size) & p2) != hard<0>{};
166+
167+
// determine block termination
168+
reuse_prediction(~val<1>{block_entry >> (LINEINST - 1 - block_size)});
169+
block_size++;
170+
return taken.fo1();
171+
}
172+
173+
// update interface
174+
void update_condbr(val<64> branch_pc, val<1> taken, [[maybe_unused]] val<64> next_pc)
175+
{
176+
assert(num_branch < LINEINST);
177+
branch_offset[num_branch] = branch_pc.fo1() >> 2;
178+
branch_dir[num_branch] = taken.fo1();
179+
num_branch++;
180+
}
181+
182+
void update_cycle(instruction_info &block_end_info)
183+
{
184+
val<1> &mispredict = block_end_info.is_mispredict;
185+
val<64> &next_pc = block_end_info.next_pc;
186+
187+
// updates for all conditional branches in the predicted block
188+
if (num_branch == 0) {
189+
// no conditional branch in this block
190+
val<1> line_end = block_entry >> (LINEINST - block_size);
191+
// update global history if previous block ended on a mispredicted not-taken branch
192+
// (we are still in the same line, this is the last chunk)
193+
// or if the block ends before the line boundary (unconditional jump)
194+
true_block.fanout(hard<2>{});
195+
val<1> actual_block = ~(true_block & line_end.fo1());
196+
actual_block.fanout(hard<MAXHIST+NUMHIST+3>{});
197+
execute_if(actual_block, [&](){
198+
next_pc.fanout(hard<2>{});
199+
global_history1 = (global_history1 << 1) ^ val<GHIST1>{next_pc >> 2};
200+
gfolds.update(val<PATHBITS>{next_pc >> 2});
201+
true_block = 1;
202+
});
203+
return; // stop here
204+
}
205+
206+
branch_dir.fanout(hard<2>{});
207+
branch_offset.fanout(hard<LINEINST>{});
208+
index1.fanout(hard<LINEINST*3>{});
209+
index2.fanout(hard<LINEINST>{});
210+
yout.fanout(hard<3>{});
211+
theta_and_tc.fanout(hard<2>{});
212+
p2.fanout(hard<2>{});
213+
214+
// masks mapping executed branches to offset
215+
u64 update_valid = (u64(1) << num_branch) - 1;
216+
arr<val<LINEINST>, LINEINST> update_mask = [&](u64 offset){
217+
arr<val<1>, LINEINST> match_offset = [&](u64 i){ return branch_offset[i] == offset; };
218+
return match_offset.fo1().concat() & update_valid;
219+
};
220+
update_mask.fanout(hard<2>{});
221+
222+
// Is there a branch instruction at the offset?
223+
arr<val<1>, LINEINST> is_branch = [&](u64 offset){
224+
return update_mask[offset] != hard<0>{};
225+
};
226+
is_branch.fanout(hard<3>{});
227+
val<LINEINST> branch_mask = is_branch.concat();
228+
branch_mask.fanout(hard<5>{});
229+
230+
// is the outcome of the branch at the offset taken?
231+
val<LINEINST> actualdirs = branch_dir.concat();
232+
actualdirs.fanout(hard<LINEINST>{});
233+
arr<val<1>, LINEINST> branch_taken = [&](u64 offset){
234+
return (actualdirs & update_mask[offset]) != hard<0>{};
235+
};
236+
branch_taken.fanout(hard<NTABLES+1>{});
237+
238+
// is the P2 (=final) prediction correct?
239+
auto p2_split = p2.make_array(val<1>{});
240+
p2_split.fanout(hard<3>{});
241+
arr<val<1>, LINEINST> correct = [&](u64 offset){
242+
return p2_split[offset] == branch_taken[offset];
243+
};
244+
val<LINEINST> correct_mask = correct.fo1().concat();
245+
correct_mask.fanout(hard<3>{});
246+
247+
// is the prediction weak?
248+
auto [theta, tc] = split<YBITS, TCBITS>(theta_and_tc);
249+
theta.fanout(hard<LINEINST>{});
250+
arr<val<1>, LINEINST> weak = [&](u64 offset){
251+
auto absy = select(yout[offset] < hard<0>{}, -yout[offset], yout[offset]);
252+
return absy.fo1() < theta;
253+
};
254+
val<LINEINST> weak_mask = weak.fo1().concat();
255+
weak_mask.fanout(hard<2>{});
256+
257+
// perceptrons are updated if the prediction is wrong or weak
258+
val<LINEINST> train_mask = branch_mask & (~correct_mask | weak_mask);
259+
train_mask.fanout(hard<2>{});
260+
arr<val<1>,LINEINST> train = train_mask.make_array(val<1>{});
261+
262+
// ---- P1 (gshare) update ----
263+
// did P1 and P2 disagree?
264+
val<LINEINST> disagree_mask = (p1 ^ p2) & branch_mask;
265+
disagree_mask.fanout(hard<2>{});
266+
arr<val<1>,LINEINST> disagree = disagree_mask.make_array(val<1>{});
267+
disagree.fanout(hard<2>{});
268+
269+
// read P1 hysteresis if P1 and P2 disagree
270+
arr<val<1>, LINEINST> p1_weak = [&](u64 offset) -> val<1> {
271+
return execute_if(disagree[offset], [&](){
272+
return ~table1_hyst[offset].read(index1); // hyst=0 means weak
273+
});
274+
};
275+
276+
val<1> extra_cycle = (train_mask != hard<0>{}) | (disagree_mask != hard<0>{});
277+
need_extra_cycle(extra_cycle.fo1());
278+
279+
// update P1 prediction if P1 and P2 disagree and hysteresis bit is weak
280+
for (u64 offset=0; offset<LINEINST; offset++) {
281+
execute_if(p1_weak[offset].fo1(), [&](){
282+
table1_pred[offset].write(index1, p2_split[offset]);
283+
});
284+
}
285+
// update P1 hysteresis for executed branches
286+
for (u64 offset=0; offset<LINEINST; offset++) {
287+
execute_if(is_branch[offset], [&](){
288+
table1_hyst[offset].write(index1, ~disagree[offset]);
289+
});
290+
}
291+
292+
// ---- P2 (hashed perceptron) update ----
293+
// weight update
294+
for (u64 offset=0; offset<LINEINST; offset++) {
295+
execute_if(train[offset].fo1(), [&](){
296+
for (u64 i=0; i<NTABLES; i++) {
297+
wtable[i][offset].write(index2[i], update_ctr(readw[offset][i], ~branch_taken[offset]));
298+
}
299+
});
300+
}
301+
302+
// update packed counter
303+
// decrement on weak & correct
304+
// increment on mispredict
305+
theta_and_tc = theta_and_tc - (branch_mask & weak_mask & correct_mask).ones() + (branch_mask & ~correct_mask).ones();
306+
307+
// ---- update global history ----
308+
val<1> line_end = block_entry >> (LINEINST - block_size);
309+
true_block = ~mispredict.fo1() | branch_dir[num_branch-1] | line_end.fo1();
310+
true_block.fanout(hard<MAXHIST+NUMHIST+2>{});
311+
execute_if(true_block, [&](){
312+
next_pc.fanout(hard<2>{});
313+
global_history1 = (global_history1 << 1) ^ val<GHIST1>{next_pc >> 2};
314+
gfolds.update(val<PATHBITS>{next_pc >> 2});
315+
});
316+
317+
num_branch = 0;
318+
}
319+
};

0 commit comments

Comments
 (0)