Skip to content

Commit d68c745

Browse files
committed
M16: simplify — deduplicate helpers, remove structural copy-paste
- Eliminate deep_cast: express as deep_apply + Nx.as_type closure - Extract close?/4 and flunk_trajectory/5 to TrainingHelper (was copy-pasted across 3 curve tests) - Extract MNIST load/evaluate helpers to MnistHelper (was duplicated between f32 and bf16 test modules) - Collapse bf16/f16 tensor generators into a single clause that delegates to the f32 generator + as_type - Restore coerce fast path for {:u, 8} (avoids Native.dtype NIF call on the most common mismatch type) - Remove section comments on self-describing private functions - Single-source LossScaler default scale via @default_scale attribute
1 parent 010a5bf commit d68c745

10 files changed

Lines changed: 100 additions & 203 deletions

lib/emily/backend.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ defmodule Emily.Backend do
5555
%{out | data: %B{ref: coerce(ref, type, w)}}
5656
end
5757

58+
# Fast path: pred→u8 is the most common mismatch (MLX comparison/logical
59+
# ops yield bool; Nx expects u8). The general path covers bf16→f32 type
60+
# promotion from Nx.Defn.grad and any other MLX/Nx dtype disagreement.
61+
defp coerce(ref, {:u, 8}, w), do: Native.astype(w, ref, {:u, 8})
62+
5863
defp coerce(ref, type, w) do
5964
case Native.dtype(ref) do
6065
^type -> ref

