data-archetype commited on
Commit
f7020d0
·
verified ·
1 Parent(s): 1b703d5

Fix AMP precision policy

Browse files
Files changed (4) hide show
  1. README.md +5 -4
  2. dinac_ae/encoder.py +3 -3
  3. dinac_ae/model.py +84 -37
  4. dinac_ae/precision.py +83 -0
README.md CHANGED
@@ -69,10 +69,11 @@ encoder.
69
  `num_steps=1` means one NFE.
70
 
71
  The export ships weights in `float32`. The recommended and default runtime path
72
- is `bfloat16` AMP for the main encoder, decoder, and class-token path, with
73
- `float32` retained for sensitive operations such as whitening/dewhitening,
74
- normalization math, RoPE frequency construction, and VP diffusion schedule
75
- helpers.
 
76
 
77
  ## Usage
78
 
 
69
  `num_steps=1` means one NFE.
70
 
71
  The export ships weights in `float32`. The recommended and default runtime path
72
+ is `bfloat16` AMP for the main encoder, decoder, and class-token path. The
73
+ loader retains normalization affine parameters, GRN/residual gates, the final
74
+ pixel projection, latent statistics, RoPE/time frequencies, sampler state, and
75
+ whitening/dewhitening in `float32`. These tensors are loaded from the original
76
+ FP32 weights before ordinary parameters are converted to BF16.
77
 
78
  ## Usage
79
 
dinac_ae/encoder.py CHANGED
@@ -37,7 +37,7 @@ def _resolve_encoder_mlp_type(name: str) -> MLPType:
37
  return MLPType.RELU
38
  case _ as unreachable:
39
  raise ValueError(
40
- "Unsupported encoder_mlp_type for DinacAE export: " f"{unreachable!r}"
41
  )
42
 
43
 
@@ -65,7 +65,7 @@ class EncoderPosterior:
65
  def mode(self) -> Tensor:
66
  """Return the posterior mode in token space."""
67
 
68
- return (self.alpha * self.mean.to(torch.float32)).to(dtype=self.mean.dtype)
69
 
70
  def sample(self, *, generator: torch.Generator | None = None) -> Tensor:
71
  """Sample from the posterior."""
@@ -77,7 +77,7 @@ class EncoderPosterior:
77
  dtype=torch.float32,
78
  generator=generator,
79
  )
80
- return (self.alpha * mean_fp32 + self.sigma * eps).to(dtype=self.mean.dtype)
81
 
82
 
83
  class Encoder(nn.Module):
 
37
  return MLPType.RELU
38
  case _ as unreachable:
39
  raise ValueError(
40
+ f"Unsupported encoder_mlp_type for DinacAE export: {unreachable!r}"
41
  )
42
 
43
 
 
65
  def mode(self) -> Tensor:
66
  """Return the posterior mode in token space."""
67
 
68
+ return self.alpha * self.mean.to(torch.float32)
69
 
70
  def sample(self, *, generator: torch.Generator | None = None) -> Tensor:
71
  """Sample from the posterior."""
 
77
  dtype=torch.float32,
78
  generator=generator,
79
  )
80
+ return self.alpha * mean_fp32 + self.sigma * eps
81
 
82
 
83
  class Encoder(nn.Module):
dinac_ae/model.py CHANGED
@@ -2,6 +2,7 @@
2
 
3
  from __future__ import annotations
4
 
 
5
  from pathlib import Path
6
 
7
  import torch
@@ -13,6 +14,7 @@ from dit.repa_projection import DinoTokenAlignmentHead
13
  from .config import DinacAEConfig, DinacAEInferenceConfig
14
  from .decoder import Decoder
15
  from .encoder import Encoder, EncoderPosterior
 
16
  from .samplers import run_ddim, run_dpmpp_2m
17
  from .vp_diffusion import get_schedule, make_initial_state, sample_noise
18
 
@@ -58,8 +60,7 @@ def _resolve_class_head_mlp_type(name: str) -> MLPType:
58
  return MLPType.RELU
59
  case _ as unreachable:
60
  raise ValueError(
61
- "Unsupported class_head_mlp_type for DinacAE export: "
62
- f"{unreachable!r}"
63
  )
64
 
65
 
@@ -111,27 +112,50 @@ class DinacAE(nn.Module):
111
  register_token_count=int(config.class_head_register_token_count),
112
  )
113
 
114
- def _restore_float32_norm_buffers(self) -> None:
115
- """Keep latent running stats in float32 after device/dtype moves."""
116
-
117
- self.latent_norm_running_mean = self.latent_norm_running_mean.to(
118
- dtype=torch.float32
119
- )
120
- self.latent_norm_running_var = self.latent_norm_running_var.to(
121
- dtype=torch.float32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  )
123
 
