shubhxho commited on
Commit
d050866
·
verified ·
1 Parent(s): ab861bb

Upload reference/net.rs with huggingface_hub

Browse files
Files changed (1) hide show
  1. reference/net.rs +34 -15
reference/net.rs CHANGED
@@ -87,21 +87,39 @@ fn net() -> &'static Net {
87
  /// two accumulations to rediscover a number computed a microsecond ago is most
88
  /// of what the evaluator does.
89
  ///
90
- /// One `u64` per slot: the top 32 bits of the Zobrist key as a tag, the score in
91
- /// the low 32. The index comes from the low bits of the key, which do not
92
- /// overlap the tag, so a slot can only be claimed by a position that agrees on
93
- /// both halves. An empty slot is all-zero, which is why this lives in BSS and
94
- /// costs the binary nothing.
 
 
 
 
 
 
95
  const CACHE_BITS: usize = 16;
96
  static CACHE: SyncCell<[u64; 1 << CACHE_BITS]> = SyncCell::new([0; 1 << CACHE_BITS]);
 
 
 
 
 
 
97
 
98
  /// Not needed for correctness — a cached score is as valid as the day it was
99
  /// stored — but `bench` and datagen want each position measured from a cold
100
  /// start, and the search's own `clear` is where that is expressed.
101
  pub fn clear_cache() {
102
- let c = unsafe { CACHE.as_mut() };
103
- for e in c.iter_mut() {
104
- *e = 0;
 
 
 
 
 
 
105
  }
106
  }
107
 
@@ -405,15 +423,16 @@ fn propagate(acc: &[i16; H], w: &[i8]) -> i32 {
405
 
406
  /// Evaluation in centipawns, from the side to move's point of view.
407
  pub fn evaluate(pos: &Position) -> i32 {
408
- // Tag 0 is indistinguishable from an empty slot, so the one key in four
409
- // billion whose top half is zero simply never caches. Cheaper than spending
410
- // a bit on a validity flag.
411
  let slot = (pos.key as usize) & ((1 << CACHE_BITS) - 1);
412
- let tag = pos.key & 0xFFFF_FFFF_0000_0000;
 
413
  let c = unsafe { CACHE.as_mut() };
414
  let hit = c[slot];
415
- if tag != 0 && hit & 0xFFFF_FFFF_0000_0000 == tag {
416
- return hit as u32 as i32;
417
  }
418
 
419
  let n = net();
@@ -427,6 +446,6 @@ pub fn evaluate(pos: &Position) -> i32 {
427
  let w = &n.out_w[b * 2 * H..(b + 1) * 2 * H];
428
  let out = propagate(&us, &w[..H]) + propagate(&them, &w[H..]) + n.out_b[b];
429
  let score = (out * SCALE / (QA * QB)).clamp(-20_000, 20_000);
430
- c[slot] = tag | score as u32 as u64;
431
  score
432
  }
 
87
  /// two accumulations to rediscover a number computed a microsecond ago is most
88
  /// of what the evaluator does.
89
  ///
90
+ /// One `u64` per slot, packed as `tag:40 | generation:8 | score:16`. The index
91
+ /// is the low bits of the key and the tag is bits 24 and up, so the two never
92
+ /// overlap: a slot only answers for a position that agrees on both. Scores are
93
+ /// clamped to ±20,000, so sixteen bits hold one exactly.
94
+ ///
95
+ /// The generation is what makes the table cheap to empty. The first version
96
+ /// zeroed all of it, and that memset was large enough to decide the sizing:
97
+ /// 18-bit and 20-bit tables both measured *slower* than 16-bit, because the
98
+ /// clear between bench positions cost more than the extra hits were worth.
99
+ /// Bumping a counter invalidates every entry at once, so the size question is
100
+ /// now about cache footprint alone.
101
  const CACHE_BITS: usize = 16;
102
  static CACHE: SyncCell<[u64; 1 << CACHE_BITS]> = SyncCell::new([0; 1 << CACHE_BITS]);
103
+ static GEN: SyncCell<u8> = SyncCell::new(0);
104
+
105
+ #[inline(always)]
106
+ fn pack(key: u64, gen: u8, score: i32) -> u64 {
107
+ (key >> 24 << 24) | ((gen as u64) << 16) | (score as i16 as u16 as u64)
108
+ }
109
 
110
  /// Not needed for correctness — a cached score is as valid as the day it was
111
  /// stored — but `bench` and datagen want each position measured from a cold
112
  /// start, and the search's own `clear` is where that is expressed.
113
  pub fn clear_cache() {
114
+ let g = unsafe { GEN.as_mut() };
115
+ *g = g.wrapping_add(1);
116
+ // Eight bits of generation wrap after 256 clears, and an entry that old
117
+ // would start answering again. That only happens once every 256 clears, so
118
+ // pay for the real erase then.
119
+ if *g == 0 {
120
+ for e in unsafe { CACHE.as_mut() }.iter_mut() {
121
+ *e = 0;
122
+ }
123
  }
124
  }
125
 
 
423
 
424
  /// Evaluation in centipawns, from the side to move's point of view.
425
  pub fn evaluate(pos: &Position) -> i32 {
426
+ // An erased slot is all-zero, which is a real entry for the one position in
427
+ // a trillion whose key has forty zero bits on top and whose score is zero.
428
+ // That costs a zero instead of a zero; no validity bit is worth the space.
429
  let slot = (pos.key as usize) & ((1 << CACHE_BITS) - 1);
430
+ let gen = unsafe { *GEN.as_ref() };
431
+ let want = pack(pos.key, gen, 0);
432
  let c = unsafe { CACHE.as_mut() };
433
  let hit = c[slot];
434
+ if hit & !0xFFFF == want {
435
+ return hit as u16 as i16 as i32;
436
  }
437
 
438
  let n = net();
 
446
  let w = &n.out_w[b * 2 * H..(b + 1) * 2 * H];
447
  let out = propagate(&us, &w[..H]) + propagate(&them, &w[H..]) + n.out_b[b];
448
  let score = (out * SCALE / (QA * QB)).clamp(-20_000, 20_000);
449
+ c[slot] = pack(pos.key, gen, score);
450
  score
451
  }