lib/emily/mixed_precision.ex

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,10 @@ defmodule Emily.MixedPrecision do
6262
after `growth_interval` successful steps it doubles.
6363
"""
6464

65+
@default_scale 65_536.0
66+
6567
@enforce_keys [:scale]
66-
defstruct scale: 65_536.0,
68+
defstruct scale: @default_scale,
6769
growth_factor: 2.0,
6870
backoff_factor: 0.5,
6971
growth_interval: 2000,
@@ -81,30 +83,26 @@ defmodule Emily.MixedPrecision do
8183
:growth_interval,
8284
:min_scale
8385
])
84-
|> then(&Keyword.merge(Map.to_list(%__MODULE__{scale: 65_536.0}), &1))
86+
|> then(&Keyword.merge(Map.to_list(%__MODULE__{scale: @default_scale}), &1))
8587

8688
struct!(__MODULE__, fields)
8789
end
8890
end
8991

90-
# -------------------------------------------------------------------
91-
# Public API
92-
# -------------------------------------------------------------------
93-
9492
@doc """
9593
Downcast float tensors in a nested structure to `type`.
9694
9795
Integer and predicate tensors are left unchanged.
9896
"""
99-
def cast_params(params, type), do: deep_cast(params, type)
97+
def cast_params(params, type), do: deep_apply(params, &Nx.as_type(&1, type))
10098

10199
@doc """
102100
Upcast float tensors in a nested gradient structure to `type`.
103101
104102
Semantically identical to `cast_params/2` — exists for readability
105103
at the call site (the direction of the cast is part of the name).
106104
"""
107-
def accumulate_grad(grads, type), do: deep_cast(grads, type)
105+
def accumulate_grad(grads, type), do: deep_apply(grads, &Nx.as_type(&1, type))
108106

109107
@doc """
110108
Create a new dynamic loss scaler.
@@ -167,35 +165,9 @@ defmodule Emily.MixedPrecision do
167165
"""
168166
def has_overflow?(structure), do: deep_overflow?(structure)
169167

170-
# -------------------------------------------------------------------
171-
# Internal: recursive structure traversal
172-
# -------------------------------------------------------------------
173-
174168
defp float_type?({kind, _}) when kind in [:f, :bf], do: true
175169
defp float_type?(_), do: false
176170

177-
# deep_cast — cast float tensors to target type
178-
179-
defp deep_cast(%Nx.Tensor{} = t, type) do
180-
if float_type?(t.type), do: Nx.as_type(t, type), else: t
181-
end
182-
183-
defp deep_cast(map, type) when is_map(map) and not is_struct(map) do
184-
Map.new(map, fn {k, v} -> {k, deep_cast(v, type)} end)
185-
end
186-
187-
defp deep_cast(tuple, type) when is_tuple(tuple) do
188-
tuple |> Tuple.to_list() |> Enum.map(&deep_cast(&1, type)) |> List.to_tuple()
189-
end
190-
191-
defp deep_cast(list, type) when is_list(list) do
192-
Enum.map(list, &deep_cast(&1, type))
193-
end
194-
195-
defp deep_cast(other, _type), do: other
196-
197-
# deep_apply — apply fun to every float tensor
198-
199171
defp deep_apply(%Nx.Tensor{} = t, fun) do
200172
if float_type?(t.type), do: fun.(t), else: t
201173
end
@@ -214,8 +186,6 @@ defmodule Emily.MixedPrecision do
214186

215187
defp deep_apply(other, _fun), do: other
216188

217-
# deep_overflow? — check for nan/inf in any float tensor
218-
219189
defp deep_overflow?(%Nx.Tensor{} = t) do
220190
if float_type?(t.type) do
221191
t

test/emily/training/bf16_mlp_curve_test.exs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ defmodule Emily.Training.Bf16MlpCurveTest do
1313
use ExUnit.Case, async: true
1414

1515
alias Emily.TrainingHelper, as: TH
16+
import TH, only: [close?: 4, flunk_trajectory: 5]
1617

1718
@dims {4, 8, 3}
1819
@batch_shape {16, 4, 3}
@@ -75,18 +76,4 @@ defmodule Emily.Training.Bf16MlpCurveTest do
7576
"final loss divergence: emily=#{le_final} bin=#{lb_final} " <>
7677
"reldiff=#{abs(le_final - lb_final) / abs(lb_final)}"
7778
end
78-
79-
defp close?(a, b, atol, rtol), do: abs(a - b) <= atol + rtol * abs(b)
80-
81-
defp flunk_trajectory(i, le, lb, losses_emily, losses_bin) do
82-
preview_e = losses_emily |> Enum.take(min(i + 3, length(losses_emily)))
83-
preview_b = losses_bin |> Enum.take(min(i + 3, length(losses_bin)))
84-
85-
flunk("""
86-
per-step loss diverged at step #{i}:
87-
emily=#{le} bin=#{lb} reldiff=#{abs(le - lb) / abs(lb)}
88-
emily trajectory (first #{length(preview_e)} steps): #{inspect(preview_e)}
89-
bin trajectory (first #{length(preview_b)} steps): #{inspect(preview_b)}
90-
""")
91-
end
9279
end

test/emily/training/mlp_curve_test.exs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ defmodule Emily.Training.MlpCurveTest do
2424
use ExUnit.Case, async: true
2525

2626
alias Emily.TrainingHelper, as: TH
27+
import TH, only: [close?: 4, flunk_trajectory: 5]
2728

2829
@dims {4, 8, 3}
2930
@batch_shape {16, 4, 3}
@@ -83,18 +84,4 @@ defmodule Emily.Training.MlpCurveTest do
8384
"final loss divergence: emily=#{le_final} bin=#{lb_final} " <>
8485
"reldiff=#{abs(le_final - lb_final) / abs(lb_final)}"
8586
end
86-
87-
defp close?(a, b, atol, rtol), do: abs(a - b) <= atol + rtol * abs(b)
88-
89-
defp flunk_trajectory(i, le, lb, losses_emily, losses_bin) do
90-
preview_e = losses_emily |> Enum.take(min(i + 3, length(losses_emily)))
91-
preview_b = losses_bin |> Enum.take(min(i + 3, length(losses_bin)))
92-
93-
flunk("""
94-
per-step loss diverged at step #{i}:
95-
emily=#{le} bin=#{lb} reldiff=#{abs(le - lb) / abs(lb)}
96-
emily trajectory (first #{length(preview_e)} steps): #{inspect(preview_e)}
97-
bin trajectory (first #{length(preview_b)} steps): #{inspect(preview_b)}
98-
""")
99-
end
10087
end

test/emily/training/mnist_bf16_full_test.exs

Lines changed: 4 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ defmodule Emily.Training.MnistBf16FullTest do
1212

1313
use ExUnit.Case, async: false
1414

15+
alias Emily.MnistHelper
16+
1517
@moduletag :training_full
1618
@moduletag capture_log: true
1719
@moduletag timeout: 600_000
@@ -28,7 +30,7 @@ defmodule Emily.Training.MnistBf16FullTest do
2830
@target_accuracy 0.955
2931

3032
test "bf16 Axon MLP reaches >#{trunc(@target_accuracy * 100)}% test accuracy under Emily.Compiler" do
31-
{train_batches, test_images, test_labels} = load_mnist(@batch_size)
33+
{train_batches, test_images, test_labels} = MnistHelper.load_mnist(@batch_size)
3234

3335
policy =
3436
Axon.MixedPrecision.create_policy(
@@ -51,60 +53,9 @@ defmodule Emily.Training.MnistBf16FullTest do
5153
compiler: Emily.Compiler
5254
)
5355

54-
accuracy = evaluate(model, trained_state, test_images, test_labels)
56+
accuracy = MnistHelper.evaluate(model, trained_state, test_images, test_labels)
5557

5658
assert accuracy >= @target_accuracy,
5759
"bf16 MNIST accuracy #{Float.round(accuracy, 4)} below target #{@target_accuracy}"
5860
end
59-
60-
defp load_mnist(batch_size) do
61-
{train_images_raw, train_labels_raw} = Scidata.MNIST.download()
62-
63-
train_images =
64-
train_images_raw
65-
|> mnist_images_to_tensor()
66-
|> Nx.to_batched(batch_size)
67-
68-
train_labels =
69-
train_labels_raw
70-
|> mnist_labels_to_tensor()
71-
|> Nx.to_batched(batch_size)
72-
73-
train_batches = Stream.zip(train_images, train_labels)
74-
75-
{test_images_raw, test_labels_raw} = Scidata.MNIST.download_test()
76-
77-
test_images = mnist_images_to_tensor(test_images_raw)
78-
test_labels = mnist_labels_to_tensor(test_labels_raw)
79-
80-
{train_batches, test_images, test_labels}
81-
end
82-
83-
defp mnist_images_to_tensor({bin, type, shape}) do
84-
bin
85-
|> Nx.from_binary(type)
86-
|> Nx.reshape(shape)
87-
|> Nx.reshape({elem(shape, 0), 784})
88-
|> Nx.divide(255.0)
89-
end
90-
91-
defp mnist_labels_to_tensor({bin, type, shape}) do
92-
bin
93-
|> Nx.from_binary(type)
94-
|> Nx.reshape(shape)
95-
|> Nx.new_axis(-1)
96-
|> Nx.equal(Nx.iota({1, 10}))
97-
end
98-
99-
defp evaluate(model, state, test_images, test_labels) do
100-
logits =
101-
Axon.predict(model, state, test_images, compiler: Emily.Compiler)
102-
103-
predicted = Nx.argmax(logits, axis: -1)
104-
actual = Nx.argmax(test_labels, axis: -1)
105-
106-
Nx.mean(Nx.equal(predicted, actual))
107-
|> Nx.backend_transfer(Nx.BinaryBackend)
108-
|> Nx.to_number()
109-
end
11061
end

test/emily/training/mnist_full_test.exs

Lines changed: 4 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ defmodule Emily.Training.MnistFullTest do
2020

2121
use ExUnit.Case, async: false
2222

23+
alias Emily.MnistHelper
24+
2325
@moduletag :training_full
2426
@moduletag capture_log: true
2527
@moduletag timeout: 600_000
@@ -36,7 +38,7 @@ defmodule Emily.Training.MnistFullTest do
3638
@target_accuracy 0.96
3739

3840
test "Axon MLP reaches >#{trunc(@target_accuracy * 100)}% test accuracy under Emily.Compiler" do
39-
{train_batches, test_images, test_labels} = load_mnist(@batch_size)
41+
{train_batches, test_images, test_labels} = MnistHelper.load_mnist(@batch_size)
4042

4143
model =
4244
Axon.input("input", shape: {nil, 784})
@@ -51,67 +53,9 @@ defmodule Emily.Training.MnistFullTest do
5153
compiler: Emily.Compiler
5254
)
5355

54-
accuracy = evaluate(model, trained_state, test_images, test_labels)
56+
accuracy = MnistHelper.evaluate(model, trained_state, test_images, test_labels)
5557

5658
assert accuracy >= @target_accuracy,
5759
"MNIST accuracy #{Float.round(accuracy, 4)} below target #{@target_accuracy}"
5860
end
59-
60-
# ---- Data loading ----
61-
62-
defp load_mnist(batch_size) do
63-
# Training set — streamed in batches for Axon.Loop.
64-
{train_images_raw, train_labels_raw} = Scidata.MNIST.download()
65-
66-
train_images =
67-
train_images_raw
68-
|> mnist_images_to_tensor()
69-
|> Nx.to_batched(batch_size)
70-
71-
train_labels =
72-
train_labels_raw
73-
|> mnist_labels_to_tensor()
74-
|> Nx.to_batched(batch_size)
75-
76-
train_batches = Stream.zip(train_images, train_labels)
77-
78-
# Test set — loaded as whole tensors for one-shot evaluation.
79-
{test_images_raw, test_labels_raw} = Scidata.MNIST.download_test()
80-
81-
test_images = mnist_images_to_tensor(test_images_raw)
82-
test_labels = mnist_labels_to_tensor(test_labels_raw)
83-
84-
{train_batches, test_images, test_labels}
85-
end
86-
87-
defp mnist_images_to_tensor({bin, type, shape}) do
88-
bin
89-
|> Nx.from_binary(type)
90-
|> Nx.reshape(shape)
91-
# {N, 1, 28, 28} → {N, 784} + normalize to [0, 1].
92-
|> Nx.reshape({elem(shape, 0), 784})
93-
|> Nx.divide(255.0)
94-
end
95-
96-
defp mnist_labels_to_tensor({bin, type, shape}) do
97-
bin
98-
|> Nx.from_binary(type)
99-
|> Nx.reshape(shape)
100-
|> Nx.new_axis(-1)
101-
|> Nx.equal(Nx.iota({1, 10}))
102-
end
103-
104-
# ---- Evaluation ----
105-
106-
defp evaluate(model, state, test_images, test_labels) do
107-
logits =
108-
Axon.predict(model, state, test_images, compiler: Emily.Compiler)
109-
110-
predicted = Nx.argmax(logits, axis: -1)
111-
actual = Nx.argmax(test_labels, axis: -1)
112-
113-
Nx.mean(Nx.equal(predicted, actual))
114-
|> Nx.backend_transfer(Nx.BinaryBackend)
115-
|> Nx.to_number()
116-
end
11761
end

test/emily/training/transformer_block_curve_test.exs

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ defmodule Emily.Training.TransformerBlockCurveTest do
1616
use ExUnit.Case, async: true
1717

1818
alias Emily.TrainingHelper, as: TH
19+
import TH, only: [close?: 4, flunk_trajectory: 5]
1920

2021
@embed 16
2122
@ff 32
@@ -75,18 +76,4 @@ defmodule Emily.Training.TransformerBlockCurveTest do
7576
"final loss divergence: emily=#{le_final} bin=#{lb_final} " <>
7677
"reldiff=#{abs(le_final - lb_final) / abs(lb_final)}"
7778
end
78-
79-
defp close?(a, b, atol, rtol), do: abs(a - b) <= atol + rtol * abs(b)
80-
81-
defp flunk_trajectory(i, le, lb, losses_emily, losses_bin) do
82-
preview_e = losses_emily |> Enum.take(min(i + 3, length(losses_emily)))
83-
preview_b = losses_bin |> Enum.take(min(i + 3, length(losses_bin)))
84-
85-
flunk("""
86-
per-step transformer-block loss diverged at step #{i}:
87-
emily=#{le} bin=#{lb} reldiff=#{abs(le - lb) / abs(lb)}
88-
emily trajectory (first #{length(preview_e)} steps): #{inspect(preview_e)}
89-
bin trajectory (first #{length(preview_b)} steps): #{inspect(preview_b)}
90-
""")
91-
end
9279
end

test/support/backend_generators.ex

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,8 @@ defmodule Emily.BackendGenerators do
4040
|> map(&Nx.reshape(&1, shape))
4141
end
4242

43-
def tensor(shape, {:bf, 16}) do
44-
list_of(float(min: -10.0, max: 10.0), length: Nx.size(shape))
45-
|> map(&Nx.tensor(&1, type: {:f, 32}, backend: Nx.BinaryBackend))
46-
|> map(&Nx.reshape(&1, shape))
47-
|> map(&Nx.as_type(&1, {:bf, 16}))
48-
end
49-
50-
def tensor(shape, {:f, 16}) do
51-
list_of(float(min: -10.0, max: 10.0), length: Nx.size(shape))
52-
|> map(&Nx.tensor(&1, type: {:f, 32}, backend: Nx.BinaryBackend))
53-
|> map(&Nx.reshape(&1, shape))
54-
|> map(&Nx.as_type(&1, {:f, 16}))
43+
def tensor(shape, {kind, 16} = type) when kind in [:bf, :f] do
44+
tensor(shape, {:f, 32}) |> map(&Nx.as_type(&1, type))
5545
end
5646

5747
def tensor(shape, {:s, bits}) do

0 commit comments

Comments
 (0)