124
- def to(self, *args: object, **kwargs: object) -> DinacAE:
125
- """Move the model while preserving float32 latent stats buffers."""
126
-
127
- moved = super().to(*args, **kwargs)
128
- if not isinstance(moved, DinacAE):
129
- raise RuntimeError(
130
- f"Expected DinacAE after nn.Module.to(), got {type(moved).__name__}"
131
- )
132
- moved._restore_float32_norm_buffers()
133
- return moved
134
-
135
  @classmethod
136
  def from_pretrained(
137
  cls,
@@ -144,6 +168,8 @@ class DinacAE(nn.Module):
144
  ) -> DinacAE:
145
  """Load a pretrained export from a local directory or the Hub."""
146
 
 
 
147
  model_dir = _resolve_model_dir(
148
  path_or_repo_id,
149
  revision=revision,
@@ -167,6 +193,7 @@ class DinacAE(nn.Module):
167
  model.load_state_dict(state_dict, strict=True)
168
  model = model.to(dtype=dtype, device=torch.device(device))
169
  model.eval()
 
170
  return model
171
 
172
  def _latent_norm_stats(self) -> tuple[Tensor, Tensor]:
@@ -210,9 +237,13 @@ class DinacAE(nn.Module):
210
  height=int(images.shape[2]),
211
  width=int(images.shape[3]),
212
  )
213
- model_dtype = next(self.parameters()).dtype
214
- latents = self.encoder(images.to(dtype=model_dtype))
215
- return self.whiten(latents).to(dtype=model_dtype)
 
 
 
 
216
 
217
  def encode_posterior(self, images: Tensor) -> EncoderPosterior:
218
  """Encode images and return the raw posterior."""
@@ -221,23 +252,34 @@ class DinacAE(nn.Module):
221
  height=int(images.shape[2]),
222
  width=int(images.shape[3]),
223
  )
224
- model_dtype = next(self.parameters()).dtype
225
- return self.encoder.encode_posterior(images.to(dtype=model_dtype))
 
 
 
 
 
 
226
 
227
  def predict_class(self, latents: Tensor) -> Tensor:
228
  """Predict the exported DINO class token from whitened latents."""
229
 
230
- dewhitened = self.dewhiten(latents)
 
 
 
 
231
  t_zero = torch.zeros(
232
  (int(latents.shape[0]),),
233
- device=latents.device,
234
  dtype=torch.float32,
235
  )
236
- head_dtype = self.dino_token_alignment_head.in_proj.weight.dtype
237
- device_type = "cuda" if latents.device.type == "cuda" else "cpu"
238
- with torch.autocast(device_type=device_type, enabled=False):
 
239
  out = self.dino_token_alignment_head(
240
- dewhitened.to(device=latents.device, dtype=head_dtype),
241
  t=t_zero,
242
  )
243
  return out.class_token.to(torch.float32)
@@ -260,8 +302,10 @@ class DinacAE(nn.Module):
260
  self._require_image_size_divisible(height=int(height), width=int(width))
261
  batch = int(latents.shape[0])
262
  device = latents.device
263
- model_dtype = next(self.parameters()).dtype
264
- decoder_latents = self.dewhiten(latents).to(device=device, dtype=model_dtype)
 
 
265
  noise = sample_noise(
266
  (batch, int(self.config.in_channels), int(height), int(width)),
267
  noise_std=float(self.config.pixel_noise_std),
@@ -277,7 +321,10 @@ class DinacAE(nn.Module):
277
  logsnr_max=float(self.config.logsnr_max),
278
  )
279
  device_type = "cuda" if device.type == "cuda" else "cpu"
280
- with torch.autocast(device_type=device_type, enabled=False):
 
 
 
281
 
282
  def _forward_fn(
283
  x_t: Tensor,
@@ -289,9 +336,9 @@ class DinacAE(nn.Module):
289
  ) -> Tensor:
290
  _ = mask_latent_tokens
291
  return self.decoder(
292
- x_t.to(dtype=model_dtype),
293
  t,
294
- latents_in.to(dtype=model_dtype),
295
  drop_middle_blocks=bool(drop_middle_blocks),
296
  )
297
 
 
2
 
3
  from __future__ import annotations
4
 
5
+ from collections.abc import Callable
6
  from pathlib import Path
7
 
8
  import torch
 
14
  from .config import DinacAEConfig, DinacAEInferenceConfig
15
  from .decoder import Decoder
