diff --git a/Cargo.toml b/Cargo.toml index 22a643b..e73fd96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ default = ["syzygy"] syzygy = [] numa = [] spsa = [] +# Rama Chess Android: load the net from a file (UCI option EvalFile) instead of embedding it +external-net = [] [profile.dev] opt-level = 3 diff --git a/src/main.rs b/src/main.rs index f254eb6..18aa981 100644 --- a/src/main.rs +++ b/src/main.rs @@ -31,6 +31,8 @@ mod bindings; fn main() { lookup::initialize(); nnue::initialize(); + #[cfg(feature = "external-net")] + nnue::init_external_net(); let buffer: std::collections::VecDeque = std::env::args().skip(1).collect(); diff --git a/src/nnue.rs b/src/nnue.rs index facea25..ec05de2 100644 --- a/src/nnue.rs +++ b/src/nnue.rs @@ -292,8 +292,77 @@ struct Parameters { l3_biases: Aligned<[f32; OUTPUT_BUCKETS]>, } +#[cfg(not(feature = "external-net"))] static PARAMETERS: Parameters = unsafe { std::mem::transmute(*include_bytes!(env!("MODEL"))) }; +// Android build (Rama Chess): feature "external-net" - the net is not embedded. It is read from the file +// given by "setoption name EvalFile value ". Until then PARAMETERS points at a zeroed block (never +// touched by a search: "go" refuses to search without a net). +#[cfg(feature = "external-net")] +static PARAMETERS: ParametersRef = ParametersRef; + +#[cfg(feature = "external-net")] +static PARAMETERS_PTR: std::sync::atomic::AtomicPtr = std::sync::atomic::AtomicPtr::new(std::ptr::null_mut()); + +#[cfg(feature = "external-net")] +static NET_LOADED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); + +#[cfg(feature = "external-net")] +struct ParametersRef; + +#[cfg(feature = "external-net")] +impl std::ops::Deref for ParametersRef { + type Target = Parameters; + + #[inline(always)] + fn deref(&self) -> &Parameters { + unsafe { &*PARAMETERS_PTR.load(std::sync::atomic::Ordering::Relaxed) } + } +} + +#[cfg(feature = "external-net")] +pub fn init_external_net() { + if PARAMETERS_PTR.load(std::sync::atomic::Ordering::Acquire).is_null() { + let ptr = unsafe { std::alloc::alloc_zeroed(std::alloc::Layout::new::()) }; + assert!(!ptr.is_null(), "out of memory"); + PARAMETERS_PTR.store(ptr as *mut Parameters, std::sync::atomic::Ordering::Release); + } +} + +#[cfg(feature = "external-net")] +pub fn net_loaded() -> bool { + NET_LOADED.load(std::sync::atomic::Ordering::Acquire) +} + +/// Loads the net file at `path` (must be exactly the size of the net). Call only while no search runs. +#[cfg(feature = "external-net")] +pub fn load_external_net(path: &str) -> Result<(), String> { + use std::io::Read; + let size = std::mem::size_of::(); + let mut file = std::fs::File::open(path).map_err(|e| format!("cannot open {path}: {e}"))?; + let len = file.metadata().map_err(|e| e.to_string())?.len(); + if len != size as u64 { + return Err(format!("{path} has {len} bytes, expected {size}")); + } + let layout = std::alloc::Layout::new::(); + unsafe { + let ptr = std::alloc::alloc(layout); + if ptr.is_null() { + return Err("out of memory".to_string()); + } + if let Err(e) = file.read_exact(std::slice::from_raw_parts_mut(ptr, size)) { + std::alloc::dealloc(ptr, layout); + return Err(format!("cannot read {path}: {e}")); + } + let old = PARAMETERS_PTR.swap(ptr as *mut Parameters, std::sync::atomic::Ordering::AcqRel); + if !old.is_null() { + std::alloc::dealloc(old as *mut u8, layout); + } + } + NET_LOADED.store(true, std::sync::atomic::Ordering::Release); + Ok(()) +} + #[repr(align(64))] #[derive(Clone)] struct Aligned { diff --git a/src/uci.rs b/src/uci.rs index 1862a7a..e8277fe 100644 --- a/src/uci.rs +++ b/src/uci.rs @@ -67,6 +67,15 @@ pub fn message_loop(mut buffer: VecDeque) { ["isready"] => println!("readyok"), + #[cfg(feature = "external-net")] + ["go", ..] if !crate::nnue::net_loaded() => { + println!("info string ERROR: no network loaded, use setoption name EvalFile value "); + println!("bestmove 0000"); + } + #[cfg(feature = "external-net")] + ["bench", ..] | ["eval"] if !crate::nnue::net_loaded() => { + println!("info string ERROR: no network loaded, use setoption name EvalFile value "); + } ["go", tokens @ ..] => go(&mut threads, &settings, &shared, tokens), ["position", tokens @ ..] => position(&mut threads, &settings, tokens), ["setoption", tokens @ ..] => set_option(&mut threads, &mut settings, &shared, tokens), @@ -110,15 +119,24 @@ fn spawn_listener(shared: Arc) -> std::sync::mpsc::Receiver { + while shared.status.get() == Status::RUNNING { + std::thread::sleep(std::time::Duration::from_millis(5)); + } let _ = tx.send("quit".to_string()); + break; } + Ok(_) => (), + Err(_) => continue, // not UTF-8: ignore the line instead of panicking } match message.trim_end() { - "isready" => println!("readyok"), + // Rama Chess: while idle, isready goes through the queue, so readyok comes after a net load + "isready" if shared.status.get() == Status::RUNNING => println!("readyok"), "stop" => shared.status.set(Status::STOPPED), "quit" => { shared.status.set(Status::STOPPED); @@ -151,6 +169,9 @@ fn uci() { println!("option name UCI_Chess960 type check default false"); println!("option name MultiPV type spin default 1 min 1 max {MAX_MOVES}"); + #[cfg(feature = "external-net")] + println!("option name EvalFile type string default "); + #[cfg(feature = "syzygy")] println!("option name SyzygyPath type string default"); @@ -180,6 +201,14 @@ fn reset(threads: &mut ThreadPool, shared: &Arc) { fn go(threads: &mut ThreadPool, settings: &Settings, shared: &Arc, tokens: &[&str]) { let board = &threads.main_thread().board; + + // Rama Chess: no legal move (checkmate / stalemate) - answer at once instead of searching an empty move list + if !board.generate_all_moves().iter().any(|v| board.is_legal(v.mv)) { + println!("info depth 0 score {}", if board.in_check() { "mate 0" } else { "cp 0" }); + println!("bestmove 0000"); + return; + } + let limits = parse_limits(board.side_to_move(), tokens); let time_manager = TimeManager::new(limits, board.fullmove_number(), settings.move_overhead); @@ -310,6 +339,22 @@ fn set_option(threads: &mut ThreadPool, settings: &mut Settings, shared: &Arc println!("info string Loaded Syzygy tablebases with {size} pieces"), None => eprintln!("Failed to load Syzygy tablebases"), }, + #[cfg(feature = "external-net")] + ["name", "EvalFile", "value", path @ ..] => { + let path = path.join(" "); + match crate::nnue::load_external_net(&path) { + Ok(()) => { + // Rebuild the thread data: accumulator caches copy the net's biases when they are created. + let board = threads.main_thread().board.clone(); + threads.clear(); + for thread in threads.iter_mut() { + thread.board = board.clone(); + } + println!("info string loaded network {path}"); + } + Err(e) => println!("info string ERROR: {e}"), + } + } ["name", "UCI_Chess960", "value", v] => { settings.frc = v.parse().unwrap_or_default(); println!("info string set UCI_Chess960 to {v}");