Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions OPTIMIZATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<!--
SPDX-FileCopyrightText: 2026 OpenAI
SPDX-License-Identifier: Apache-2.0
-->

# Exact GELU and reusable primal outputs for SQL training

These changes let [xarray-sql](https://github.com/xqlsystems/xarray-sql),
a SQL interface to labeled arrays, train a
[nanoGPT](https://github.com/karpathy/nanoGPT) character model through
ddx-generated gradients. ddx remains the automatic differentiation engine:
the downstream GPU executor evaluates its SQL programs and does not replace
them with a framework backward pass.

## Error-function differentiation

The scalar registry now supports `erf`, the error function used in exact
GELU: `GELU(x) = x * (1 + erf(x / sqrt(2))) / 2`. Its derivative is
`2 / sqrt(pi) * exp(-x*x)`, composed with the existing chain rule.
The SQL plan reader/writer preserves `erf` in generated programs, so a
derivative containing the original primal function can be serialized and
executed. No tanh approximation is substituted for exact GELU.

The engine executing the SQL must register an `erf` implementation. This
change adds differentiation and round-tripping, not an engine-specific
SQL function. xarray-sql registers the DataFusion function and an
equivalent trusted array primitive; replacing that function invalidates
array lowering permission there.

The Rust scalar-rule test checks chain-rule composition and the analytic
second derivative, rather than only matching one first derivative. Existing
unsupported-expression and derivative semantics remain in effect. Exact
GELU support is a feature/correctness change; no isolated speedup or memory
reduction was measured for the rule itself.

1 change: 1 addition & 0 deletions crates/ddx-ad/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ functions! {
Sqrt => "sqrt",
Ln => "ln",
Exp => "exp",
Erf => "erf",
Sin => "sin",
Cos => "cos",
Tan => "tan",
Expand Down
8 changes: 4 additions & 4 deletions crates/ddx-ad/src/to_sql/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1488,10 +1488,10 @@ fn syntax(name: &str) -> Option<Syntax> {

/// Scalar functions named alike in every dialect, by their canonical names.
const SCALARS: &[&str] = &[
"abs", "sqrt", "cbrt", "exp", "ln", "log10", "log2", "power", "sin", "cos", "tan", "asin",
"acos", "atan", "atan2", "sinh", "cosh", "tanh", "asinh", "acosh", "atanh", "floor", "ceil",
"round", "trunc", "greatest", "least", "coalesce", "nullif", "pi", "degrees", "radians",
"random",
"erf", "abs", "sqrt", "cbrt", "exp", "ln", "log10", "log2", "power", "sin", "cos", "tan",
"asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh", "asinh", "acosh", "atanh", "floor",
"ceil", "round", "trunc", "greatest", "least", "coalesce", "nullif", "pi", "degrees",
"radians", "random",
];

/// Aggregate functions named alike in every dialect.
Expand Down
9 changes: 7 additions & 2 deletions crates/ddx-core/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
//! only in their *leaf rule* — the tangent assigned to each column.

use std::collections::HashMap;
use std::f64::consts::{LN_10, LN_2};
use std::f64::consts::{LN_10, LN_2, PI};
use std::sync::Arc;

use sqlparser::ast::{
Expand All @@ -33,7 +33,7 @@ use crate::error::{DiffError, Result};
/// A one-line summary of what ddx-core differentiates, appended to "unsupported"
/// errors so a user reading the message learns what *is* supported and can act.
const SUPPORTED: &str = "ddx differentiates the operators + - * /, unary calls to \
sin/cos/tan/asin/acos/atan/exp/ln/log2/log10/sqrt/sinh/cosh/tanh/abs, power(...) with a \
sin/cos/tan/asin/acos/atan/exp/ln/log2/log10/sqrt/sinh/cosh/tanh/abs/erf, power(...) with a \
constant base or exponent, casts to a numeric type, and column/literal leaves";

/// A differentiation rule for a unary primitive `f(u)`: given the argument
Expand Down Expand Up @@ -105,6 +105,11 @@ impl RuleRegistry {
"sqrt".into(),
rule(|u| div(one(), mul(num(2.0), func1("sqrt", u.clone())))),
);
// Error function, including the chain rule through exact GELU.
unary.insert(
"erf".into(),
rule(|u| mul(num(2.0 / PI.sqrt()), func1("exp", neg(square(u.clone()))))),
);
// Hyperbolic.
unary.insert("sinh".into(), rule(|u| func1("cosh", u.clone())));
unary.insert("cosh".into(), rule(|u| func1("sinh", u.clone())));
Expand Down
14 changes: 14 additions & 0 deletions crates/ddx-core/tests/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,3 +372,17 @@ fn general_power_error_suggests_the_constant_side_and_the_exp_ln_rewrite() {
"should suggest the u^v rewrite: {m}"
);
}

#[test]
fn error_function_chain_and_second_derivative() {
use ddx_core::test_utils::{eval, parse_expr};
let first = parse_expr(&d("erf(2 * x)", "x"));
let second = Ddx::new()
.differentiate(&first, &ColRef::bare("x"))
.unwrap();
for x in [-2.0_f64, -0.5, 0.0, 0.5, 2.0] {
let expected = 4.0 / std::f64::consts::PI.sqrt() * (-4.0 * x * x).exp();
assert!((eval(&first, x, 0.0).unwrap() - expected).abs() < 1e-12);
assert!((eval(&second, x, 0.0).unwrap() + 8.0 * x * expected).abs() < 1e-12);
}
}