16
  from .encoder import Encoder, EncoderPosterior
17
+ from .precision import parameter_storage_dtype, validate_inference_storage
18
  from .samplers import run_ddim, run_dpmpp_2m
19
  from .vp_diffusion import get_schedule, make_initial_state, sample_noise
20
 
 
60
  return MLPType.RELU
61
  case _ as unreachable:
62
  raise ValueError(
63
+ f"Unsupported class_head_mlp_type for DinacAE export: {unreachable!r}"
 
64
  )
65
 
66
 
 
112
  register_token_count=int(config.class_head_register_token_count),
113
  )
114
 
115
+ def _apply(
116
+ self,
117
+ fn: Callable[[Tensor], Tensor],
118
+ recurse: bool = True,
119
+ ) -> DinacAE:
120
+ """Move the model without rounding FP32 parameter or buffer islands."""
121
+
122
+ preserved_parameters = {
123
+ name: parameter.detach().float()
124
+ for name, parameter in self.named_parameters()
125
+ if parameter_storage_dtype(name, torch.bfloat16) is torch.float32
126
+ }
127
+ preserved_buffers = {
128
+ name: buffer.detach().float()
129
+ for name, buffer in self.named_buffers()
130
+ if buffer.is_floating_point()
131
+ }
132
+ super()._apply(fn, recurse=recurse)
133
+ for name, parameter in self.named_parameters():
134
+ preserved = preserved_parameters.get(name)
135
+ if preserved is not None:
136
+ parameter.data = preserved.to(device=parameter.device)
137
+ for name, buffer in self.named_buffers():
138
+ preserved = preserved_buffers.get(name)
139
+ if preserved is not None:
140
+ buffer.data = preserved.to(device=buffer.device)
141
+ return self
142
+
143
+ @property
144
+ def ordinary_weight_storage_dtype(self) -> torch.dtype:
145
+ """Return the ordinary-weight storage dtype."""
146
+
147
+ return self.decoder.patchify.proj.weight.dtype
148
+
149
+ def validate_inference_storage(self) -> None:
150
+ """Require the loaded model's complete mixed-precision policy."""
151
+
152
+ device = self.decoder.patchify.proj.weight.device
153
+ validate_inference_storage(
154
+ self,
155
+ base_dtype=self.ordinary_weight_storage_dtype,
156
+ device=device,
157
  )
158
 
 
 
 
 
 
 
 
 
 
 
 
159
  @classmethod
160
  def from_pretrained(
161
  cls,
 
168
  ) -> DinacAE:
169
  """Load a pretrained export from a local directory or the Hub."""
170
 
171
+ if dtype is not torch.float32 and dtype is not torch.bfloat16:
172
+ raise ValueError(f"Unsupported DINAC-AE parameter storage dtype: {dtype}")
173
  model_dir = _resolve_model_dir(
174
  path_or_repo_id,
175
  revision=revision,
 
193
  model.load_state_dict(state_dict, strict=True)
194
  model = model.to(dtype=dtype, device=torch.device(device))
195
  model.eval()
196
+ model.validate_inference_storage()
197
  return model
198
 
199
  def _latent_norm_stats(self) -> tuple[Tensor, Tensor]:
 
237
  height=int(images.shape[2]),
238
  width=int(images.shape[3]),
239
  )
240
+ device = self.decoder.patchify.proj.weight.device
241
+ with torch.autocast(
242
+ device_type=device.type,
243
+ dtype=torch.bfloat16,
244
+ ):
245
+ latents = self.encoder(images.to(device=device, dtype=torch.bfloat16))
246
+ return self.whiten(latents)
247
 
248
  def encode_posterior(self, images: Tensor) -> EncoderPosterior:
249
  """Encode images and return the raw posterior."""
 
252
  height=int(images.shape[2]),
253
  width=int(images.shape[3]),
254
  )
255
+ device = self.decoder.patchify.proj.weight.device
256
+ with torch.autocast(
257
+ device_type=device.type,
258
+ dtype=torch.bfloat16,
259
+ ):
260
+ return self.encoder.encode_posterior(
261
+ images.to(device=device, dtype=torch.bfloat16)
262
+ )
263
 
264
  def predict_class(self, latents: Tensor) -> Tensor:
265
  """Predict the exported DINO class token from whitened latents."""
266
 
267
+ device = self.decoder.patchify.proj.weight.device
268
+ dewhitened = self.dewhiten(latents).to(
269
+ device=device,
270
+ dtype=torch.float32,
271
+ )
272
  t_zero = torch.zeros(
273
  (int(latents.shape[0]),),
274
+ device=device,
275
  dtype=torch.float32,
276
  )
