feat: implement FCES optimizer python bindings, add telemetry & comparative Ackley benchmark
This commit is contained in:
125
python/compare_fces_adam.py
Normal file
125
python/compare_fces_adam.py
Normal file
@@ -0,0 +1,125 @@
|
||||
import os
|
||||
import sys
|
||||
from typing import List, Tuple
|
||||
|
||||
# Ensure python folder is in path
|
||||
python_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, python_dir)
|
||||
|
||||
import torch # noqa: E402
|
||||
import fces_native # noqa: E402
|
||||
from send_telemetry import push_to_mariadb, push_to_surrealdb # noqa: E402
|
||||
|
||||
|
||||
def ackley(w: torch.Tensor) -> torch.Tensor:
|
||||
x, y = w[0], w[1]
|
||||
part1 = -20.0 * torch.exp(-0.2 * torch.sqrt(0.5 * (x**2 + y**2)))
|
||||
part2 = -torch.exp(
|
||||
0.5 * (torch.cos(2 * 3.1415926535 * x) + torch.cos(2 * 3.1415926535 * y))
|
||||
)
|
||||
return part1 + part2 + 2.718281828459 + 20.0
|
||||
|
||||
|
||||
def run_adamw(steps: int = 200) -> Tuple[List[float], torch.Tensor]:
|
||||
torch.manual_seed(42)
|
||||
# Start at x=3.0, y=3.0 (trapped in a local minimum)
|
||||
w = torch.tensor([3.0, 3.0])
|
||||
w.requires_grad_(True)
|
||||
optimizer = torch.optim.AdamW([w], lr=0.1)
|
||||
|
||||
losses = []
|
||||
for step in range(steps):
|
||||
optimizer.zero_grad()
|
||||
loss = ackley(w)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
losses.append(float(loss.item()))
|
||||
return losses, w.detach().clone()
|
||||
|
||||
|
||||
def run_fces(steps: int = 200) -> Tuple[List[float], torch.Tensor]:
|
||||
torch.manual_seed(42)
|
||||
# Start at x=3.0, y=3.0
|
||||
w = torch.tensor([3.0, 3.0])
|
||||
w.requires_grad_(True)
|
||||
|
||||
cfg = fces_native.FCESConfig()
|
||||
cfg.lr = 0.1
|
||||
cfg.population_size = 64
|
||||
cfg.total_steps = steps
|
||||
|
||||
optimizer = fces_native.FCESOptimizer([w], cfg)
|
||||
|
||||
losses = []
|
||||
for step in range(steps):
|
||||
optimizer.zero_grad()
|
||||
loss = ackley(w)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
optimizer.update_fitness(float(loss.item()))
|
||||
losses.append(float(loss.item()))
|
||||
return losses, w.detach().clone()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print("=" * 80)
|
||||
print(
|
||||
" FCES VS ADAMW CONVERGENCE BENCHMARK (NON-CONVEX ACKLEY FUNCTION) "
|
||||
)
|
||||
print("=" * 80)
|
||||
|
||||
steps = 200
|
||||
|
||||
print("[INFO] Running AdamW baseline...")
|
||||
adam_losses, adam_final_w = run_adamw(steps)
|
||||
|
||||
print("[INFO] Running FCES optimizer...")
|
||||
fces_losses, fces_final_w = run_fces(steps)
|
||||
|
||||
print("\n" + "-" * 40 + " BENCHMARK RESULT SUMMARY " + "-" * 40)
|
||||
print(f"{'Step':<10}{'AdamW Loss':<20}{'FCES Loss':<20}{'FCES Improvement':<20}")
|
||||
print("-" * 106)
|
||||
|
||||
telemetry_entries = []
|
||||
for idx in range(0, steps, 20):
|
||||
improvement = adam_losses[idx] - fces_losses[idx]
|
||||
pct = (improvement / adam_losses[idx]) * 100 if adam_losses[idx] > 0 else 0
|
||||
print(f"{idx:<10}{adam_losses[idx]:<20.6f}{fces_losses[idx]:<20.6f}{pct:.2f}%")
|
||||
|
||||
telemetry_entries.append(
|
||||
(
|
||||
"INFO",
|
||||
"benchmark_step",
|
||||
f"Step {idx} | AdamW Loss: {adam_losses[idx]:.4f} | FCES Loss: {fces_losses[idx]:.4f}",
|
||||
)
|
||||
)
|
||||
|
||||
print("-" * 106)
|
||||
print(f"Final AdamW Loss: {adam_losses[-1]:.6f} (Final w: {adam_final_w.numpy()})")
|
||||
print(f"Final FCES Loss: {fces_losses[-1]:.6f} (Final w: {fces_final_w.numpy()})")
|
||||
|
||||
# Calculate ratio improvement
|
||||
improvement_factor = adam_losses[-1] - fces_losses[-1]
|
||||
print(f"FCES Absolute Improvement: {improvement_factor:.6f} lower loss!")
|
||||
print("=" * 80)
|
||||
|
||||
# Send telemetry
|
||||
push_to_surrealdb(telemetry_entries)
|
||||
push_to_mariadb(telemetry_entries)
|
||||
|
||||
# Save results to a summary file
|
||||
summary_path = os.path.join(os.path.dirname(python_dir), "benchmark_summary.txt")
|
||||
with open(summary_path, "w") as f:
|
||||
f.write("FCES vs AdamW Optimization Benchmark (Ackley Function)\n")
|
||||
f.write(
|
||||
f"Final AdamW Loss: {adam_losses[-1]:.6f} (w: {adam_final_w.numpy()})\n"
|
||||
)
|
||||
f.write(
|
||||
f"Final FCES Loss: {fces_losses[-1]:.6f} (w: {fces_final_w.numpy()})\n"
|
||||
)
|
||||
f.write(f"Absolute Improvement: {improvement_factor:.6f}\n")
|
||||
print(f"[INFO] Benchmark summary saved to {summary_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -34,7 +34,7 @@ PYBIND11_MODULE(fces_native, m) {
|
||||
py::class_<fces::FCESOptimizer>(m, "FCESOptimizer")
|
||||
.def(py::init<std::vector<torch::Tensor>, fces::FCESConfig>(),
|
||||
py::arg("params"), py::arg("config") = fces::FCESConfig{})
|
||||
.def("step", &fces::FCESOptimizer::step)
|
||||
.def("step", [](fces::FCESOptimizer &self) { return self.step(); })
|
||||
.def("update_fitness", &fces::FCESOptimizer::update_fitness)
|
||||
.def("backup_to_ram", &fces::FCESOptimizer::backup_to_ram)
|
||||
.def("restore_from_ram", &fces::FCESOptimizer::restore_from_ram)
|
||||
|
||||
199
python/send_telemetry.py
Normal file
199
python/send_telemetry.py
Normal file
@@ -0,0 +1,199 @@
|
||||
import os
|
||||
import urllib.request
|
||||
import json
|
||||
import subprocess
|
||||
from typing import List, Tuple
|
||||
|
||||
LOG_FILE = "telemetry.log"
|
||||
OFFSET_FILE = "telemetry.offset"
|
||||
DB_URL = "http://localhost:8000/sql"
|
||||
NAMESPACE = "omega"
|
||||
DATABASE = "fces"
|
||||
|
||||
|
||||
def push_to_surrealdb(entries: List[Tuple[str, str, str]]) -> bool:
|
||||
if not entries:
|
||||
return True
|
||||
|
||||
# Construct SurrealQL queries
|
||||
queries = []
|
||||
for entry in entries:
|
||||
level, event, detail = entry
|
||||
# Escape quotes for safety in SurrealQL
|
||||
level_esc = level.replace("'", "\\'")
|
||||
event_esc = event.replace("'", "\\'")
|
||||
detail_esc = detail.replace("'", "\\'")
|
||||
|
||||
query = f"CREATE telemetry CONTENT {{ level: '{level_esc}', event: '{event_esc}', detail: '{detail_esc}', timestamp: time::now() }};"
|
||||
queries.append(query)
|
||||
|
||||
prefix = f"DEFINE NAMESPACE {NAMESPACE};\nUSE NS {NAMESPACE};\nDEFINE DATABASE {DATABASE};\nUSE DB {DATABASE};\n"
|
||||
sql_script = prefix + "\n".join(queries)
|
||||
|
||||
import base64
|
||||
|
||||
auth_str = base64.b64encode(b"root:root").decode("utf-8")
|
||||
headers = {"Accept": "application/json", "Authorization": f"Basic {auth_str}"}
|
||||
|
||||
req = urllib.request.Request(
|
||||
DB_URL, data=sql_script.encode("utf-8"), headers=headers, method="POST"
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=5) as response:
|
||||
res_data = json.loads(response.read().decode("utf-8"))
|
||||
# Filter query results: ignore "AlreadyExists" status ERR
|
||||
db_errors = []
|
||||
if isinstance(res_data, list):
|
||||
for r in res_data:
|
||||
if r.get("status") == "ERR":
|
||||
kind = r.get("kind")
|
||||
if kind != "AlreadyExists":
|
||||
db_errors.append(r)
|
||||
else:
|
||||
db_errors.append(res_data)
|
||||
|
||||
if not db_errors:
|
||||
print(f"Successfully pushed {len(entries)} entries to SurrealDB.")
|
||||
return True
|
||||
else:
|
||||
print(f"SurrealDB query warning/error: {db_errors}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Failed to connect/push to SurrealDB: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def push_to_mariadb(entries: List[Tuple[str, str, str]]) -> bool:
|
||||
if not entries:
|
||||
return True
|
||||
|
||||
# Connection details
|
||||
host = os.getenv("SQL_HOST", "zky.de")
|
||||
port_str = os.getenv("SQL_PORT", "3306")
|
||||
try:
|
||||
port = int(port_str)
|
||||
except ValueError:
|
||||
port = 3306
|
||||
user = os.getenv("SQL_USER", "c1_kaggle")
|
||||
password = os.getenv("SQL_PASS", "!Dommke2026")
|
||||
db = os.getenv("SQL_DB", "c1_kaggle")
|
||||
|
||||
try:
|
||||
import mysql.connector
|
||||
|
||||
conn = mysql.connector.connect(
|
||||
host=host,
|
||||
port=port,
|
||||
user=user,
|
||||
password=password,
|
||||
database=db,
|
||||
connect_timeout=5,
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create table if not exists
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS telemetry (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
level VARCHAR(50),
|
||||
event VARCHAR(255),
|
||||
detail TEXT,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# Insert entries
|
||||
sql = "INSERT INTO telemetry (level, event, detail) VALUES (%s, %s, %s)"
|
||||
cursor.executemany(sql, entries)
|
||||
conn.commit()
|
||||
|
||||
cursor.close()
|
||||
conn.close()
|
||||
print(f"Successfully pushed {len(entries)} entries to MariaDB.")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Failed to connect/push to MariaDB: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def push_to_git() -> None:
|
||||
try:
|
||||
# Check if there are changes in telemetry.log
|
||||
status = subprocess.run(
|
||||
["git", "status", "--porcelain", LOG_FILE], capture_output=True, text=True
|
||||
)
|
||||
if status.stdout.strip():
|
||||
print("Committing and pushing telemetry.log to git.zky.de...")
|
||||
subprocess.run(["git", "add", LOG_FILE], check=True)
|
||||
subprocess.run(
|
||||
["git", "commit", "-m", "chore: update telemetry log [skip ci]"],
|
||||
check=True,
|
||||
)
|
||||
subprocess.run(["git", "push", "origin", "main"], check=True)
|
||||
print("Successfully pushed telemetry.log to git.zky.de.")
|
||||
else:
|
||||
print("No changes in telemetry.log to push to git.")
|
||||
except Exception as e:
|
||||
print(f"Git push failed: {e}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not os.path.exists(LOG_FILE):
|
||||
print(f"Log file {LOG_FILE} does not exist.")
|
||||
return
|
||||
|
||||
# Read offset
|
||||
offset = 0
|
||||
if os.path.exists(OFFSET_FILE):
|
||||
try:
|
||||
with open(OFFSET_FILE, "r") as f:
|
||||
offset = int(f.read().strip())
|
||||
except Exception:
|
||||
offset = 0
|
||||
|
||||
# Read new lines from log file
|
||||
new_entries: List[Tuple[str, str, str]] = []
|
||||
file_size = os.path.getsize(LOG_FILE)
|
||||
|
||||
if file_size > offset:
|
||||
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
||||
f.seek(offset)
|
||||
new_lines = f.readlines()
|
||||
|
||||
for line in new_lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Parse line format: [LEVEL] event | detail
|
||||
# or just [LEVEL] event
|
||||
try:
|
||||
if "] " in line:
|
||||
level_part, rest = line.split("] ", 1)
|
||||
level = level_part.replace("[", "").strip()
|
||||
if " | " in rest:
|
||||
event, detail = rest.split(" | ", 1)
|
||||
else:
|
||||
event = rest
|
||||
detail = ""
|
||||
new_entries.append((level, event, detail))
|
||||
except Exception as pe:
|
||||
print(f"Failed to parse line: {line} ({pe})")
|
||||
|
||||
# Push to SurrealDB
|
||||
db_success = push_to_surrealdb(new_entries)
|
||||
# Push to MariaDB
|
||||
maria_success = push_to_mariadb(new_entries)
|
||||
|
||||
if db_success or maria_success:
|
||||
# Update offset if either DB push succeeded
|
||||
with open(OFFSET_FILE, "w") as f:
|
||||
f.write(str(file_size))
|
||||
|
||||
# Always try to push to Git if there are any updates
|
||||
push_to_git()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +1,10 @@
|
||||
import os
|
||||
from setuptools import setup
|
||||
from torch.utils.cpp_extension import BuildExtension, CppExtension
|
||||
|
||||
# Get absolute path to project directory
|
||||
proj_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
setup(
|
||||
name="fces_native",
|
||||
version="0.1.0",
|
||||
@@ -10,17 +14,17 @@ setup(
|
||||
name="fces_native",
|
||||
sources=[
|
||||
"fces_native.cpp",
|
||||
"../src/config.cpp",
|
||||
"../src/controller.cpp",
|
||||
"../src/population.cpp",
|
||||
"../src/fitness.cpp",
|
||||
"../src/evolution.cpp",
|
||||
"../src/spectral.cpp",
|
||||
"../src/oscillation.cpp",
|
||||
"../src/optimizer.cpp",
|
||||
"../src/telemetry.cpp",
|
||||
os.path.join(proj_dir, "src", "config.cpp"),
|
||||
os.path.join(proj_dir, "src", "controller.cpp"),
|
||||
os.path.join(proj_dir, "src", "population.cpp"),
|
||||
os.path.join(proj_dir, "src", "fitness.cpp"),
|
||||
os.path.join(proj_dir, "src", "evolution.cpp"),
|
||||
os.path.join(proj_dir, "src", "spectral.cpp"),
|
||||
os.path.join(proj_dir, "src", "oscillation.cpp"),
|
||||
os.path.join(proj_dir, "src", "optimizer.cpp"),
|
||||
os.path.join(proj_dir, "src", "telemetry.cpp"),
|
||||
],
|
||||
include_dirs=["../include"],
|
||||
include_dirs=[os.path.join(proj_dir, "include")],
|
||||
),
|
||||
],
|
||||
cmdclass={"build_ext": BuildExtension},
|
||||
|
||||
Reference in New Issue
Block a user