1use anyhow::Result;
16use fx_durable_ga::{
17 bootstrap,
18 models::{
19 Crossover, Distribution, Encodeable, Evaluator, FitnessGoal, GeneBounds, Mutagen,
20 MutationRate, Request, Schedule, Selector, Temperature, Terminated,
21 },
22 register_event_handlers, register_job_handlers,
23};
24use fx_mq_jobs::FX_MQ_JOBS_SCHEMA_NAME;
25use fx_mq_jobs::Queries;
26use serde::Deserialize;
27use sqlx::postgres::PgPoolOptions;
28use std::time::Duration;
29use std::{env, sync::Arc};
30use tracing::Level;
31use uuid::Uuid;
32
33const WORKERS: usize = 4;
34const FITNESS_TARGET: f64 = 0.1;
35
36#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
37pub enum ActivationFunction {
38 Relu,
39 Gelu,
40 Sigmoid,
41}
42
43impl std::fmt::Display for ActivationFunction {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::Relu => write!(f, "relu"),
47 Self::Gelu => write!(f, "gelu"),
48 Self::Sigmoid => write!(f, "sigmoid"),
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
54struct NeuralArchitecture {
55 pub hidden_size: usize,
56 pub num_hidden_layers: usize,
57 pub activation_fn: ActivationFunction,
58 pub use_bias: bool,
59 pub learning_rate: f64,
60}
61
62impl Encodeable for NeuralArchitecture {
63 const NAME: &'static str = "neural_architecture";
64
65 type Phenotype = NeuralArchitecture;
66
67 fn morphology() -> Vec<GeneBounds> {
68 vec![
69 GeneBounds::integer(0, 3, 4).unwrap(), GeneBounds::integer(0, 7, 8).unwrap(), GeneBounds::integer(0, 2, 3).unwrap(), GeneBounds::integer(0, 1, 2).unwrap(), GeneBounds::integer(0, 2, 3).unwrap(), ]
75 }
76
77 fn encode(&self) -> Vec<i64> {
78 let hidden_size_idx = match self.hidden_size {
79 32 => 0,
80 64 => 1,
81 128 => 2,
82 256 => 3,
83 _ => 1,
84 };
85
86 let layers_idx = (self.num_hidden_layers - 1).min(2) as i64;
87
88 let activation_idx = match self.activation_fn {
89 ActivationFunction::Relu => 0,
90 ActivationFunction::Gelu => 1,
91 ActivationFunction::Sigmoid => 2,
92 };
93
94 let bias_idx = if self.use_bias { 1 } else { 0 };
95
96 let lr_idx = if self.learning_rate <= 1e-4 {
97 0
98 } else if self.learning_rate <= 1e-3 {
99 1
100 } else {
101 2
102 };
103
104 vec![
105 hidden_size_idx,
106 layers_idx,
107 activation_idx,
108 bias_idx,
109 lr_idx,
110 ]
111 }
112
113 fn decode(genes: &[i64]) -> Self::Phenotype {
114 let hidden_size = match genes[0] {
115 0 => 32,
116 1 => 64,
117 2 => 128,
118 3 => 256,
119 _ => 64,
120 };
121
122 let num_hidden_layers = (genes[1] + 1).clamp(1, 8) as usize;
123
124 let activation_fn = match genes[2] {
125 0 => ActivationFunction::Relu,
126 1 => ActivationFunction::Gelu,
127 2 => ActivationFunction::Sigmoid,
128 _ => ActivationFunction::Relu,
129 };
130
131 let use_bias = genes[3] == 1;
132
133 let learning_rate = match genes[4] {
134 0 => 1e-4,
135 1 => 1e-3,
136 2 => 1e-2,
137 _ => 1e-3,
138 };
139
140 NeuralArchitecture {
141 hidden_size,
142 num_hidden_layers,
143 activation_fn,
144 use_bias,
145 learning_rate,
146 }
147 }
148}
149
150struct ArchitectureEvaluator;
151
152#[derive(Deserialize)]
153struct ResultOutput {
154 validation_loss: f64,
155}
156
157impl Evaluator<NeuralArchitecture> for ArchitectureEvaluator {
158 fn fitness<'a>(
159 &self,
160 _genotype_id: Uuid,
161 phenotype: NeuralArchitecture,
162 _: &Request,
163 _: &'a Box<dyn Terminated>,
164 ) -> futures::future::BoxFuture<'a, Result<f64, anyhow::Error>> {
165 Box::pin(async move {
166 let output = tokio::process::Command::new("fx-example-regression")
168 .args([
169 "--hidden-size",
170 &phenotype.hidden_size.to_string(),
171 "--num-hidden-layers",
172 &phenotype.num_hidden_layers.to_string(),
173 "--activation-fn",
174 &phenotype.activation_fn.to_string(),
175 "--learning-rate",
176 &phenotype.learning_rate.to_string(),
177 ])
178 .output()
179 .await
180 .expect("Failed to run fx-example-regression");
181
182 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
184
185 let result: ResultOutput =
187 serde_json::from_slice(&output.stdout).expect("Invalid JSON from training binary");
188
189 Ok(result.validation_loss)
190 })
191 }
192}
193
194#[tokio::main]
195async fn main() -> Result<()> {
196 dotenv::from_filename(".env.local").ok();
197 tracing_subscriber::fmt()
198 .pretty()
199 .with_thread_ids(true)
200 .with_max_level(Level::INFO)
201 .init();
202
203 let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
204 let pool = PgPoolOptions::new()
205 .max_connections(10)
206 .connect(&database_url)
207 .await?;
208
209 fx_durable_ga::migrations::run_default_migrations(&pool).await?;
211
212 let service = Arc::new(
213 bootstrap(pool.clone())
214 .await?
215 .register::<NeuralArchitecture, _>(ArchitectureEvaluator)
216 .await?
217 .build(),
218 );
219
220 let mut registry = fx_event_bus::EventHandlerRegistry::new();
221 register_event_handlers(
222 Arc::new(Queries::new(FX_MQ_JOBS_SCHEMA_NAME)),
223 service.clone(),
224 &mut registry,
225 );
226 let mut listener = fx_event_bus::Listener::new(pool.clone(), registry);
227 tokio::spawn(async move { listener.listen(None).await });
228
229 let host_id = Uuid::parse_str("00000000-0000-0000-0000-123456789abc")?;
230 let mut jobs_listener = fx_mq_jobs::Listener::new(
231 pool.clone(),
232 register_job_handlers(&service, fx_mq_jobs::RegistryBuilder::new()),
233 WORKERS,
234 host_id,
235 Duration::from_secs(600),
236 )
237 .await?;
238 tokio::spawn(async move { jobs_listener.listen().await });
239
240 service
241 .new_optimization_request(
242 NeuralArchitecture::NAME,
243 NeuralArchitecture::HASH,
244 FitnessGoal::minimize(FITNESS_TARGET)?,
245 Schedule::generational(10, 10),
246 Selector::tournament(5, 15)?,
247 Mutagen::new(Temperature::constant(0.8)?, MutationRate::constant(0.4)?),
248 Crossover::uniform(0.5)?,
249 Distribution::latin_hypercube(15),
250 None::<()>,
251 )
252 .await?;
253
254 tokio::time::sleep(Duration::from_secs(3600)).await;
255 Ok(())
256}