277
+ with torch.autocast(
278
+ device_type=device.type,
279
+ dtype=torch.bfloat16,
280
+ ):
281
  out = self.dino_token_alignment_head(
282
+ dewhitened,
283
  t=t_zero,
284
  )
285
  return out.class_token.to(torch.float32)
 
302
  self._require_image_size_divisible(height=int(height), width=int(width))
303
  batch = int(latents.shape[0])
304
  device = latents.device
305
+ decoder_latents = self.dewhiten(latents).to(
306
+ device=device,
307
+ dtype=torch.float32,
308
+ )
309
  noise = sample_noise(
310
  (batch, int(self.config.in_channels), int(height), int(width)),
311
  noise_std=float(self.config.pixel_noise_std),
 
321
  logsnr_max=float(self.config.logsnr_max),
322
  )
323
  device_type = "cuda" if device.type == "cuda" else "cpu"
324
+ with torch.autocast(
325
+ device_type=device_type,
326
+ dtype=torch.bfloat16,
327
+ ):
328
 
329
  def _forward_fn(
330
  x_t: Tensor,
 
336
  ) -> Tensor:
337
  _ = mask_latent_tokens
338
  return self.decoder(
339
+ x_t.to(dtype=torch.float32),
340
  t,
341
+ latents_in.to(dtype=torch.float32),
342
  drop_middle_blocks=bool(drop_middle_blocks),
343
  )
344
 
dinac_ae/precision.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Frozen mixed-precision storage policy for DINAC-AE inference."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+
9
+ def _is_norm_parameter(name: str) -> bool:
10
+ """Return whether a parameter belongs to a normalization module."""
11
+
12
+ return any("norm" in component for component in name.split("."))
13
+
14
+
15
+ def _is_gate_parameter(name: str) -> bool:
16
+ """Return whether a parameter is a learned residual or GRN gate."""
17
+
18
+ return (
19
+ name.endswith(".layer_scale")
20
+ or name.endswith(".grn.gamma")
21
+ or name.endswith(".grn.beta")
22
+ or name.endswith("decoder.path_drop_mask_feature")
23
+ )
24
+
25
+
26
+ def _is_decoder_output_parameter(name: str) -> bool:
27
+ """Return whether a parameter belongs to the final pixel projection."""
28
+
29
+ return ".decoder.out_proj." in f".{name}."
30
+
31
+
32
+ def parameter_storage_dtype(name: str, base_dtype: torch.dtype) -> torch.dtype:
33
+ """Return storage dtype for one frozen DINAC-AE parameter."""
34
+
35
+ if base_dtype is torch.float32:
36
+ return torch.float32
37
+ if base_dtype is not torch.bfloat16:
38
+ raise ValueError(f"Unsupported DINAC-AE parameter storage dtype: {base_dtype}")
39
+ preserve = (
40
+ _is_norm_parameter(name)
41
+ or _is_gate_parameter(name)
42
+ or _is_decoder_output_parameter(name)
43
+ )
44
+ return torch.float32 if preserve else torch.bfloat16
45
+
46
+
47
+ def validate_inference_storage(
48
+ module: nn.Module,
49
+ *,
50
+ base_dtype: torch.dtype,
51
+ device: torch.device,
52
+ ) -> None:
53
+ """Require the exact mixed parameter and FP32-buffer inference policy."""
54
+
55
+ def _device_matches(actual: torch.device) -> bool:
56
+ """Return whether an actual device satisfies the requested device."""
57
+
58
+ return actual.type == device.type and (
59
+ device.index is None or actual.index == device.index
60
+ )
61
+
62
+ parameter_mismatches = tuple(
63
+ name
64
+ for name, parameter in module.named_parameters()
65
+ if not _device_matches(parameter.device)
66
+ or parameter.dtype is not parameter_storage_dtype(name, base_dtype)
67
+ )
68
+ if parameter_mismatches:
69
+ raise RuntimeError(
70
+ "DINAC-AE parameter storage does not match its inference policy: "
71
+ f"{parameter_mismatches}"
72
+ )
73
+ buffer_mismatches = tuple(
74
+ name
75
+ for name, buffer in module.named_buffers()
76
+ if not _device_matches(buffer.device)
77
+ or (buffer.is_floating_point() and buffer.dtype is not torch.float32)
78
+ )
79
+ if buffer_mismatches:
80
+ raise RuntimeError(
81
+ "DINAC-AE floating inference buffers must retain FP32 storage: "
82
+ f"{buffer_mismatches}"
83
+ )