Skip to content

Model Card for LACE

OpenReview venue license base dataset dataset dataset vendor-parity hub

This package ports LACE, an ICLR 2024 layout diffusion editor, into a 🧨diffusers-style package for PubLayNet and RICO checkpoints.

Model Details

Model Description

LACE is a diffusers-style layout generator that samples layouts under learned aesthetic constraints for document and UI domains. Converted checkpoints run unconditional generation for PubLayNet, RICO25, and the RICO13 label space when the corresponding original weights are available. Public outputs use normalized center xywh boxes in [0, 1], dataset-local integer labels, a valid-element mask, and id2label.

  • Developed by: Jian Chen et al.
  • Shared by: creative-graphic-design.
  • Model type: content-agnostic; task: task-agnostic; conditioning: unconditional, label, label_size, completion, refinement.
  • Language(s) (NLP): not applicable.
  • License: MIT.

Model Sources

Supported Checkpoints

Checkpoint Hub ID Status
PubLayNet creative-graphic-design/lace-publaynet not-published; denoiser forward exact-match parity only
RICO13 creative-graphic-design/lace-rico13 planned; public original-implementation checkpoint not present in model.tar.gz
RICO25 creative-graphic-design/lace-rico25 not-published; denoiser forward exact-match parity only

Uses

Direct Use

Use this package for research inference, conversion checks, and agreement checks against the original implementation for generated layouts.

LACE runs unconditional, label-conditioned, label-size-conditioned, completion, and refinement generation from converted PubLayNet and RICO checkpoints. beautify=True applies the aesthetic-constraint post-optimization used by the package.

condition_type Required inputs Effect
unconditional none samples all labels and boxes
label labels, bbox, optional mask keeps labels fixed and samples geometry
label_size labels, bbox, optional mask keeps labels and element sizes fixed
completion labels, bbox, optional mask preserves a random valid subset and fills the rest
refinement labels, bbox, optional mask starts from a noised layout and refines it

Local original checkpoints can be converted with:

uv run --package lace models/lace/scripts/convert_checkpoint.py \
  --dataset publaynet \
  --checkpoint .cache/lace/original/model/publaynet_best.pt \
  --output .cache/lace/converted/lace-publaynet

Downstream Use

Generated layouts may feed rendering, design tooling, layout evaluation, or downstream content placement systems after task-specific validation.

Out-of-Scope Use

Do not treat generated layouts as production accessibility annotations, OCR output, semantic scene understanding, or license-cleared design assets without separate review.

Bias, Risks, and Limitations

The converted behavior follows the upstream checkpoints, prompt fixtures, and datasets. Dataset coverage, label vocabularies, and layout quality inherit the limits of those sources.

Recommendations

Re-run the agreement-check suite against the original implementation before publishing converted checkpoints or comparing new results.

How to Get Started with the Model

Install the package directly from this repository. The command includes shared packages when they are not published on PyPI.

pip install \
  "laygen @ git+https://github.com/creative-graphic-design/design-generators.git#subdirectory=lib/laygen" \
  "lace @ git+https://github.com/creative-graphic-design/design-generators.git#subdirectory=models/lace"

Clone this repository, install the workspace member, and run the download and conversion steps in REPRODUCING.md. Those steps create .cache/lace/converted/lace-publaynet.

git clone https://github.com/creative-graphic-design/design-generators.git
cd design-generators
uv sync --package lace
uv run --package lace python
from lace import LacePipeline

path = ".cache/lace/converted/lace-publaynet"
# After Hub publication: from_pretrained("creative-graphic-design/lace-publaynet")
pipe = LacePipeline.from_pretrained(path)
out = pipe(batch_size=1, seed=0)

print(out.bbox.shape)
print(out.labels.shape)
print(out.id2label[int(out.labels[out.mask][0])])
print(out.mask.any().item())
torch.Size([1, 25, 4])
torch.Size([1, 25])
text
True
out = pipe(
    condition_type="label_size",
    labels=[[0, 1]],
    bbox=[[[0.50, 0.50, 0.30, 0.20], [0.30, 0.25, 0.20, 0.10]]],
    mask=[[True, True]],
    beautify=True,
    seed=0,
)
print(out.bbox.shape)

Training Details

Training Data

Dataset Dataset ID Notes
RICO25 creative-graphic-design/Rico ui-screenshots-and-hierarchies-with-semantic-annotations
RICO13 creative-graphic-design/Rico original-source-derived RICO13 mapping
PubLayNet creative-graphic-design/PubLayNet default

The original LACE project trains on PubLayNet and RICO25 annotations prepared as max-25 layout sequences. RICO13 remains outside the package-local training claim because the approved archive has no authors' RICO13 checkpoint.

Training Procedure

Package-local reproduction passes the asserted staged gates for PubLayNet and RICO25 through the full TEST evaluation path; full-run statistical reproduction is not claimed. The final-head commands and machine-written evidence are recorded in TRAINING.md, and full-run reproduction is outside issue 423's scope.

Preprocessing

Inputs and outputs are normalized to the public layout schema at package boundaries. Boxes, tokens, prompts, or analog bits that follow the original implementation stay inside package adapters and fixtures used for agreement checks.

Training Hyperparameters

  • Training regime: package-local training and exact agreement checks pass through the full TEST evaluation path for PubLayNet and RICO25; full-run statistical training is not claimed.

Speeds, Sizes, Times

Training-time and carbon measurements are unknown.

Evaluation

Testing Data, Factors & Metrics

Testing Data

Agreement checks use local-only generated fixtures and converted checkpoint directories. Large generated tensors, images, weights, and downloaded artifacts are not committed.

Factors

Agreement results are reported separately by dataset, checkpoint, condition mode, seed, or prompt fixture where the package has recorded evidence.

Metrics

Metrics are exact tensor equality, exact token or byte equality, or an explicitly stated numeric tolerance against the original implementation.

Parity Results

Dataset Test Shape Max abs rtol atol
PubLayNet Denoiser logits (2, 25, 10) 0.0 0 0
Rico25 Denoiser logits (2, 25, 30) 0.0 0 0

Conversion smoke tests pass for PubLayNet and Rico25. Rico13 conversion agreement checks are skipped unless .cache/lace/original/model/rico13_best.pt is supplied, because the public model.tar.gz archive contains only publaynet_best.pt and rico25_best.pt. The committed agreement-check tests cover only the denoiser forward path; sampler loops, beautify, and conditional generation modes are not verified against the original implementation.

Reproducibility

See REPRODUCING.md for the commands that download original-implementation assets, generate reference outputs, run agreement checks, convert checkpoints, and smoke-test local loading.

Environmental Impact

Package-local training support exists, but this PR did not perform full-run model training. Conversion, agreement checks, and any future training costs depend on the selected checkpoint and local hardware.

Technical Specifications

Model Architecture and Objective

LACE uses a continuous diffusion denoiser over layout element features. The converted LacePipeline loads checkpoint-specific category vocabularies and generates normalized center xywh boxes, category labels, and masks for PubLayNet, RICO25, or RICO13 variants.

Compute Infrastructure

Agreement-check commands are intended for one explicitly selected GPU when the original implementation requires CUDA.

Hardware

CPU is sufficient for import and most smoke tests. CUDA is required for heavyweight agreement checks when the original implementation requires it.

Software

Use uv run --package lace ... from the repository root so workspace dependency sources and extras resolve correctly.

License

Repository wrapper code is Apache-2.0. The original LACE implementation is released under the MIT License. Upstream code, checkpoints, datasets, and prompt provider outputs keep their original licenses and terms.

Vendor links:

  • Original implementation: https://github.com/puar-playground/LACE
  • Checkpoints and datasets: https://huggingface.co/datasets/puar-playground/LACE
  • Paper: https://openreview.net/forum?id=kJ0qp9Xdsh

Citation

@inproceedings{
    chen2024towards,
    title={Towards Aligned Layout Generation via Diffusion Model with Aesthetic Constraints},
    author={Jian Chen and Ruiyi Zhang and Yufan Zhou and Changyou Chen},
    booktitle={The Twelfth International Conference on Learning Representations},
    year={2024},
    url={https://openreview.net/forum?id=kJ0qp9Xdsh}
}

API Reference

Diffusers-style LACE layout generation package.

LaceDatasetSpec dataclass

Static dataset metadata used to configure a LACE checkpoint.

Attributes:

Name Type Description
dataset DatasetName

Canonical dataset name.

labels tuple[LaceLabel, ...]

Ordered category labels without the padding class.

max_seq_length int

Maximum number of layout elements.

dim_transformer int

Transformer hidden size used by the original model.

nhead int

Number of attention heads.

num_layers int

Number of transformer blocks.

dim_feedforward int

Feed-forward hidden size.

Source code in models/lace/src/lace/configuration_lace.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@dataclass(frozen=True)
class LaceDatasetSpec:
    """Static dataset metadata used to configure a LACE checkpoint.

    Attributes:
        dataset: Canonical dataset name.
        labels: Ordered category labels without the padding class.
        max_seq_length: Maximum number of layout elements.
        dim_transformer: Transformer hidden size used by the original model.
        nhead: Number of attention heads.
        num_layers: Number of transformer blocks.
        dim_feedforward: Feed-forward hidden size.
    """

    dataset: DatasetName
    labels: tuple[LaceLabel, ...]
    max_seq_length: int = 25
    dim_transformer: int = 512
    nhead: int = 16
    num_layers: int = 4
    dim_feedforward: int = 2048

    @property
    def pad_label_id(self) -> int:
        """Return the integer id reserved for padding."""
        return len(self.labels)

    @property
    def num_classes_with_pad(self) -> int:
        """Return the number of category channels including padding."""
        return len(self.labels) + 1

    @property
    def seq_dim(self) -> int:
        """Return the latent per-element feature size."""
        return self.num_classes_with_pad + 4

    @property
    def id2label(self) -> dict[int, str]:
        """Return the category id to label mapping."""
        return dict(enumerate(str(label) for label in self.labels))

pad_label_id property

pad_label_id: int

Return the integer id reserved for padding.

num_classes_with_pad property

num_classes_with_pad: int

Return the number of category channels including padding.

seq_dim property

seq_dim: int

Return the latent per-element feature size.

id2label property

id2label: dict[int, str]

Return the category id to label mapping.

LaceModelOutput dataclass

Bases: BaseOutput

Output returned by the LACE transformer.

Attributes:

Name Type Description
sample Float[Tensor, 'batch elements channels']

Predicted noise tensor with the same shape as the input sample.

Source code in models/lace/src/lace/modeling_lace.py
46
47
48
49
50
51
52
53
54
@dataclass
class LaceModelOutput(BaseOutput):
    """Output returned by the LACE transformer.

    Attributes:
        sample: Predicted noise tensor with the same shape as the input sample.
    """

    sample: Float[torch.Tensor, "batch elements channels"]

LaceTransformerModel

Bases: ModelMixin, ConfigMixin

Transformer denoiser for continuous LACE layout tensors.

Parameters:

Name Type Description Default
seq_dim int

Number of channels per layout element.

required
max_seq_length int

Maximum number of layout elements.

25
num_layers int

Number of transformer blocks.

4
dim_transformer int

Hidden dimension.

512
nhead int

Number of attention heads.

16
dim_feedforward int

Feed-forward hidden dimension.

2048
diffusion_step int

Maximum diffusion timestep.

1000
timestep_type TimestepEmbeddingType | str | None

Timestep-conditioned normalization variant.

adalayernorm
Source code in models/lace/src/lace/modeling_lace.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
class LaceTransformerModel(ModelMixin, ConfigMixin):
    """Transformer denoiser for continuous LACE layout tensors.

    Args:
        seq_dim: Number of channels per layout element.
        max_seq_length: Maximum number of layout elements.
        num_layers: Number of transformer blocks.
        dim_transformer: Hidden dimension.
        nhead: Number of attention heads.
        dim_feedforward: Feed-forward hidden dimension.
        diffusion_step: Maximum diffusion timestep.
        timestep_type: Timestep-conditioned normalization variant.
    """

    config_name = "model_config.json"

    @register_to_config
    def __init__(
        self,
        *,
        seq_dim: int,
        max_seq_length: int = 25,
        num_layers: int = 4,
        dim_transformer: int = 512,
        nhead: int = 16,
        dim_feedforward: int = 2048,
        diffusion_step: int = 1000,
        timestep_type: TimestepEmbeddingType
        | str
        | None = TimestepEmbeddingType.adalayernorm,
    ) -> None:
        """Initialize a LACE transformer denoiser."""
        super().__init__()
        self.seq_dim = seq_dim
        self.max_seq_length = max_seq_length
        self.pos_encoder = SinusoidalPosEmb(max_seq_length, dim_transformer)
        self.layer_in = nn.Linear(seq_dim, dim_transformer)
        encoder_layer = Block(
            d_model=dim_transformer,
            nhead=nhead,
            dim_feedforward=dim_feedforward,
            diffusion_step=diffusion_step,
            timestep_type=timestep_type,
        )
        self.layers = clone_module_list(encoder_layer, num_layers)
        self.num_layers = num_layers
        self.layer_out = nn.Linear(dim_transformer, seq_dim)

    def forward(
        self,
        sample: Float[torch.Tensor, "batch elements channels"],
        timestep: Int[torch.Tensor, "batch"],
        attention_mask: Bool[torch.Tensor, "batch elements"] | None = None,
        return_dict: bool = True,
    ) -> LaceModelOutput | tuple[Float[torch.Tensor, "batch elements channels"]]:
        """Predict denoising residuals for a layout sample.

        Args:
            sample: Noisy layout tensor.
            timestep: Diffusion timestep per sample.
            attention_mask: Optional valid-element mask.
            return_dict: Whether to return ``LaceModelOutput``.

        Returns:
            Output dataclass or a one-item tuple containing the prediction.
        """
        output = F.softplus(self.layer_in(sample))
        pos_i = torch.arange(output.shape[1], device=output.device)
        output = output + self.pos_encoder(pos_i)
        key_padding_mask = None if attention_mask is None else ~attention_mask.bool()
        for i, layer in enumerate(self.layers):
            output = layer(
                output,
                src_key_padding_mask=key_padding_mask,
                timestep=timestep,
            )
            if i < self.num_layers - 1:
                output = F.softplus(output)
        output = self.layer_out(output)
        if not return_dict:
            return (output,)
        return LaceModelOutput(sample=output)

__init__

__init__(
    *,
    seq_dim: int,
    max_seq_length: int = 25,
    num_layers: int = 4,
    dim_transformer: int = 512,
    nhead: int = 16,
    dim_feedforward: int = 2048,
    diffusion_step: int = 1000,
    timestep_type: TimestepEmbeddingType
    | str
    | None = TimestepEmbeddingType.adalayernorm,
) -> None

Initialize a LACE transformer denoiser.

Source code in models/lace/src/lace/modeling_lace.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@register_to_config
def __init__(
    self,
    *,
    seq_dim: int,
    max_seq_length: int = 25,
    num_layers: int = 4,
    dim_transformer: int = 512,
    nhead: int = 16,
    dim_feedforward: int = 2048,
    diffusion_step: int = 1000,
    timestep_type: TimestepEmbeddingType
    | str
    | None = TimestepEmbeddingType.adalayernorm,
) -> None:
    """Initialize a LACE transformer denoiser."""
    super().__init__()
    self.seq_dim = seq_dim
    self.max_seq_length = max_seq_length
    self.pos_encoder = SinusoidalPosEmb(max_seq_length, dim_transformer)
    self.layer_in = nn.Linear(seq_dim, dim_transformer)
    encoder_layer = Block(
        d_model=dim_transformer,
        nhead=nhead,
        dim_feedforward=dim_feedforward,
        diffusion_step=diffusion_step,
        timestep_type=timestep_type,
    )
    self.layers = clone_module_list(encoder_layer, num_layers)
    self.num_layers = num_layers
    self.layer_out = nn.Linear(dim_transformer, seq_dim)

forward

forward(
    sample: Float[Tensor, "batch elements channels"],
    timestep: Int[Tensor, "batch"],
    attention_mask: Bool[Tensor, "batch elements"]
    | None = None,
    return_dict: bool = True,
) -> (
    LaceModelOutput
    | tuple[Float[torch.Tensor, "batch elements channels"]]
)

Predict denoising residuals for a layout sample.

Parameters:

Name Type Description Default
sample Float[Tensor, 'batch elements channels']

Noisy layout tensor.

required
timestep Int[Tensor, 'batch']

Diffusion timestep per sample.

required
attention_mask Bool[Tensor, 'batch elements'] | None

Optional valid-element mask.

None
return_dict bool

Whether to return LaceModelOutput.

True

Returns:

Type Description
LaceModelOutput | tuple[Float[Tensor, 'batch elements channels']]

Output dataclass or a one-item tuple containing the prediction.

Source code in models/lace/src/lace/modeling_lace.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def forward(
    self,
    sample: Float[torch.Tensor, "batch elements channels"],
    timestep: Int[torch.Tensor, "batch"],
    attention_mask: Bool[torch.Tensor, "batch elements"] | None = None,
    return_dict: bool = True,
) -> LaceModelOutput | tuple[Float[torch.Tensor, "batch elements channels"]]:
    """Predict denoising residuals for a layout sample.

    Args:
        sample: Noisy layout tensor.
        timestep: Diffusion timestep per sample.
        attention_mask: Optional valid-element mask.
        return_dict: Whether to return ``LaceModelOutput``.

    Returns:
        Output dataclass or a one-item tuple containing the prediction.
    """
    output = F.softplus(self.layer_in(sample))
    pos_i = torch.arange(output.shape[1], device=output.device)
    output = output + self.pos_encoder(pos_i)
    key_padding_mask = None if attention_mask is None else ~attention_mask.bool()
    for i, layer in enumerate(self.layers):
        output = layer(
            output,
            src_key_padding_mask=key_padding_mask,
            timestep=timestep,
        )
        if i < self.num_layers - 1:
            output = F.softplus(output)
    output = self.layer_out(output)
    if not return_dict:
        return (output,)
    return LaceModelOutput(sample=output)

LacePipeline

Bases: DiffusionPipeline

Generate layouts with a converted LACE checkpoint.

Parameters:

Name Type Description Default
model LaceTransformerModel

LACE transformer denoiser.

required
scheduler LaceScheduler

DDIM-style scheduler.

required
processor LaceProcessor

Processor that encodes and decodes layout tensors.

required

Examples:

>>> from lace import LaceProcessor, LaceScheduler, LaceTransformerModel
>>> model = LaceTransformerModel(seq_dim=10, max_seq_length=2, num_layers=1, dim_transformer=8, nhead=2, dim_feedforward=16)
>>> pipe = LacePipeline(model=model, scheduler=LaceScheduler(ddim_num_steps=1), processor=LaceProcessor.from_dataset("publaynet"))
>>> pipe.processor.max_seq_length
25
Source code in models/lace/src/lace/pipeline_lace.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
class LacePipeline(DiffusionPipeline):
    """Generate layouts with a converted LACE checkpoint.

    Args:
        model: LACE transformer denoiser.
        scheduler: DDIM-style scheduler.
        processor: Processor that encodes and decodes layout tensors.

    Examples:
        >>> from lace import LaceProcessor, LaceScheduler, LaceTransformerModel
        >>> model = LaceTransformerModel(seq_dim=10, max_seq_length=2, num_layers=1, dim_transformer=8, nhead=2, dim_feedforward=16)
        >>> pipe = LacePipeline(model=model, scheduler=LaceScheduler(ddim_num_steps=1), processor=LaceProcessor.from_dataset("publaynet"))
        >>> pipe.processor.max_seq_length
        25
    """

    model_cpu_offload_seq = "model"
    _optional_components = ["processor"]

    def __init__(
        self,
        model: LaceTransformerModel,
        scheduler: LaceScheduler,
        processor: LaceProcessor,
    ) -> None:
        """Attach the converted LACE denoiser, scheduler, and processor."""
        super().__init__()
        self.register_modules(model=model, scheduler=scheduler)
        self.model = model
        self.scheduler = scheduler
        self.processor = processor
        self.model.eval()

    @property
    def components(
        self,
    ) -> dict[str, LaceTransformerModel | LaceScheduler | LaceProcessor]:
        """Expose modules and processor metadata for Diffusers serialization."""
        return dict(
            model=self.model, scheduler=self.scheduler, processor=self.processor
        )

    @torch.no_grad()
    def __call__(
        self,
        *,
        batch_size: int = 1,
        num_inference_steps: int | None = None,
        generator: torch.Generator | None = None,
        seed: int | None = None,
        condition_type: ConditionType | str | None = ConditionType.unconditional,
        bbox: Float[torch.Tensor, "batch elements 4"] | None = None,
        labels: Int[torch.Tensor, "batch elements"] | None = None,
        mask: Bool[torch.Tensor, "batch elements"] | None = None,
        box_format: BoxFormat | str = BoxFormat.xywh,
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
        completion_ratio: float = 0.2,
        refinement_noise: float = 0.1,
        beautify: bool = False,
        beautify_overlap_weight: float | None = None,
        beautify_alignment_weight: float = 1.0,
        output_type: PipelineOutputType | str = PipelineOutputType.dataclass,
        return_intermediates: bool = False,
    ) -> LayoutGenerationOutput | dict[str, Shaped[torch.Tensor, "..."]]:
        """Run LACE denoising and return generated layouts.

        Args:
            batch_size: Number of layouts to generate for unconditional calls.
            num_inference_steps: Number of DDIM steps. Uses scheduler default if
                omitted.
            generator: Optional torch generator. Takes precedence over ``seed``.
            seed: Convenience seed used only when ``generator`` is absent.
            condition_type: Conditioning mode or alias.
            bbox: Conditioning boxes for non-unconditional modes.
            labels: Conditioning labels for non-unconditional modes.
            mask: Optional conditioning mask.
            box_format: Input box format for conditioning boxes.
            normalized: Whether conditioning boxes are normalized.
            canvas_size: Pixel canvas size required when ``normalized`` is false.
            completion_ratio: Maximum random completion fraction.
            refinement_noise: Noise scale for refinement conditioning.
            beautify: Whether to run the aesthetic post-optimization.
            beautify_overlap_weight: Optional overlap penalty override.
            beautify_alignment_weight: Alignment penalty weight.
            output_type: ``"dataclass"`` or ``"dict"``.
            return_intermediates: Whether to return the denoising trajectory.

        Returns:
            Layout output dataclass or a dictionary.

        Raises:
            ValueError: If a condition/output mode is unsupported or required
                conditioning tensors are missing.
        """
        generator = resolve_torch_generator(generator=generator, seed=seed)
        canonical = normalize_condition_type(condition_type)
        encoded = None
        if canonical is not ConditionType.unconditional:
            if bbox is None or labels is None:
                raise ValueError(
                    f"bbox and labels are required for condition_type={condition_type}"
                )

            encoded = self.processor(
                bbox=bbox,
                labels=labels,
                mask=mask,
                box_format=box_format,
                normalized=normalized,
                canvas_size=canvas_size,
            )
            batch_size = encoded[LACE_LAYOUT_KEY].shape[0]
        self.scheduler.set_timesteps(num_inference_steps, device=self.device)
        sample = self.scheduler.initial_sample(
            batch_size,
            self.processor.max_seq_length,
            self.processor.seq_dim,
            device=self.device,
            generator=generator,
        )
        if canonical is ConditionType.refinement:
            assert encoded is not None
            noise = randn(
                encoded[LACE_BBOX_KEY].shape,
                dtype=encoded[LACE_BBOX_KEY].dtype,
                device=encoded[LACE_BBOX_KEY].device,
                generator=generator,
            )
            noisy_bbox = (encoded[LACE_BBOX_KEY] + refinement_noise * noise).clamp(0, 1)
            sample = self.processor.encode(
                noisy_bbox.to(self.device),
                encoded[LACE_LABELS_KEY].to(self.device),
                encoded[LACE_MASK_KEY].to(self.device),
            )
        real_layout = (
            None if encoded is None else encoded[LACE_LAYOUT_KEY].to(self.device)
        )
        fix_mask = self._build_fix_mask(
            canonical, real_layout, completion_ratio, generator
        )
        trajectory = [] if return_intermediates else None
        step_indices = (
            self.scheduler.refinement_indices()
            if canonical is ConditionType.refinement
            else list(range(len(self.scheduler.timesteps) - 1, -1, -1))
        )
        for index in step_indices:
            step = self.scheduler.ddim_timesteps[index]
            timestep = torch.full(
                (batch_size,), int(step.item()), device=self.device, dtype=torch.long
            )
            if real_layout is not None and fix_mask is not None:
                sample[fix_mask] = real_layout[fix_mask]
            model_output = self.model(sample=sample, timestep=timestep).sample
            out = self.scheduler.step(
                model_output, timestep, sample, index=index, generator=generator
            )
            sample = out.prev_sample
            if real_layout is not None and fix_mask is not None:
                sample[fix_mask] = real_layout[fix_mask]
            if trajectory is not None:
                trajectory.append(sample.detach().cpu())
        decoded = self.processor.decode(sample.detach().cpu())
        bbox_out = decoded.bbox
        mask_out = decoded.mask
        if beautify:
            overlap = beautify_overlap_weight
            if overlap is None:
                overlap = (
                    1.0
                    if normalize_dataset(self.processor.dataset)
                    is DatasetName.publaynet
                    else 0.0
                )
            bbox_out, mask_out = beautify_layout(
                bbox_out,
                mask_out,
                overlap_weight=overlap,
                alignment_weight=beautify_alignment_weight,
            )
        output = LayoutGenerationOutput(
            bbox=bbox_out,
            labels=decoded.labels,
            mask=mask_out,
            id2label=decoded.id2label,
            trajectory=trajectory,
            intermediates={"condition_type": str(canonical)}
            if return_intermediates
            else None,
        )
        out_type = normalize_output_type(output_type)
        if out_type is PipelineOutputType.dict:
            return dict(output)
        if out_type is PipelineOutputType.dataclass:
            return output
        assert_never(out_type)

    generate = __call__

    def save_pretrained(self, save_directory: str | Path) -> None:
        """Save pipeline components.

        Args:
            save_directory: Output directory.
        """
        super().save_pretrained(save_directory)
        self.processor.save_pretrained(save_directory)

    @classmethod
    def from_pretrained(
        cls,
        pretrained_model_name_or_path: str | Path,
        processor: LaceProcessor | None = None,
    ) -> "LacePipeline":
        """Load a saved LACE pipeline.

        Args:
            pretrained_model_name_or_path: Local path or Hub id.
            processor: Optional processor override.

        Returns:
            Loaded pipeline with the serialized processor attached.
        """
        loaded_processor = (
            LaceProcessor.from_pretrained(pretrained_model_name_or_path)
            if processor is None
            else processor
        )
        pipe = super().from_pretrained(pretrained_model_name_or_path)
        pipe.processor = loaded_processor
        return pipe

    def _build_fix_mask(
        self,
        condition_type: ConditionType,
        real_layout: Float[torch.Tensor, "batch elements channels"] | None,
        completion_ratio: float,
        generator: torch.Generator | None,
    ) -> Bool[torch.Tensor, "batch elements channels"] | None:
        """Build the fixed-channel mask for conditional generation."""
        if real_layout is None or condition_type is ConditionType.refinement:
            return None

        batch_size, seq_len, seq_dim = real_layout.shape
        num_class = seq_dim - 4

        if condition_type is ConditionType.label:
            fix_mask = torch.zeros_like(real_layout, dtype=torch.bool)
            fix_mask[:, :, :num_class] = True
            return fix_mask

        if condition_type is ConditionType.label_size:
            fix_mask = torch.zeros_like(real_layout, dtype=torch.bool)
            fix_indices = list(range(num_class)) + [num_class + 2, num_class + 3]
            fix_mask[:, :, fix_indices] = True
            return fix_mask

        if condition_type is ConditionType.completion:
            labels = real_layout[:, :, :num_class].argmax(dim=2)
            real_mask = labels != (num_class - 1)
            cutoff = rand((), device=real_layout.device, generator=generator).item()
            element_mask = (
                rand(
                    batch_size,
                    seq_len,
                    device=real_layout.device,
                    generator=generator,
                )
                <= cutoff * completion_ratio
            ) & real_mask
            return element_mask.unsqueeze(-1).expand(-1, -1, seq_dim)

        if condition_type is ConditionType.unconditional:
            return None
        raise ValueError(f"Unsupported LACE condition_type: {condition_type}")

components property

components: dict[
    str,
    LaceTransformerModel | LaceScheduler | LaceProcessor,
]

Expose modules and processor metadata for Diffusers serialization.

__init__

__init__(
    model: LaceTransformerModel,
    scheduler: LaceScheduler,
    processor: LaceProcessor,
) -> None

Attach the converted LACE denoiser, scheduler, and processor.

Source code in models/lace/src/lace/pipeline_lace.py
144
145
146
147
148
149
150
151
152
153
154
155
156
def __init__(
    self,
    model: LaceTransformerModel,
    scheduler: LaceScheduler,
    processor: LaceProcessor,
) -> None:
    """Attach the converted LACE denoiser, scheduler, and processor."""
    super().__init__()
    self.register_modules(model=model, scheduler=scheduler)
    self.model = model
    self.scheduler = scheduler
    self.processor = processor
    self.model.eval()

__call__

__call__(
    *,
    batch_size: int = 1,
    num_inference_steps: int | None = None,
    generator: Generator | None = None,
    seed: int | None = None,
    condition_type: ConditionType
    | str
    | None = ConditionType.unconditional,
    bbox: Float[Tensor, "batch elements 4"] | None = None,
    labels: Int[Tensor, "batch elements"] | None = None,
    mask: Bool[Tensor, "batch elements"] | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    completion_ratio: float = 0.2,
    refinement_noise: float = 0.1,
    beautify: bool = False,
    beautify_overlap_weight: float | None = None,
    beautify_alignment_weight: float = 1.0,
    output_type: PipelineOutputType
    | str = PipelineOutputType.dataclass,
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[str, Shaped[torch.Tensor, "..."]]
)

Run LACE denoising and return generated layouts.

Parameters:

Name Type Description Default
batch_size int

Number of layouts to generate for unconditional calls.

1
num_inference_steps int | None

Number of DDIM steps. Uses scheduler default if omitted.

None
generator Generator | None

Optional torch generator. Takes precedence over seed.

None
seed int | None

Convenience seed used only when generator is absent.

None
condition_type ConditionType | str | None

Conditioning mode or alias.

unconditional
bbox Float[Tensor, 'batch elements 4'] | None

Conditioning boxes for non-unconditional modes.

None
labels Int[Tensor, 'batch elements'] | None

Conditioning labels for non-unconditional modes.

None
mask Bool[Tensor, 'batch elements'] | None

Optional conditioning mask.

None
box_format BoxFormat | str

Input box format for conditioning boxes.

xywh
normalized bool

Whether conditioning boxes are normalized.

True
canvas_size tuple[int, int] | None

Pixel canvas size required when normalized is false.

None
completion_ratio float

Maximum random completion fraction.

0.2
refinement_noise float

Noise scale for refinement conditioning.

0.1
beautify bool

Whether to run the aesthetic post-optimization.

False
beautify_overlap_weight float | None

Optional overlap penalty override.

None
beautify_alignment_weight float

Alignment penalty weight.

1.0
output_type PipelineOutputType | str

"dataclass" or "dict".

dataclass
return_intermediates bool

Whether to return the denoising trajectory.

False

Returns:

Type Description
LayoutGenerationOutput | dict[str, Shaped[Tensor, '...']]

Layout output dataclass or a dictionary.

Raises:

Type Description
ValueError

If a condition/output mode is unsupported or required conditioning tensors are missing.

Source code in models/lace/src/lace/pipeline_lace.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
@torch.no_grad()
def __call__(
    self,
    *,
    batch_size: int = 1,
    num_inference_steps: int | None = None,
    generator: torch.Generator | None = None,
    seed: int | None = None,
    condition_type: ConditionType | str | None = ConditionType.unconditional,
    bbox: Float[torch.Tensor, "batch elements 4"] | None = None,
    labels: Int[torch.Tensor, "batch elements"] | None = None,
    mask: Bool[torch.Tensor, "batch elements"] | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    completion_ratio: float = 0.2,
    refinement_noise: float = 0.1,
    beautify: bool = False,
    beautify_overlap_weight: float | None = None,
    beautify_alignment_weight: float = 1.0,
    output_type: PipelineOutputType | str = PipelineOutputType.dataclass,
    return_intermediates: bool = False,
) -> LayoutGenerationOutput | dict[str, Shaped[torch.Tensor, "..."]]:
    """Run LACE denoising and return generated layouts.

    Args:
        batch_size: Number of layouts to generate for unconditional calls.
        num_inference_steps: Number of DDIM steps. Uses scheduler default if
            omitted.
        generator: Optional torch generator. Takes precedence over ``seed``.
        seed: Convenience seed used only when ``generator`` is absent.
        condition_type: Conditioning mode or alias.
        bbox: Conditioning boxes for non-unconditional modes.
        labels: Conditioning labels for non-unconditional modes.
        mask: Optional conditioning mask.
        box_format: Input box format for conditioning boxes.
        normalized: Whether conditioning boxes are normalized.
        canvas_size: Pixel canvas size required when ``normalized`` is false.
        completion_ratio: Maximum random completion fraction.
        refinement_noise: Noise scale for refinement conditioning.
        beautify: Whether to run the aesthetic post-optimization.
        beautify_overlap_weight: Optional overlap penalty override.
        beautify_alignment_weight: Alignment penalty weight.
        output_type: ``"dataclass"`` or ``"dict"``.
        return_intermediates: Whether to return the denoising trajectory.

    Returns:
        Layout output dataclass or a dictionary.

    Raises:
        ValueError: If a condition/output mode is unsupported or required
            conditioning tensors are missing.
    """
    generator = resolve_torch_generator(generator=generator, seed=seed)
    canonical = normalize_condition_type(condition_type)
    encoded = None
    if canonical is not ConditionType.unconditional:
        if bbox is None or labels is None:
            raise ValueError(
                f"bbox and labels are required for condition_type={condition_type}"
            )

        encoded = self.processor(
            bbox=bbox,
            labels=labels,
            mask=mask,
            box_format=box_format,
            normalized=normalized,
            canvas_size=canvas_size,
        )
        batch_size = encoded[LACE_LAYOUT_KEY].shape[0]
    self.scheduler.set_timesteps(num_inference_steps, device=self.device)
    sample = self.scheduler.initial_sample(
        batch_size,
        self.processor.max_seq_length,
        self.processor.seq_dim,
        device=self.device,
        generator=generator,
    )
    if canonical is ConditionType.refinement:
        assert encoded is not None
        noise = randn(
            encoded[LACE_BBOX_KEY].shape,
            dtype=encoded[LACE_BBOX_KEY].dtype,
            device=encoded[LACE_BBOX_KEY].device,
            generator=generator,
        )
        noisy_bbox = (encoded[LACE_BBOX_KEY] + refinement_noise * noise).clamp(0, 1)
        sample = self.processor.encode(
            noisy_bbox.to(self.device),
            encoded[LACE_LABELS_KEY].to(self.device),
            encoded[LACE_MASK_KEY].to(self.device),
        )
    real_layout = (
        None if encoded is None else encoded[LACE_LAYOUT_KEY].to(self.device)
    )
    fix_mask = self._build_fix_mask(
        canonical, real_layout, completion_ratio, generator
    )
    trajectory = [] if return_intermediates else None
    step_indices = (
        self.scheduler.refinement_indices()
        if canonical is ConditionType.refinement
        else list(range(len(self.scheduler.timesteps) - 1, -1, -1))
    )
    for index in step_indices:
        step = self.scheduler.ddim_timesteps[index]
        timestep = torch.full(
            (batch_size,), int(step.item()), device=self.device, dtype=torch.long
        )
        if real_layout is not None and fix_mask is not None:
            sample[fix_mask] = real_layout[fix_mask]
        model_output = self.model(sample=sample, timestep=timestep).sample
        out = self.scheduler.step(
            model_output, timestep, sample, index=index, generator=generator
        )
        sample = out.prev_sample
        if real_layout is not None and fix_mask is not None:
            sample[fix_mask] = real_layout[fix_mask]
        if trajectory is not None:
            trajectory.append(sample.detach().cpu())
    decoded = self.processor.decode(sample.detach().cpu())
    bbox_out = decoded.bbox
    mask_out = decoded.mask
    if beautify:
        overlap = beautify_overlap_weight
        if overlap is None:
            overlap = (
                1.0
                if normalize_dataset(self.processor.dataset)
                is DatasetName.publaynet
                else 0.0
            )
        bbox_out, mask_out = beautify_layout(
            bbox_out,
            mask_out,
            overlap_weight=overlap,
            alignment_weight=beautify_alignment_weight,
        )
    output = LayoutGenerationOutput(
        bbox=bbox_out,
        labels=decoded.labels,
        mask=mask_out,
        id2label=decoded.id2label,
        trajectory=trajectory,
        intermediates={"condition_type": str(canonical)}
        if return_intermediates
        else None,
    )
    out_type = normalize_output_type(output_type)
    if out_type is PipelineOutputType.dict:
        return dict(output)
    if out_type is PipelineOutputType.dataclass:
        return output
    assert_never(out_type)

save_pretrained

save_pretrained(save_directory: str | Path) -> None

Save pipeline components.

Parameters:

Name Type Description Default
save_directory str | Path

Output directory.

required
Source code in models/lace/src/lace/pipeline_lace.py
325
326
327
328
329
330
331
332
def save_pretrained(self, save_directory: str | Path) -> None:
    """Save pipeline components.

    Args:
        save_directory: Output directory.
    """
    super().save_pretrained(save_directory)
    self.processor.save_pretrained(save_directory)

from_pretrained classmethod

from_pretrained(
    pretrained_model_name_or_path: str | Path,
    processor: LaceProcessor | None = None,
) -> "LacePipeline"

Load a saved LACE pipeline.

Parameters:

Name Type Description Default
pretrained_model_name_or_path str | Path

Local path or Hub id.

required
processor LaceProcessor | None

Optional processor override.

None

Returns:

Type Description
'LacePipeline'

Loaded pipeline with the serialized processor attached.

Source code in models/lace/src/lace/pipeline_lace.py
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
@classmethod
def from_pretrained(
    cls,
    pretrained_model_name_or_path: str | Path,
    processor: LaceProcessor | None = None,
) -> "LacePipeline":
    """Load a saved LACE pipeline.

    Args:
        pretrained_model_name_or_path: Local path or Hub id.
        processor: Optional processor override.

    Returns:
        Loaded pipeline with the serialized processor attached.
    """
    loaded_processor = (
        LaceProcessor.from_pretrained(pretrained_model_name_or_path)
        if processor is None
        else processor
    )
    pipe = super().from_pretrained(pretrained_model_name_or_path)
    pipe.processor = loaded_processor
    return pipe

PipelineOutputType

Bases: StrEnum

Supported LACE pipeline output containers.

Source code in models/lace/src/lace/pipeline_lace.py
33
34
35
36
37
class PipelineOutputType(StrEnum):
    """Supported LACE pipeline output containers."""

    dataclass = auto()
    dict = auto()

LaceProcessor

Bases: ProcessorMixin

Encode public layout tensors into the continuous LACE sequence format.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset name serialized with the processor config.

required
labels list[str]

Ordered category labels without the padding label.

required
max_seq_length int

Maximum number of layout elements.

25

Examples:

>>> processor = LaceProcessor.from_dataset("publaynet")
>>> processor.seq_dim
10
Source code in models/lace/src/lace/processing_lace.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
class LaceProcessor(ProcessorMixin):
    """Encode public layout tensors into the continuous LACE sequence format.

    Args:
        dataset: Canonical dataset name serialized with the processor config.
        labels: Ordered category labels without the padding label.
        max_seq_length: Maximum number of layout elements.

    Examples:
        >>> processor = LaceProcessor.from_dataset("publaynet")
        >>> processor.seq_dim
        10
    """

    config_name = "processor_config.json"

    def __init__(
        self,
        dataset: DatasetName | str,
        labels: list[str],
        max_seq_length: int = 25,
    ) -> None:
        """Initialize processor metadata.

        Args:
            dataset: Canonical dataset name.
            labels: Ordered category labels without padding.
            max_seq_length: Maximum number of layout elements.
        """
        super().__init__()
        self.dataset = str(normalize_dataset(dataset))
        self.labels = tuple(labels)
        self.max_seq_length = max_seq_length

    @classmethod
    def from_dataset(cls, dataset: DatasetName | str) -> "LaceProcessor":
        """Create a processor from built-in dataset metadata.

        Args:
            dataset: LACE dataset name or alias.

        Returns:
            Processor configured for the dataset.

        Raises:
            ValueError: If the dataset is unsupported.

        Examples:
            >>> LaceProcessor.from_dataset("rico13").pad_label_id
            13
        """
        spec = get_dataset_spec(dataset)
        return cls(
            dataset=str(spec.dataset),
            labels=[str(label) for label in spec.labels],
            max_seq_length=spec.max_seq_length,
        )

    @property
    def id2label(self) -> dict[int, str]:
        """Return the category id to label mapping."""
        return dict(enumerate(self.labels))

    @property
    def pad_label_id(self) -> int:
        """Return the padding label id."""
        return len(self.labels)

    @property
    def num_classes_with_pad(self) -> int:
        """Return the label-channel count including padding."""
        return len(self.labels) + 1

    @property
    def seq_dim(self) -> int:
        """Return the latent per-element feature size."""
        return self.num_classes_with_pad + 4

    def __call__(
        self,
        *,
        bbox: Float[torch.Tensor, "batch elements 4"]
        | Float[np.ndarray, "batch elements 4"]
        | Sequence[ArrayLikeInput],
        labels: Int[torch.Tensor, "batch elements"]
        | Int[np.ndarray, "batch elements"]
        | Sequence[ArrayLikeInput],
        mask: Bool[torch.Tensor, "batch elements"]
        | Bool[np.ndarray, "batch elements"]
        | Sequence[ArrayLikeInput]
        | None = None,
        box_format: BoxFormat | str = BoxFormat.xywh,
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
    ) -> dict[str, Shaped[torch.Tensor, "..."]]:
        """Encode a public layout batch.

        Args:
            bbox: Boxes in ``box_format``.
            labels: Integer category labels.
            mask: Optional valid-element mask.
            box_format: Input box format.
            normalized: Whether input coordinates are already normalized.
            canvas_size: Pixel canvas size required when ``normalized`` is false.

        Returns:
            Dictionary containing encoded layout, normalized boxes, labels, and mask.

        Raises:
            ValueError: If pixel boxes are passed without ``canvas_size`` or if
                ``box_format`` is unsupported.

        Examples:
            >>> processor = LaceProcessor.from_dataset("publaynet")
            >>> out = processor(bbox=[[[0.5, 0.5, 0.2, 0.2]]], labels=[[0]])
            >>> tuple(out["layout"].shape)
            (1, 25, 10)
        """
        bbox_t, labels_t, mask_t = prepare_layout_tensors(
            bbox=bbox,
            labels=labels,
            mask=mask,
            box_format=box_format,
            normalized=normalized,
            canvas_size=canvas_size,
        )
        bbox_t, labels_t, mask_t = self.pad(bbox_t, labels_t, mask_t)
        return {
            LACE_LAYOUT_KEY: self.encode(bbox_t, labels_t, mask_t),
            LACE_BBOX_KEY: bbox_t,
            LACE_LABELS_KEY: labels_t,
            LACE_MASK_KEY: mask_t,
        }

    def pad(
        self,
        bbox: Float[torch.Tensor, "batch elements 4"],
        labels: Int[torch.Tensor, "batch elements"],
        mask: Bool[torch.Tensor, "batch elements"] | None = None,
        max_seq_length: int | None = None,
    ) -> tuple[
        Float[torch.Tensor, "batch padded_elements 4"],
        Int[torch.Tensor, "batch padded_elements"],
        Bool[torch.Tensor, "batch padded_elements"],
    ]:
        """Pad a batch to ``max_seq_length``.

        Args:
            bbox: Normalized boxes with shape ``(batch, seq, 4)``.
            labels: Integer labels with shape ``(batch, seq)``.
            mask: Optional valid-element mask.
            max_seq_length: Optional override for output length.

        Returns:
            Padded boxes, labels, and mask.

        Raises:
            ValueError: If the input has too many elements.
        """
        max_len = max_seq_length or self.max_seq_length
        if bbox.shape[1] > max_len:
            raise ValueError(f"LACE supports at most {max_len} elements")

        if mask is None:
            mask = torch.ones(labels.shape, dtype=torch.bool, device=labels.device)
        pad_count = max_len - bbox.shape[1]
        if pad_count:
            bbox_pad = torch.zeros(
                bbox.shape[0], pad_count, 4, dtype=bbox.dtype, device=bbox.device
            )
            label_pad = torch.full(
                (labels.shape[0], pad_count),
                self.pad_label_id,
                dtype=labels.dtype,
                device=labels.device,
            )
            mask_pad = torch.zeros(
                mask.shape[0], pad_count, dtype=torch.bool, device=mask.device
            )
            bbox = torch.cat((bbox, bbox_pad), dim=1)
            labels = torch.cat((labels, label_pad), dim=1)
            mask = torch.cat((mask, mask_pad), dim=1)
        labels = labels.clone()
        labels[~mask] = self.pad_label_id
        return bbox, labels, mask

    def encode(
        self,
        bbox: Float[torch.Tensor, "batch elements 4"],
        labels: Int[torch.Tensor, "batch elements"],
        mask: Bool[torch.Tensor, "batch elements"] | None = None,
    ) -> Float[torch.Tensor, "batch padded_elements channels"]:
        """Encode normalized boxes and labels into the LACE latent range.

        Args:
            bbox: Normalized center ``xywh`` boxes.
            labels: Integer labels.
            mask: Optional valid-element mask.

        Returns:
            Tensor with one-hot labels followed by box channels in ``[-1, 1]``.
        """
        bbox, labels, mask = self.pad(bbox, labels, mask)
        bbox_in = 2 * (bbox.clamp(0.0, 1.0) - 0.5)
        labels = labels.clamp(0, self.pad_label_id)
        labels[~mask] = self.pad_label_id
        one_hot = torch.nn.functional.one_hot(
            labels, num_classes=self.num_classes_with_pad
        ).to(dtype=bbox.dtype, device=bbox.device)
        return torch.cat((one_hot, bbox_in), dim=-1)

    def decode(
        self, layout: Float[torch.Tensor, "batch elements channels"], clamp: bool = True
    ) -> LayoutGenerationOutput:
        """Decode a LACE layout tensor into public output fields.

        Args:
            layout: Tensor with one-hot label channels and box channels.
            clamp: Whether to clamp latent box channels before conversion.

        Returns:
            Layout generation output with boxes in normalized center ``xywh``.
        """
        decoded = layout.clone()
        bbox_latent = decoded[:, :, self.num_classes_with_pad :]
        if clamp:
            bbox_latent = bbox_latent.clamp(-1.0, 1.0)
        bbox = bbox_latent / 2 + 0.5
        labels = decoded[:, :, : self.num_classes_with_pad].argmax(dim=2).long()
        mask = labels != self.pad_label_id
        return LayoutGenerationOutput(
            bbox=bbox,
            labels=labels,
            mask=mask,
            id2label=self.id2label,
            intermediates={"dataset": self.dataset},
        )

    def save_pretrained(  # ty: ignore[invalid-method-override]
        self, save_directory: str | Path
    ) -> None:
        """Save processor config to a Diffusers directory.

        Args:
            save_directory: Directory where ``processor_config.json`` is written.
        """
        super().save_pretrained(save_directory)

    @classmethod
    def from_pretrained(  # ty: ignore[invalid-method-override]
        cls,
        pretrained_model_name_or_path: str | Path,
        cache_dir: str | Path | None = None,
        force_download: bool = False,
        local_files_only: bool = False,
        token: str | bool | None = None,
        revision: str = "main",
    ) -> "LaceProcessor":
        """Load processor config from a Diffusers directory.

        Args:
            pretrained_model_name_or_path: Directory or Hub id containing
                ``processor_config.json``.
            cache_dir: Optional Hugging Face cache directory.
            force_download: Whether to force a fresh download.
            local_files_only: Whether to avoid network access.
            token: Optional Hugging Face token.
            revision: Hub revision to load.

        Returns:
            Loaded processor.
        """
        return super().from_pretrained(
            pretrained_model_name_or_path,
            cache_dir=cache_dir,
            force_download=force_download,
            local_files_only=local_files_only,
            token=token,
            revision=revision,
        )

id2label property

id2label: dict[int, str]

Return the category id to label mapping.

pad_label_id property

pad_label_id: int

Return the padding label id.

num_classes_with_pad property

num_classes_with_pad: int

Return the label-channel count including padding.

seq_dim property

seq_dim: int

Return the latent per-element feature size.

__init__

__init__(
    dataset: DatasetName | str,
    labels: list[str],
    max_seq_length: int = 25,
) -> None

Initialize processor metadata.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset name.

required
labels list[str]

Ordered category labels without padding.

required
max_seq_length int

Maximum number of layout elements.

25
Source code in models/lace/src/lace/processing_lace.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(
    self,
    dataset: DatasetName | str,
    labels: list[str],
    max_seq_length: int = 25,
) -> None:
    """Initialize processor metadata.

    Args:
        dataset: Canonical dataset name.
        labels: Ordered category labels without padding.
        max_seq_length: Maximum number of layout elements.
    """
    super().__init__()
    self.dataset = str(normalize_dataset(dataset))
    self.labels = tuple(labels)
    self.max_seq_length = max_seq_length

from_dataset classmethod

from_dataset(dataset: DatasetName | str) -> 'LaceProcessor'

Create a processor from built-in dataset metadata.

Parameters:

Name Type Description Default
dataset DatasetName | str

LACE dataset name or alias.

required

Returns:

Type Description
'LaceProcessor'

Processor configured for the dataset.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Examples:

>>> LaceProcessor.from_dataset("rico13").pad_label_id
13
Source code in models/lace/src/lace/processing_lace.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@classmethod
def from_dataset(cls, dataset: DatasetName | str) -> "LaceProcessor":
    """Create a processor from built-in dataset metadata.

    Args:
        dataset: LACE dataset name or alias.

    Returns:
        Processor configured for the dataset.

    Raises:
        ValueError: If the dataset is unsupported.

    Examples:
        >>> LaceProcessor.from_dataset("rico13").pad_label_id
        13
    """
    spec = get_dataset_spec(dataset)
    return cls(
        dataset=str(spec.dataset),
        labels=[str(label) for label in spec.labels],
        max_seq_length=spec.max_seq_length,
    )

__call__

__call__(
    *,
    bbox: Float[Tensor, "batch elements 4"]
    | Float[ndarray, "batch elements 4"]
    | Sequence[ArrayLikeInput],
    labels: Int[Tensor, "batch elements"]
    | Int[ndarray, "batch elements"]
    | Sequence[ArrayLikeInput],
    mask: Bool[Tensor, "batch elements"]
    | Bool[ndarray, "batch elements"]
    | Sequence[ArrayLikeInput]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
) -> dict[str, Shaped[torch.Tensor, "..."]]

Encode a public layout batch.

Parameters:

Name Type Description Default
bbox Float[Tensor, 'batch elements 4'] | Float[ndarray, 'batch elements 4'] | Sequence[ArrayLikeInput]

Boxes in box_format.

required
labels Int[Tensor, 'batch elements'] | Int[ndarray, 'batch elements'] | Sequence[ArrayLikeInput]

Integer category labels.

required
mask Bool[Tensor, 'batch elements'] | Bool[ndarray, 'batch elements'] | Sequence[ArrayLikeInput] | None

Optional valid-element mask.

None
box_format BoxFormat | str

Input box format.

xywh
normalized bool

Whether input coordinates are already normalized.

True
canvas_size tuple[int, int] | None

Pixel canvas size required when normalized is false.

None

Returns:

Type Description
dict[str, Shaped[Tensor, '...']]

Dictionary containing encoded layout, normalized boxes, labels, and mask.

Raises:

Type Description
ValueError

If pixel boxes are passed without canvas_size or if box_format is unsupported.

Examples:

>>> processor = LaceProcessor.from_dataset("publaynet")
>>> out = processor(bbox=[[[0.5, 0.5, 0.2, 0.2]]], labels=[[0]])
>>> tuple(out["layout"].shape)
(1, 25, 10)
Source code in models/lace/src/lace/processing_lace.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def __call__(
    self,
    *,
    bbox: Float[torch.Tensor, "batch elements 4"]
    | Float[np.ndarray, "batch elements 4"]
    | Sequence[ArrayLikeInput],
    labels: Int[torch.Tensor, "batch elements"]
    | Int[np.ndarray, "batch elements"]
    | Sequence[ArrayLikeInput],
    mask: Bool[torch.Tensor, "batch elements"]
    | Bool[np.ndarray, "batch elements"]
    | Sequence[ArrayLikeInput]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
) -> dict[str, Shaped[torch.Tensor, "..."]]:
    """Encode a public layout batch.

    Args:
        bbox: Boxes in ``box_format``.
        labels: Integer category labels.
        mask: Optional valid-element mask.
        box_format: Input box format.
        normalized: Whether input coordinates are already normalized.
        canvas_size: Pixel canvas size required when ``normalized`` is false.

    Returns:
        Dictionary containing encoded layout, normalized boxes, labels, and mask.

    Raises:
        ValueError: If pixel boxes are passed without ``canvas_size`` or if
            ``box_format`` is unsupported.

    Examples:
        >>> processor = LaceProcessor.from_dataset("publaynet")
        >>> out = processor(bbox=[[[0.5, 0.5, 0.2, 0.2]]], labels=[[0]])
        >>> tuple(out["layout"].shape)
        (1, 25, 10)
    """
    bbox_t, labels_t, mask_t = prepare_layout_tensors(
        bbox=bbox,
        labels=labels,
        mask=mask,
        box_format=box_format,
        normalized=normalized,
        canvas_size=canvas_size,
    )
    bbox_t, labels_t, mask_t = self.pad(bbox_t, labels_t, mask_t)
    return {
        LACE_LAYOUT_KEY: self.encode(bbox_t, labels_t, mask_t),
        LACE_BBOX_KEY: bbox_t,
        LACE_LABELS_KEY: labels_t,
        LACE_MASK_KEY: mask_t,
    }

pad

pad(
    bbox: Float[Tensor, "batch elements 4"],
    labels: Int[Tensor, "batch elements"],
    mask: Bool[Tensor, "batch elements"] | None = None,
    max_seq_length: int | None = None,
) -> tuple[
    Float[torch.Tensor, "batch padded_elements 4"],
    Int[torch.Tensor, "batch padded_elements"],
    Bool[torch.Tensor, "batch padded_elements"],
]

Pad a batch to max_seq_length.

Parameters:

Name Type Description Default
bbox Float[Tensor, 'batch elements 4']

Normalized boxes with shape (batch, seq, 4).

required
labels Int[Tensor, 'batch elements']

Integer labels with shape (batch, seq).

required
mask Bool[Tensor, 'batch elements'] | None

Optional valid-element mask.

None
max_seq_length int | None

Optional override for output length.

None

Returns:

Type Description
tuple[Float[Tensor, 'batch padded_elements 4'], Int[Tensor, 'batch padded_elements'], Bool[Tensor, 'batch padded_elements']]

Padded boxes, labels, and mask.

Raises:

Type Description
ValueError

If the input has too many elements.

Source code in models/lace/src/lace/processing_lace.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def pad(
    self,
    bbox: Float[torch.Tensor, "batch elements 4"],
    labels: Int[torch.Tensor, "batch elements"],
    mask: Bool[torch.Tensor, "batch elements"] | None = None,
    max_seq_length: int | None = None,
) -> tuple[
    Float[torch.Tensor, "batch padded_elements 4"],
    Int[torch.Tensor, "batch padded_elements"],
    Bool[torch.Tensor, "batch padded_elements"],
]:
    """Pad a batch to ``max_seq_length``.

    Args:
        bbox: Normalized boxes with shape ``(batch, seq, 4)``.
        labels: Integer labels with shape ``(batch, seq)``.
        mask: Optional valid-element mask.
        max_seq_length: Optional override for output length.

    Returns:
        Padded boxes, labels, and mask.

    Raises:
        ValueError: If the input has too many elements.
    """
    max_len = max_seq_length or self.max_seq_length
    if bbox.shape[1] > max_len:
        raise ValueError(f"LACE supports at most {max_len} elements")

    if mask is None:
        mask = torch.ones(labels.shape, dtype=torch.bool, device=labels.device)
    pad_count = max_len - bbox.shape[1]
    if pad_count:
        bbox_pad = torch.zeros(
            bbox.shape[0], pad_count, 4, dtype=bbox.dtype, device=bbox.device
        )
        label_pad = torch.full(
            (labels.shape[0], pad_count),
            self.pad_label_id,
            dtype=labels.dtype,
            device=labels.device,
        )
        mask_pad = torch.zeros(
            mask.shape[0], pad_count, dtype=torch.bool, device=mask.device
        )
        bbox = torch.cat((bbox, bbox_pad), dim=1)
        labels = torch.cat((labels, label_pad), dim=1)
        mask = torch.cat((mask, mask_pad), dim=1)
    labels = labels.clone()
    labels[~mask] = self.pad_label_id
    return bbox, labels, mask

encode

encode(
    bbox: Float[Tensor, "batch elements 4"],
    labels: Int[Tensor, "batch elements"],
    mask: Bool[Tensor, "batch elements"] | None = None,
) -> Float[torch.Tensor, "batch padded_elements channels"]

Encode normalized boxes and labels into the LACE latent range.

Parameters:

Name Type Description Default
bbox Float[Tensor, 'batch elements 4']

Normalized center xywh boxes.

required
labels Int[Tensor, 'batch elements']

Integer labels.

required
mask Bool[Tensor, 'batch elements'] | None

Optional valid-element mask.

None

Returns:

Type Description
Float[Tensor, 'batch padded_elements channels']

Tensor with one-hot labels followed by box channels in [-1, 1].

Source code in models/lace/src/lace/processing_lace.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def encode(
    self,
    bbox: Float[torch.Tensor, "batch elements 4"],
    labels: Int[torch.Tensor, "batch elements"],
    mask: Bool[torch.Tensor, "batch elements"] | None = None,
) -> Float[torch.Tensor, "batch padded_elements channels"]:
    """Encode normalized boxes and labels into the LACE latent range.

    Args:
        bbox: Normalized center ``xywh`` boxes.
        labels: Integer labels.
        mask: Optional valid-element mask.

    Returns:
        Tensor with one-hot labels followed by box channels in ``[-1, 1]``.
    """
    bbox, labels, mask = self.pad(bbox, labels, mask)
    bbox_in = 2 * (bbox.clamp(0.0, 1.0) - 0.5)
    labels = labels.clamp(0, self.pad_label_id)
    labels[~mask] = self.pad_label_id
    one_hot = torch.nn.functional.one_hot(
        labels, num_classes=self.num_classes_with_pad
    ).to(dtype=bbox.dtype, device=bbox.device)
    return torch.cat((one_hot, bbox_in), dim=-1)

decode

decode(
    layout: Float[Tensor, "batch elements channels"],
    clamp: bool = True,
) -> LayoutGenerationOutput

Decode a LACE layout tensor into public output fields.

Parameters:

Name Type Description Default
layout Float[Tensor, 'batch elements channels']

Tensor with one-hot label channels and box channels.

required
clamp bool

Whether to clamp latent box channels before conversion.

True

Returns:

Type Description
LayoutGenerationOutput

Layout generation output with boxes in normalized center xywh.

Source code in models/lace/src/lace/processing_lace.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
def decode(
    self, layout: Float[torch.Tensor, "batch elements channels"], clamp: bool = True
) -> LayoutGenerationOutput:
    """Decode a LACE layout tensor into public output fields.

    Args:
        layout: Tensor with one-hot label channels and box channels.
        clamp: Whether to clamp latent box channels before conversion.

    Returns:
        Layout generation output with boxes in normalized center ``xywh``.
    """
    decoded = layout.clone()
    bbox_latent = decoded[:, :, self.num_classes_with_pad :]
    if clamp:
        bbox_latent = bbox_latent.clamp(-1.0, 1.0)
    bbox = bbox_latent / 2 + 0.5
    labels = decoded[:, :, : self.num_classes_with_pad].argmax(dim=2).long()
    mask = labels != self.pad_label_id
    return LayoutGenerationOutput(
        bbox=bbox,
        labels=labels,
        mask=mask,
        id2label=self.id2label,
        intermediates={"dataset": self.dataset},
    )

save_pretrained

save_pretrained(save_directory: str | Path) -> None

Save processor config to a Diffusers directory.

Parameters:

Name Type Description Default
save_directory str | Path

Directory where processor_config.json is written.

required
Source code in models/lace/src/lace/processing_lace.py
268
269
270
271
272
273
274
275
276
def save_pretrained(  # ty: ignore[invalid-method-override]
    self, save_directory: str | Path
) -> None:
    """Save processor config to a Diffusers directory.

    Args:
        save_directory: Directory where ``processor_config.json`` is written.
    """
    super().save_pretrained(save_directory)

from_pretrained classmethod

from_pretrained(
    pretrained_model_name_or_path: str | Path,
    cache_dir: str | Path | None = None,
    force_download: bool = False,
    local_files_only: bool = False,
    token: str | bool | None = None,
    revision: str = "main",
) -> "LaceProcessor"

Load processor config from a Diffusers directory.

Parameters:

Name Type Description Default
pretrained_model_name_or_path str | Path

Directory or Hub id containing processor_config.json.

required
cache_dir str | Path | None

Optional Hugging Face cache directory.

None
force_download bool

Whether to force a fresh download.

False
local_files_only bool

Whether to avoid network access.

False
token str | bool | None

Optional Hugging Face token.

None
revision str

Hub revision to load.

'main'

Returns:

Type Description
'LaceProcessor'

Loaded processor.

Source code in models/lace/src/lace/processing_lace.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
@classmethod
def from_pretrained(  # ty: ignore[invalid-method-override]
    cls,
    pretrained_model_name_or_path: str | Path,
    cache_dir: str | Path | None = None,
    force_download: bool = False,
    local_files_only: bool = False,
    token: str | bool | None = None,
    revision: str = "main",
) -> "LaceProcessor":
    """Load processor config from a Diffusers directory.

    Args:
        pretrained_model_name_or_path: Directory or Hub id containing
            ``processor_config.json``.
        cache_dir: Optional Hugging Face cache directory.
        force_download: Whether to force a fresh download.
        local_files_only: Whether to avoid network access.
        token: Optional Hugging Face token.
        revision: Hub revision to load.

    Returns:
        Loaded processor.
    """
    return super().from_pretrained(
        pretrained_model_name_or_path,
        cache_dir=cache_dir,
        force_download=force_download,
        local_files_only=local_files_only,
        token=token,
        revision=revision,
    )

LaceScheduler

Bases: SchedulerMixin, ConfigMixin

Scheduler for the converted LACE diffusion process.

Parameters:

Name Type Description Default
num_train_timesteps int

Number of timesteps used during training.

1000
beta_schedule BetaSchedule | str

Beta schedule enum or string value.

cosine
ddim_num_steps int

Default number of inference steps.

100
ddim_discretize DDIMDiscretization | str

DDIM discretization enum or string value.

uniform
eta float

Stochasticity parameter used by DDIM.

0.0
Source code in models/lace/src/lace/scheduling_lace.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
class LaceScheduler(SchedulerMixin, ConfigMixin):
    """Scheduler for the converted LACE diffusion process.

    Args:
        num_train_timesteps: Number of timesteps used during training.
        beta_schedule: Beta schedule enum or string value.
        ddim_num_steps: Default number of inference steps.
        ddim_discretize: DDIM discretization enum or string value.
        eta: Stochasticity parameter used by DDIM.
    """

    config_name = "scheduler_config.json"
    order = 1

    @register_to_config
    def __init__(
        self,
        *,
        num_train_timesteps: int = 1000,
        beta_schedule: BetaSchedule | str = BetaSchedule.cosine,
        ddim_num_steps: int = 100,
        ddim_discretize: DDIMDiscretization | str = DDIMDiscretization.uniform,
        eta: float = 0.0,
    ) -> None:
        """Initialize scheduler state and default timesteps."""
        self.num_train_timesteps = num_train_timesteps
        canonical_beta = normalize_beta_schedule(beta_schedule)
        canonical_ddim = normalize_ddim_discretization(ddim_discretize)
        self.beta_schedule = str(canonical_beta)
        self.ddim_num_steps = ddim_num_steps
        self.ddim_discretize = str(canonical_ddim)
        self.eta = eta
        betas = (
            _reference_cosine_betas(num_train_timesteps)
            if canonical_beta in (BetaSchedule.cosine, BetaSchedule.cosine_reverse)
            else make_beta_schedule(
                canonical_beta,
                num_timesteps=num_train_timesteps,
                start=0.0001,
                end=0.02,
            )
        ).float()
        alphas = 1.0 - betas
        self.alphas_cumprod = alphas.cumprod(dim=0)
        self.timesteps = torch.empty(0, dtype=torch.long)
        self.set_timesteps(ddim_num_steps)

    def set_timesteps(
        self, num_inference_steps: int | None = None, device: torch.device | None = None
    ) -> None:
        """Set the inference timesteps.

        Args:
            num_inference_steps: Number of denoising steps. Uses the configured
                default when omitted.
            device: Optional device for scheduler tensors.
        """
        steps = num_inference_steps or self.ddim_num_steps
        ddim = make_ddim_timesteps(
            self.ddim_discretize, steps, self.num_train_timesteps
        )
        self.ddim_timesteps = torch.as_tensor(ddim, dtype=torch.long, device=device)
        self.timesteps = torch.flip(self.ddim_timesteps, dims=(0,))
        if device is not None and self.beta_schedule in {
            str(BetaSchedule.cosine),
            str(BetaSchedule.cosine_reverse),
        }:
            alphas_cumprod = _alphas_cumprod_on_device(
                self.num_train_timesteps, torch.device(device)
            )
        else:
            alphas_cumprod = self.alphas_cumprod.to(device)

        self.ddim_alphas = alphas_cumprod[self.ddim_timesteps]
        self.ddim_alphas_prev = torch.as_tensor(
            [alphas_cumprod[0].item()]
            + alphas_cumprod[self.ddim_timesteps[:-1]].tolist(),
            dtype=torch.float32,
            device=device,
        )
        self.ddim_sigmas = self.eta * torch.sqrt(
            (1 - self.ddim_alphas_prev)
            / (1 - self.ddim_alphas)
            * (1 - self.ddim_alphas / self.ddim_alphas_prev)
        )
        self.sqrt_one_minus_alphas = torch.sqrt(1.0 - self.ddim_alphas)

    def alphas_cumprod_for_device(
        self, device: torch.device | str
    ) -> Float[torch.Tensor, "timesteps"]:
        """Return training cumulative alphas computed on ``device``."""
        target_device = torch.device(device)
        if self.beta_schedule in {
            str(BetaSchedule.cosine),
            str(BetaSchedule.cosine_reverse),
        }:
            return _alphas_cumprod_on_device(self.num_train_timesteps, target_device)

        return self.alphas_cumprod.to(target_device)

    def add_noise(
        self,
        original_samples: Float[torch.Tensor, "batch elements channels"],
        noise: Float[torch.Tensor, "batch elements channels"],
        timesteps: Int[torch.Tensor, "batch"],
    ) -> Float[torch.Tensor, "batch elements channels"]:
        """Add forward-process noise to clean samples.

        Args:
            original_samples: Clean layout tensor.
            noise: Noise tensor with the same shape.
            timesteps: Per-sample timestep ids.

        Returns:
            Noisy samples at the requested timesteps.
        """
        alphas = self.alphas_cumprod.to(original_samples.device)
        sqrt_alpha = torch.gather(alphas.sqrt(), 0, timesteps).reshape(-1, 1, 1)
        sqrt_one_minus = torch.gather(torch.sqrt(1.0 - alphas), 0, timesteps).reshape(
            -1, 1, 1
        )
        return sqrt_alpha * original_samples + sqrt_one_minus * noise

    def initial_sample(
        self,
        batch_size: int,
        seq_len: int,
        seq_dim: int,
        *,
        device: torch.device,
        generator: torch.Generator | None = None,
        stochastic: bool = True,
    ) -> Float[torch.Tensor, "batch elements channels"]:
        """Create the initial denoising sample.

        Args:
            batch_size: Number of layouts.
            seq_len: Number of elements per layout.
            seq_dim: Number of channels per element.
            device: Device for the output tensor.
            generator: Optional torch generator.
            stochastic: Whether to sample noise or return zeros.

        Returns:
            Initial sample tensor.
        """
        if not stochastic:
            return torch.zeros(batch_size, seq_len, seq_dim, device=device)

        return randn(batch_size, seq_len, seq_dim, device=device, generator=generator)

    def step(
        self,
        model_output: Float[torch.Tensor, "batch elements channels"],
        timestep: Int[torch.Tensor, "batch"],
        sample: Float[torch.Tensor, "batch elements channels"],
        index: int,
        generator: torch.Generator | None = None,
    ) -> LaceSchedulerOutput:
        """Take one reverse diffusion step.

        Args:
            model_output: Predicted noise from the denoiser.
            timestep: Public timestep tensor, kept for scheduler compatibility.
            sample: Current sample.
            index: Index into the scheduler timestep buffers.
            generator: Optional torch generator for stochastic DDIM noise.

        Returns:
            Previous sample and predicted clean sample.
        """
        del timestep
        alpha_t = torch.full(
            model_output.shape,
            float(self.ddim_alphas[index].item()),
            device=sample.device,
            dtype=sample.dtype,
        )
        alpha_prev = torch.full(
            model_output.shape,
            float(self.ddim_alphas_prev[index].item()),
            device=sample.device,
            dtype=sample.dtype,
        )
        sigma_t = torch.full(
            model_output.shape,
            float(self.ddim_sigmas[index].item()),
            device=sample.device,
            dtype=sample.dtype,
        )
        sqrt_one_minus = torch.full(
            model_output.shape,
            float(self.sqrt_one_minus_alphas[index].item()),
            device=sample.device,
            dtype=sample.dtype,
        )
        direction = (1.0 - alpha_prev - sigma_t**2).sqrt() * model_output
        noise = sigma_t * randn(
            sample.shape,
            dtype=sample.dtype,
            device=sample.device,
            generator=generator,
        )
        pred_original = (sample - sqrt_one_minus * model_output) / alpha_t.sqrt()
        prev_sample = alpha_prev.sqrt() * pred_original + direction + noise
        return LaceSchedulerOutput(
            prev_sample=prev_sample, pred_original_sample=pred_original
        )

    def refinement_indices(self, max_timestep: int = 201) -> list[int]:
        """Return scheduler indices used by LACE refinement sampling.

        Args:
            max_timestep: Maximum one-indexed DDIM timestep included.

        Returns:
            Descending list of scheduler buffer indices.
        """
        total = int(torch.sum(self.ddim_timesteps <= max_timestep).item())
        return list(range(total - 1, -1, -1))

__init__

__init__(
    *,
    num_train_timesteps: int = 1000,
    beta_schedule: BetaSchedule | str = BetaSchedule.cosine,
    ddim_num_steps: int = 100,
    ddim_discretize: DDIMDiscretization
    | str = DDIMDiscretization.uniform,
    eta: float = 0.0,
) -> None

Initialize scheduler state and default timesteps.

Source code in models/lace/src/lace/scheduling_lace.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@register_to_config
def __init__(
    self,
    *,
    num_train_timesteps: int = 1000,
    beta_schedule: BetaSchedule | str = BetaSchedule.cosine,
    ddim_num_steps: int = 100,
    ddim_discretize: DDIMDiscretization | str = DDIMDiscretization.uniform,
    eta: float = 0.0,
) -> None:
    """Initialize scheduler state and default timesteps."""
    self.num_train_timesteps = num_train_timesteps
    canonical_beta = normalize_beta_schedule(beta_schedule)
    canonical_ddim = normalize_ddim_discretization(ddim_discretize)
    self.beta_schedule = str(canonical_beta)
    self.ddim_num_steps = ddim_num_steps
    self.ddim_discretize = str(canonical_ddim)
    self.eta = eta
    betas = (
        _reference_cosine_betas(num_train_timesteps)
        if canonical_beta in (BetaSchedule.cosine, BetaSchedule.cosine_reverse)
        else make_beta_schedule(
            canonical_beta,
            num_timesteps=num_train_timesteps,
            start=0.0001,
            end=0.02,
        )
    ).float()
    alphas = 1.0 - betas
    self.alphas_cumprod = alphas.cumprod(dim=0)
    self.timesteps = torch.empty(0, dtype=torch.long)
    self.set_timesteps(ddim_num_steps)

set_timesteps

set_timesteps(
    num_inference_steps: int | None = None,
    device: device | None = None,
) -> None

Set the inference timesteps.

Parameters:

Name Type Description Default
num_inference_steps int | None

Number of denoising steps. Uses the configured default when omitted.

None
device device | None

Optional device for scheduler tensors.

None
Source code in models/lace/src/lace/scheduling_lace.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def set_timesteps(
    self, num_inference_steps: int | None = None, device: torch.device | None = None
) -> None:
    """Set the inference timesteps.

    Args:
        num_inference_steps: Number of denoising steps. Uses the configured
            default when omitted.
        device: Optional device for scheduler tensors.
    """
    steps = num_inference_steps or self.ddim_num_steps
    ddim = make_ddim_timesteps(
        self.ddim_discretize, steps, self.num_train_timesteps
    )
    self.ddim_timesteps = torch.as_tensor(ddim, dtype=torch.long, device=device)
    self.timesteps = torch.flip(self.ddim_timesteps, dims=(0,))
    if device is not None and self.beta_schedule in {
        str(BetaSchedule.cosine),
        str(BetaSchedule.cosine_reverse),
    }:
        alphas_cumprod = _alphas_cumprod_on_device(
            self.num_train_timesteps, torch.device(device)
        )
    else:
        alphas_cumprod = self.alphas_cumprod.to(device)

    self.ddim_alphas = alphas_cumprod[self.ddim_timesteps]
    self.ddim_alphas_prev = torch.as_tensor(
        [alphas_cumprod[0].item()]
        + alphas_cumprod[self.ddim_timesteps[:-1]].tolist(),
        dtype=torch.float32,
        device=device,
    )
    self.ddim_sigmas = self.eta * torch.sqrt(
        (1 - self.ddim_alphas_prev)
        / (1 - self.ddim_alphas)
        * (1 - self.ddim_alphas / self.ddim_alphas_prev)
    )
    self.sqrt_one_minus_alphas = torch.sqrt(1.0 - self.ddim_alphas)

alphas_cumprod_for_device

alphas_cumprod_for_device(
    device: device | str,
) -> Float[torch.Tensor, "timesteps"]

Return training cumulative alphas computed on device.

Source code in models/lace/src/lace/scheduling_lace.py
161
162
163
164
165
166
167
168
169
170
171
172
def alphas_cumprod_for_device(
    self, device: torch.device | str
) -> Float[torch.Tensor, "timesteps"]:
    """Return training cumulative alphas computed on ``device``."""
    target_device = torch.device(device)
    if self.beta_schedule in {
        str(BetaSchedule.cosine),
        str(BetaSchedule.cosine_reverse),
    }:
        return _alphas_cumprod_on_device(self.num_train_timesteps, target_device)

    return self.alphas_cumprod.to(target_device)

add_noise

add_noise(
    original_samples: Float[
        Tensor, "batch elements channels"
    ],
    noise: Float[Tensor, "batch elements channels"],
    timesteps: Int[Tensor, "batch"],
) -> Float[torch.Tensor, "batch elements channels"]

Add forward-process noise to clean samples.

Parameters:

Name Type Description Default
original_samples Float[Tensor, 'batch elements channels']

Clean layout tensor.

required
noise Float[Tensor, 'batch elements channels']

Noise tensor with the same shape.

required
timesteps Int[Tensor, 'batch']

Per-sample timestep ids.

required

Returns:

Type Description
Float[Tensor, 'batch elements channels']

Noisy samples at the requested timesteps.

Source code in models/lace/src/lace/scheduling_lace.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def add_noise(
    self,
    original_samples: Float[torch.Tensor, "batch elements channels"],
    noise: Float[torch.Tensor, "batch elements channels"],
    timesteps: Int[torch.Tensor, "batch"],
) -> Float[torch.Tensor, "batch elements channels"]:
    """Add forward-process noise to clean samples.

    Args:
        original_samples: Clean layout tensor.
        noise: Noise tensor with the same shape.
        timesteps: Per-sample timestep ids.

    Returns:
        Noisy samples at the requested timesteps.
    """
    alphas = self.alphas_cumprod.to(original_samples.device)
    sqrt_alpha = torch.gather(alphas.sqrt(), 0, timesteps).reshape(-1, 1, 1)
    sqrt_one_minus = torch.gather(torch.sqrt(1.0 - alphas), 0, timesteps).reshape(
        -1, 1, 1
    )
    return sqrt_alpha * original_samples + sqrt_one_minus * noise

initial_sample

initial_sample(
    batch_size: int,
    seq_len: int,
    seq_dim: int,
    *,
    device: device,
    generator: Generator | None = None,
    stochastic: bool = True,
) -> Float[torch.Tensor, "batch elements channels"]

Create the initial denoising sample.

Parameters:

Name Type Description Default
batch_size int

Number of layouts.

required
seq_len int

Number of elements per layout.

required
seq_dim int

Number of channels per element.

required
device device

Device for the output tensor.

required
generator Generator | None

Optional torch generator.

None
stochastic bool

Whether to sample noise or return zeros.

True

Returns:

Type Description
Float[Tensor, 'batch elements channels']

Initial sample tensor.

Source code in models/lace/src/lace/scheduling_lace.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def initial_sample(
    self,
    batch_size: int,
    seq_len: int,
    seq_dim: int,
    *,
    device: torch.device,
    generator: torch.Generator | None = None,
    stochastic: bool = True,
) -> Float[torch.Tensor, "batch elements channels"]:
    """Create the initial denoising sample.

    Args:
        batch_size: Number of layouts.
        seq_len: Number of elements per layout.
        seq_dim: Number of channels per element.
        device: Device for the output tensor.
        generator: Optional torch generator.
        stochastic: Whether to sample noise or return zeros.

    Returns:
        Initial sample tensor.
    """
    if not stochastic:
        return torch.zeros(batch_size, seq_len, seq_dim, device=device)

    return randn(batch_size, seq_len, seq_dim, device=device, generator=generator)

step

step(
    model_output: Float[Tensor, "batch elements channels"],
    timestep: Int[Tensor, "batch"],
    sample: Float[Tensor, "batch elements channels"],
    index: int,
    generator: Generator | None = None,
) -> LaceSchedulerOutput

Take one reverse diffusion step.

Parameters:

Name Type Description Default
model_output Float[Tensor, 'batch elements channels']

Predicted noise from the denoiser.

required
timestep Int[Tensor, 'batch']

Public timestep tensor, kept for scheduler compatibility.

required
sample Float[Tensor, 'batch elements channels']

Current sample.

required
index int

Index into the scheduler timestep buffers.

required
generator Generator | None

Optional torch generator for stochastic DDIM noise.

None

Returns:

Type Description
LaceSchedulerOutput

Previous sample and predicted clean sample.

Source code in models/lace/src/lace/scheduling_lace.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def step(
    self,
    model_output: Float[torch.Tensor, "batch elements channels"],
    timestep: Int[torch.Tensor, "batch"],
    sample: Float[torch.Tensor, "batch elements channels"],
    index: int,
    generator: torch.Generator | None = None,
) -> LaceSchedulerOutput:
    """Take one reverse diffusion step.

    Args:
        model_output: Predicted noise from the denoiser.
        timestep: Public timestep tensor, kept for scheduler compatibility.
        sample: Current sample.
        index: Index into the scheduler timestep buffers.
        generator: Optional torch generator for stochastic DDIM noise.

    Returns:
        Previous sample and predicted clean sample.
    """
    del timestep
    alpha_t = torch.full(
        model_output.shape,
        float(self.ddim_alphas[index].item()),
        device=sample.device,
        dtype=sample.dtype,
    )
    alpha_prev = torch.full(
        model_output.shape,
        float(self.ddim_alphas_prev[index].item()),
        device=sample.device,
        dtype=sample.dtype,
    )
    sigma_t = torch.full(
        model_output.shape,
        float(self.ddim_sigmas[index].item()),
        device=sample.device,
        dtype=sample.dtype,
    )
    sqrt_one_minus = torch.full(
        model_output.shape,
        float(self.sqrt_one_minus_alphas[index].item()),
        device=sample.device,
        dtype=sample.dtype,
    )
    direction = (1.0 - alpha_prev - sigma_t**2).sqrt() * model_output
    noise = sigma_t * randn(
        sample.shape,
        dtype=sample.dtype,
        device=sample.device,
        generator=generator,
    )
    pred_original = (sample - sqrt_one_minus * model_output) / alpha_t.sqrt()
    prev_sample = alpha_prev.sqrt() * pred_original + direction + noise
    return LaceSchedulerOutput(
        prev_sample=prev_sample, pred_original_sample=pred_original
    )

refinement_indices

refinement_indices(max_timestep: int = 201) -> list[int]

Return scheduler indices used by LACE refinement sampling.

Parameters:

Name Type Description Default
max_timestep int

Maximum one-indexed DDIM timestep included.

201

Returns:

Type Description
list[int]

Descending list of scheduler buffer indices.

Source code in models/lace/src/lace/scheduling_lace.py
283
284
285
286
287
288
289
290
291
292
293
def refinement_indices(self, max_timestep: int = 201) -> list[int]:
    """Return scheduler indices used by LACE refinement sampling.

    Args:
        max_timestep: Maximum one-indexed DDIM timestep included.

    Returns:
        Descending list of scheduler buffer indices.
    """
    total = int(torch.sum(self.ddim_timesteps <= max_timestep).item())
    return list(range(total - 1, -1, -1))

LaceSchedulerOutput dataclass

Bases: BaseOutput

Output returned by one reverse diffusion step.

Attributes:

Name Type Description
prev_sample Float[Tensor, 'batch elements channels']

Sample for the next denoising iteration.

pred_original_sample Float[Tensor, 'batch elements channels']

Scheduler estimate of the clean layout tensor.

Source code in models/lace/src/lace/scheduling_lace.py
61
62
63
64
65
66
67
68
69
70
71
@dataclass
class LaceSchedulerOutput(BaseOutput):
    """Output returned by one reverse diffusion step.

    Attributes:
        prev_sample: Sample for the next denoising iteration.
        pred_original_sample: Scheduler estimate of the clean layout tensor.
    """

    prev_sample: Float[torch.Tensor, "batch elements channels"]
    pred_original_sample: Float[torch.Tensor, "batch elements channels"]

default_model_config

default_model_config(
    dataset: DatasetName | str,
) -> LaceModelConfigKwargs

Build the model config for a dataset.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset enum or a supported string alias.

required

Returns:

Type Description
LaceModelConfigKwargs

Keyword arguments accepted by LaceTransformerModel.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Examples:

>>> default_model_config("rico25")["seq_dim"]
30
Source code in models/lace/src/lace/configuration_lace.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def default_model_config(dataset: DatasetName | str) -> LaceModelConfigKwargs:
    """Build the model config for a dataset.

    Args:
        dataset: Canonical dataset enum or a supported string alias.

    Returns:
        Keyword arguments accepted by ``LaceTransformerModel``.

    Raises:
        ValueError: If the dataset is unsupported.

    Examples:
        >>> default_model_config("rico25")["seq_dim"]
        30
    """
    spec = get_dataset_spec(dataset)
    return {
        "seq_dim": spec.seq_dim,
        "max_seq_length": spec.max_seq_length,
        "num_layers": spec.num_layers,
        "dim_transformer": spec.dim_transformer,
        "nhead": spec.nhead,
        "dim_feedforward": spec.dim_feedforward,
        "diffusion_step": 1000,
        "timestep_type": TimestepEmbeddingType.adalayernorm,
    }

get_dataset_spec

get_dataset_spec(
    dataset: DatasetName | str,
) -> LaceDatasetSpec

Return dataset metadata for a LACE checkpoint.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset enum or a supported string alias.

required

Returns:

Type Description
LaceDatasetSpec

Dataset specification.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Examples:

>>> get_dataset_spec("publaynet").seq_dim
10
Source code in models/lace/src/lace/configuration_lace.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def get_dataset_spec(dataset: DatasetName | str) -> LaceDatasetSpec:
    """Return dataset metadata for a LACE checkpoint.

    Args:
        dataset: Canonical dataset enum or a supported string alias.

    Returns:
        Dataset specification.

    Raises:
        ValueError: If the dataset is unsupported.

    Examples:
        >>> get_dataset_spec("publaynet").seq_dim
        10
    """
    return DATASET_SPECS[normalize_dataset(dataset)]

normalize_dataset

normalize_dataset(
    dataset: DatasetName | str,
) -> DatasetName

Normalize a public dataset name to the shared dataset enum.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset enum or a supported string alias.

required

Returns:

Type Description
DatasetName

Canonical shared dataset name.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Examples:

>>> str(normalize_dataset("rico13_max25"))
'rico13'
Source code in models/lace/src/lace/configuration_lace.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def normalize_dataset(dataset: DatasetName | str) -> DatasetName:
    """Normalize a public dataset name to the shared dataset enum.

    Args:
        dataset: Canonical dataset enum or a supported string alias.

    Returns:
        Canonical shared dataset name.

    Raises:
        ValueError: If the dataset is unsupported.

    Examples:
        >>> str(normalize_dataset("rico13_max25"))
        'rico13'
    """
    if isinstance(dataset, DatasetName):
        return dataset
    key = dataset.lower().replace("-", "_")
    if key in _LACE_DATASET_ALIASES:
        return _LACE_DATASET_ALIASES[key]
    try:
        return normalize_dataset_name(dataset)
    except ValueError as exc:
        raise ValueError(f"Unsupported LACE dataset: {dataset}") from exc

beautify_layout

beautify_layout(
    bbox: Float[Tensor, "batch elements 4"],
    mask: Bool[Tensor, "batch elements"],
    overlap_weight: float = 1.0,
    alignment_weight: float = 1.0,
    xy_only: bool = False,
    num_steps: int = 1000,
    lr: float = 0.0001,
) -> tuple[
    Float[torch.Tensor, "batch elements 4"],
    Bool[torch.Tensor, "batch elements"],
]

Optimize generated boxes with overlap and alignment penalties.

Parameters:

Name Type Description Default
bbox Float[Tensor, 'batch elements 4']

Normalized center xywh boxes with shape (batch, seq, 4).

required
mask Bool[Tensor, 'batch elements']

Valid-element mask with shape (batch, seq).

required
overlap_weight float

Weight for pairwise overlap penalties.

1.0
alignment_weight float

Weight for alignment penalties.

1.0
xy_only bool

Whether to keep width and height fixed.

False
num_steps int

Number of Adam optimization steps.

1000
lr float

Adam learning rate.

0.0001

Returns:

Type Description
tuple[Float[Tensor, 'batch elements 4'], Bool[Tensor, 'batch elements']]

Optimized boxes and updated mask.

Source code in models/lace/src/lace/constraints.py
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def beautify_layout(
    bbox: Float[torch.Tensor, "batch elements 4"],
    mask: Bool[torch.Tensor, "batch elements"],
    overlap_weight: float = 1.0,
    alignment_weight: float = 1.0,
    xy_only: bool = False,
    num_steps: int = 1000,
    lr: float = 1e-4,
) -> tuple[
    Float[torch.Tensor, "batch elements 4"],
    Bool[torch.Tensor, "batch elements"],
]:
    """Optimize generated boxes with overlap and alignment penalties.

    Args:
        bbox: Normalized center ``xywh`` boxes with shape ``(batch, seq, 4)``.
        mask: Valid-element mask with shape ``(batch, seq)``.
        overlap_weight: Weight for pairwise overlap penalties.
        alignment_weight: Weight for alignment penalties.
        xy_only: Whether to keep width and height fixed.
        num_steps: Number of Adam optimization steps.
        lr: Adam learning rate.

    Returns:
        Optimized boxes and updated mask.
    """
    if torch.sum(mask) == 1:
        return bbox, mask

    bbox_in = bbox
    if xy_only:
        wh = torch.abs(bbox[:, :, 2:].clone().detach())
        bbox_in = torch.cat([bbox[:, :, :2], wh], dim=2)

    bbox_in = bbox_in.clone()
    bbox_in[:, :, [0, 2]] *= 10 / 4
    bbox_in[:, :, [1, 3]] *= 10 / 6
    bbox_initial = bbox_in.clone().detach()
    bbox_param = nn.Parameter(bbox_in)
    optimizer = torch.optim.Adam([bbox_param], lr=lr)
    mse_loss = nn.MSELoss()
    mask_out = mask.clone()

    with torch.enable_grad():
        for _ in range(num_steps):
            bbox_relu = torch.relu(bbox_param)
            align_score = _layout_alignment_matrix(bbox_relu, mask_out)
            align_mask = (align_score < 1 / 64).clone().detach()
            align_loss = torch.mean(align_score * align_mask)
            piou = torch.mean(_pairwise_iou_xywh(bbox_relu, mask_out))
            mse = mse_loss(bbox_relu, bbox_initial)
            loss = mse + alignment_weight * align_loss + overlap_weight * piou
            optimizer.zero_grad()
            loss.backward()
            torch.nn.utils.clip_grad_norm_([bbox_param], 1.0)
            optimizer.step()
            min_wh = torch.min(bbox_relu[:, :, [2, 3]], dim=2).values
            mask_out = mask_out * (min_wh > 0.01)

    bbox_out = torch.relu(bbox_param.detach())
    bbox_out[:, :, [0, 2]] *= 4 / 10
    bbox_out[:, :, [1, 3]] *= 6 / 10
    return bbox_out, mask_out

build_pipeline_from_vendor_checkpoint

build_pipeline_from_vendor_checkpoint(
    dataset: DatasetName | str,
    checkpoint_path: str | Path,
    ddim_num_steps: int = 100,
) -> LacePipeline

Build a LACE pipeline from a vendor checkpoint.

Parameters:

Name Type Description Default
dataset DatasetName | str

LACE dataset name or alias.

required
checkpoint_path str | Path

Path to the vendor checkpoint.

required
ddim_num_steps int

Number of DDIM inference steps configured on the scheduler.

100

Returns:

Type Description
LacePipeline

Pipeline containing converted model, scheduler, and processor.

Raises:

Type Description
TypeError

If the checkpoint payload is not a state dictionary.

ValueError

If converted keys do not match the model architecture.

Source code in models/lace/src/lace/conversion.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def build_pipeline_from_vendor_checkpoint(
    dataset: DatasetName | str,
    checkpoint_path: str | Path,
    ddim_num_steps: int = 100,
) -> LacePipeline:
    """Build a LACE pipeline from a vendor checkpoint.

    Args:
        dataset: LACE dataset name or alias.
        checkpoint_path: Path to the vendor checkpoint.
        ddim_num_steps: Number of DDIM inference steps configured on the scheduler.

    Returns:
        Pipeline containing converted model, scheduler, and processor.

    Raises:
        TypeError: If the checkpoint payload is not a state dictionary.
        ValueError: If converted keys do not match the model architecture.
    """
    model = LaceTransformerModel(**default_model_config(dataset))
    converted = convert_state_dict(load_vendor_state_dict(checkpoint_path))
    expected = set(model.state_dict())
    actual = set(converted)
    missing = sorted(expected - actual)
    unexpected = sorted(actual - expected)
    if missing or unexpected:
        raise ValueError(
            f"State dict mismatch: missing={missing[:10]}, unexpected={unexpected[:10]}"
        )

    model.load_state_dict(converted, strict=True)
    processor = LaceProcessor.from_dataset(dataset)
    scheduler = LaceScheduler(ddim_num_steps=ddim_num_steps)
    return LacePipeline(model=model, scheduler=scheduler, processor=processor)

convert_state_dict

convert_state_dict(
    state_dict: Mapping[str, Shaped[Tensor, "..."]],
) -> dict[str, Shaped[torch.Tensor, "..."]]

Convert vendor parameter names to the Diffusers module names.

Parameters:

Name Type Description Default
state_dict Mapping[str, Shaped[Tensor, '...']]

Vendor state dictionary.

required

Returns:

Type Description
dict[str, Shaped[Tensor, '...']]

Converted state dictionary with distributed prefixes and vendor

dict[str, Shaped[Tensor, '...']]

positional buffers removed.

Source code in models/lace/src/lace/conversion.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def convert_state_dict(
    state_dict: Mapping[str, Shaped[torch.Tensor, "..."]],
) -> dict[str, Shaped[torch.Tensor, "..."]]:
    """Convert vendor parameter names to the Diffusers module names.

    Args:
        state_dict: Vendor state dictionary.

    Returns:
        Converted state dictionary with distributed prefixes and vendor
        positional buffers removed.
    """
    return {
        key.removeprefix("module."): value
        for key, value in state_dict.items()
        if key != "pos_embed" and key.removeprefix("module.") != "pos_embed"
    }

load_vendor_state_dict

load_vendor_state_dict(
    path: str | Path,
) -> dict[str, Shaped[torch.Tensor, "..."]]

Load a PyTorch checkpoint as a state dictionary.

Parameters:

Name Type Description Default
path str | Path

Path to a vendor checkpoint or state-dict file.

required

Returns:

Type Description
dict[str, Shaped[Tensor, '...']]

Mapping from parameter names to tensors.

Raises:

Type Description
TypeError

If the checkpoint does not contain a state-dict-like object.

Source code in models/lace/src/lace/conversion.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def load_vendor_state_dict(path: str | Path) -> dict[str, Shaped[torch.Tensor, "..."]]:
    """Load a PyTorch checkpoint as a state dictionary.

    Args:
        path: Path to a vendor checkpoint or state-dict file.

    Returns:
        Mapping from parameter names to tensors.

    Raises:
        TypeError: If the checkpoint does not contain a state-dict-like object.
    """
    loaded = torch.load(path, map_location="cpu")
    if isinstance(loaded, dict) and "state_dict" in loaded:
        loaded = loaded["state_dict"]
    if not isinstance(loaded, dict):
        raise TypeError(f"Expected a state_dict-like checkpoint at {path}")

    return dict(loaded)

lace_model_card

lace_model_card(
    dataset: DatasetName | str,
    *,
    parity_metrics: list[ParityMetric] | None = None,
) -> ModelCard

Create the Hugging Face model card for a LACE checkpoint.

Parameters:

Name Type Description Default
dataset DatasetName | str

LACE dataset name or alias.

required
parity_metrics list[ParityMetric] | None

Optional parity metrics to embed in the evaluation section.

None

Returns:

Type Description
ModelCard

Rendered Hugging Face Hub model card.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Source code in models/lace/src/lace/model_card.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def lace_model_card(
    dataset: DatasetName | str,
    *,
    parity_metrics: list[ParityMetric] | None = None,
) -> ModelCard:
    """Create the Hugging Face model card for a LACE checkpoint.

    Args:
        dataset: LACE dataset name or alias.
        parity_metrics: Optional parity metrics to embed in the evaluation section.

    Returns:
        Rendered Hugging Face Hub model card.

    Raises:
        ValueError: If the dataset is unsupported.
    """
    dataset_key = normalize_dataset(dataset)
    dataset_name = str(dataset_key)
    model_id = _MODEL_IDS[dataset_key]
    metrics = parity_metrics
    if metrics is None:
        metrics = (
            [_PARITY_METRICS[dataset_key]] if dataset_key in _PARITY_METRICS else []
        )
    how_to_use = f"""
from lace import LacePipeline

pipe = LacePipeline.from_pretrained("{model_id}")
out = pipe(batch_size=1, seed=0, num_inference_steps=100)
print(out.bbox, out.labels, out.mask)
"""
    details = (
        "Diffusers-format conversion of the LACE checkpoint for "
        f"`{dataset_name}`. LACE is a continuous diffusion layout generation model "
        "from the paper 'Towards Aligned Layout Generation via Diffusion Model "
        "with Aesthetic Constraints' (https://arxiv.org/abs/2402.04754). "
        "The pipeline generates normalized center `xywh` layout boxes, "
        "category labels, and masks."
    )
    if dataset_key is DatasetName.rico13:
        details += (
            " The public checkpoint archive used for local parity "
            "verification does not include `rico13_best.pt`; Rico13 parity "
            "metrics are therefore not reported here."
        )
    return build_layout_model_card(
        model_id=model_id,
        model_name=f"LACE {dataset_name}",
        dataset_ids=[_DATASET_IDS[dataset_key]],
        license="mit",
        library_name="diffusers",
        pipeline_tag="other",
        tags=[
            "layout-generation",
            "lace",
            "diffusers",
            dataset_name,
        ],
        model_details=details,
        intended_uses=(
            "Use this checkpoint for research and evaluation of document and UI "
            "layout generation workflows."
        ),
        limitations=(
            "The converted checkpoint follows the original LACE release and is "
            "intended for layout synthesis, not for image rendering or OCR. "
            "Generated layouts may require downstream filtering for task-specific "
            "aesthetic or overlap constraints."
        ),
        how_to_use=how_to_use,
        training_data=(
            f"The original checkpoint was trained on `{_DATASET_IDS[dataset_key]}` "
            "as released by the original LACE project."
        ),
        parity_metrics=metrics,
        citation_bibtex=_LACE_BIBTEX,
        original_implementation_url="https://github.com/puar-playground/LACE",
    )

write_lace_model_card

write_lace_model_card(
    output_dir: str | Path,
    dataset: DatasetName | str,
    *,
    parity_metrics: list[ParityMetric] | None = None,
) -> Path

Write the LACE model card into a model directory.

Parameters:

Name Type Description Default
output_dir str | Path

Directory where README.md is written.

required
dataset DatasetName | str

LACE dataset name or alias.

required
parity_metrics list[ParityMetric] | None

Optional parity metrics to embed in the evaluation section.

None

Returns:

Type Description
Path

Path to the written README.md.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Source code in models/lace/src/lace/model_card.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def write_lace_model_card(
    output_dir: str | Path,
    dataset: DatasetName | str,
    *,
    parity_metrics: list[ParityMetric] | None = None,
) -> Path:
    """Write the LACE model card into a model directory.

    Args:
        output_dir: Directory where ``README.md`` is written.
        dataset: LACE dataset name or alias.
        parity_metrics: Optional parity metrics to embed in the evaluation section.

    Returns:
        Path to the written ``README.md``.

    Raises:
        ValueError: If the dataset is unsupported.
    """
    path = Path(output_dir) / "README.md"
    path.write_text(
        str(lace_model_card(dataset, parity_metrics=parity_metrics)),
        encoding="utf-8",
    )
    return path

normalize_condition_type

normalize_condition_type(
    condition_type: ConditionType | str | None,
) -> ConditionType

Normalize public condition aliases.

Parameters:

Name Type Description Default
condition_type ConditionType | str | None

Canonical condition enum, string alias, or None.

required

Returns:

Type Description
ConditionType

Canonical condition enum.

Raises:

Type Description
ValueError

If the condition type is unsupported.

Examples:

>>> normalize_condition_type("cwh") is ConditionType.label_size
True
Source code in models/lace/src/lace/pipeline_lace.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def normalize_condition_type(
    condition_type: ConditionType | str | None,
) -> ConditionType:
    """Normalize public condition aliases.

    Args:
        condition_type: Canonical condition enum, string alias, or ``None``.

    Returns:
        Canonical condition enum.

    Raises:
        ValueError: If the condition type is unsupported.

    Examples:
        >>> normalize_condition_type("cwh") is ConditionType.label_size
        True
    """
    if isinstance(condition_type, ConditionType):
        canonical = condition_type
    elif condition_type is None:
        canonical = ConditionType.unconditional
    else:
        try:
            canonical = normalize_shared_condition_type(condition_type)
        except ValueError:
            key = condition_type.lower().replace("-", "_")
            try:
                canonical = _LACE_CONDITION_ALIASES[LaceConditionAlias(key)]
            except ValueError as exc:
                raise ValueError(
                    f"Unsupported LACE condition_type: {condition_type}"
                ) from exc

    if canonical not in _SUPPORTED_CONDITION_TYPES:
        raise ValueError(f"Unsupported LACE condition_type: {condition_type}")

    return canonical

normalize_output_type

normalize_output_type(
    output_type: PipelineOutputType | str,
) -> PipelineOutputType

Normalize public output type aliases.

Parameters:

Name Type Description Default
output_type PipelineOutputType | str

Output enum or string value.

required

Returns:

Type Description
PipelineOutputType

Canonical output type.

Raises:

Type Description
ValueError

If the output type is unsupported.

Source code in models/lace/src/lace/pipeline_lace.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def normalize_output_type(output_type: PipelineOutputType | str) -> PipelineOutputType:
    """Normalize public output type aliases.

    Args:
        output_type: Output enum or string value.

    Returns:
        Canonical output type.

    Raises:
        ValueError: If the output type is unsupported.
    """
    if isinstance(output_type, PipelineOutputType):
        return output_type
    try:
        return PipelineOutputType(output_type)
    except ValueError as exc:
        raise ValueError(f"Unsupported output_type: {output_type}") from exc

configuration_lace

Dataset configuration helpers for LACE checkpoints.

LaceDatasetSpec dataclass

Static dataset metadata used to configure a LACE checkpoint.

Attributes:

Name Type Description
dataset DatasetName

Canonical dataset name.

labels tuple[LaceLabel, ...]

Ordered category labels without the padding class.

max_seq_length int

Maximum number of layout elements.

dim_transformer int

Transformer hidden size used by the original model.

nhead int

Number of attention heads.

num_layers int

Number of transformer blocks.

dim_feedforward int

Feed-forward hidden size.

Source code in models/lace/src/lace/configuration_lace.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@dataclass(frozen=True)
class LaceDatasetSpec:
    """Static dataset metadata used to configure a LACE checkpoint.

    Attributes:
        dataset: Canonical dataset name.
        labels: Ordered category labels without the padding class.
        max_seq_length: Maximum number of layout elements.
        dim_transformer: Transformer hidden size used by the original model.
        nhead: Number of attention heads.
        num_layers: Number of transformer blocks.
        dim_feedforward: Feed-forward hidden size.
    """

    dataset: DatasetName
    labels: tuple[LaceLabel, ...]
    max_seq_length: int = 25
    dim_transformer: int = 512
    nhead: int = 16
    num_layers: int = 4
    dim_feedforward: int = 2048

    @property
    def pad_label_id(self) -> int:
        """Return the integer id reserved for padding."""
        return len(self.labels)

    @property
    def num_classes_with_pad(self) -> int:
        """Return the number of category channels including padding."""
        return len(self.labels) + 1

    @property
    def seq_dim(self) -> int:
        """Return the latent per-element feature size."""
        return self.num_classes_with_pad + 4

    @property
    def id2label(self) -> dict[int, str]:
        """Return the category id to label mapping."""
        return dict(enumerate(str(label) for label in self.labels))

pad_label_id property

pad_label_id: int

Return the integer id reserved for padding.

num_classes_with_pad property

num_classes_with_pad: int

Return the number of category channels including padding.

seq_dim property

seq_dim: int

Return the latent per-element feature size.

id2label property

id2label: dict[int, str]

Return the category id to label mapping.

LaceModelConfigKwargs

Bases: TypedDict

Keyword arguments accepted by LaceTransformerModel.

Source code in models/lace/src/lace/configuration_lace.py
68
69
70
71
72
73
74
75
76
77
78
class LaceModelConfigKwargs(TypedDict):
    """Keyword arguments accepted by ``LaceTransformerModel``."""

    seq_dim: int
    max_seq_length: int
    num_layers: int
    dim_transformer: int
    nhead: int
    dim_feedforward: int
    diffusion_step: int
    timestep_type: TimestepEmbeddingType

normalize_dataset

normalize_dataset(
    dataset: DatasetName | str,
) -> DatasetName

Normalize a public dataset name to the shared dataset enum.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset enum or a supported string alias.

required

Returns:

Type Description
DatasetName

Canonical shared dataset name.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Examples:

>>> str(normalize_dataset("rico13_max25"))
'rico13'
Source code in models/lace/src/lace/configuration_lace.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def normalize_dataset(dataset: DatasetName | str) -> DatasetName:
    """Normalize a public dataset name to the shared dataset enum.

    Args:
        dataset: Canonical dataset enum or a supported string alias.

    Returns:
        Canonical shared dataset name.

    Raises:
        ValueError: If the dataset is unsupported.

    Examples:
        >>> str(normalize_dataset("rico13_max25"))
        'rico13'
    """
    if isinstance(dataset, DatasetName):
        return dataset
    key = dataset.lower().replace("-", "_")
    if key in _LACE_DATASET_ALIASES:
        return _LACE_DATASET_ALIASES[key]
    try:
        return normalize_dataset_name(dataset)
    except ValueError as exc:
        raise ValueError(f"Unsupported LACE dataset: {dataset}") from exc

get_dataset_spec

get_dataset_spec(
    dataset: DatasetName | str,
) -> LaceDatasetSpec

Return dataset metadata for a LACE checkpoint.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset enum or a supported string alias.

required

Returns:

Type Description
LaceDatasetSpec

Dataset specification.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Examples:

>>> get_dataset_spec("publaynet").seq_dim
10
Source code in models/lace/src/lace/configuration_lace.py
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def get_dataset_spec(dataset: DatasetName | str) -> LaceDatasetSpec:
    """Return dataset metadata for a LACE checkpoint.

    Args:
        dataset: Canonical dataset enum or a supported string alias.

    Returns:
        Dataset specification.

    Raises:
        ValueError: If the dataset is unsupported.

    Examples:
        >>> get_dataset_spec("publaynet").seq_dim
        10
    """
    return DATASET_SPECS[normalize_dataset(dataset)]

default_model_config

default_model_config(
    dataset: DatasetName | str,
) -> LaceModelConfigKwargs

Build the model config for a dataset.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset enum or a supported string alias.

required

Returns:

Type Description
LaceModelConfigKwargs

Keyword arguments accepted by LaceTransformerModel.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Examples:

>>> default_model_config("rico25")["seq_dim"]
30
Source code in models/lace/src/lace/configuration_lace.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def default_model_config(dataset: DatasetName | str) -> LaceModelConfigKwargs:
    """Build the model config for a dataset.

    Args:
        dataset: Canonical dataset enum or a supported string alias.

    Returns:
        Keyword arguments accepted by ``LaceTransformerModel``.

    Raises:
        ValueError: If the dataset is unsupported.

    Examples:
        >>> default_model_config("rico25")["seq_dim"]
        30
    """
    spec = get_dataset_spec(dataset)
    return {
        "seq_dim": spec.seq_dim,
        "max_seq_length": spec.max_seq_length,
        "num_layers": spec.num_layers,
        "dim_transformer": spec.dim_transformer,
        "nhead": spec.nhead,
        "dim_feedforward": spec.dim_feedforward,
        "diffusion_step": 1000,
        "timestep_type": TimestepEmbeddingType.adalayernorm,
    }

constraints

Aesthetic post-processing constraints for LACE layout boxes.

beautify_layout

beautify_layout(
    bbox: Float[Tensor, "batch elements 4"],
    mask: Bool[Tensor, "batch elements"],
    overlap_weight: float = 1.0,
    alignment_weight: float = 1.0,
    xy_only: bool = False,
    num_steps: int = 1000,
    lr: float = 0.0001,
) -> tuple[
    Float[torch.Tensor, "batch elements 4"],
    Bool[torch.Tensor, "batch elements"],
]

Optimize generated boxes with overlap and alignment penalties.

Parameters:

Name Type Description Default
bbox Float[Tensor, 'batch elements 4']

Normalized center xywh boxes with shape (batch, seq, 4).

required
mask Bool[Tensor, 'batch elements']

Valid-element mask with shape (batch, seq).

required
overlap_weight float

Weight for pairwise overlap penalties.

1.0
alignment_weight float

Weight for alignment penalties.

1.0
xy_only bool

Whether to keep width and height fixed.

False
num_steps int

Number of Adam optimization steps.

1000
lr float

Adam learning rate.

0.0001

Returns:

Type Description
tuple[Float[Tensor, 'batch elements 4'], Bool[Tensor, 'batch elements']]

Optimized boxes and updated mask.

Source code in models/lace/src/lace/constraints.py
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def beautify_layout(
    bbox: Float[torch.Tensor, "batch elements 4"],
    mask: Bool[torch.Tensor, "batch elements"],
    overlap_weight: float = 1.0,
    alignment_weight: float = 1.0,
    xy_only: bool = False,
    num_steps: int = 1000,
    lr: float = 1e-4,
) -> tuple[
    Float[torch.Tensor, "batch elements 4"],
    Bool[torch.Tensor, "batch elements"],
]:
    """Optimize generated boxes with overlap and alignment penalties.

    Args:
        bbox: Normalized center ``xywh`` boxes with shape ``(batch, seq, 4)``.
        mask: Valid-element mask with shape ``(batch, seq)``.
        overlap_weight: Weight for pairwise overlap penalties.
        alignment_weight: Weight for alignment penalties.
        xy_only: Whether to keep width and height fixed.
        num_steps: Number of Adam optimization steps.
        lr: Adam learning rate.

    Returns:
        Optimized boxes and updated mask.
    """
    if torch.sum(mask) == 1:
        return bbox, mask

    bbox_in = bbox
    if xy_only:
        wh = torch.abs(bbox[:, :, 2:].clone().detach())
        bbox_in = torch.cat([bbox[:, :, :2], wh], dim=2)

    bbox_in = bbox_in.clone()
    bbox_in[:, :, [0, 2]] *= 10 / 4
    bbox_in[:, :, [1, 3]] *= 10 / 6
    bbox_initial = bbox_in.clone().detach()
    bbox_param = nn.Parameter(bbox_in)
    optimizer = torch.optim.Adam([bbox_param], lr=lr)
    mse_loss = nn.MSELoss()
    mask_out = mask.clone()

    with torch.enable_grad():
        for _ in range(num_steps):
            bbox_relu = torch.relu(bbox_param)
            align_score = _layout_alignment_matrix(bbox_relu, mask_out)
            align_mask = (align_score < 1 / 64).clone().detach()
            align_loss = torch.mean(align_score * align_mask)
            piou = torch.mean(_pairwise_iou_xywh(bbox_relu, mask_out))
            mse = mse_loss(bbox_relu, bbox_initial)
            loss = mse + alignment_weight * align_loss + overlap_weight * piou
            optimizer.zero_grad()
            loss.backward()
            torch.nn.utils.clip_grad_norm_([bbox_param], 1.0)
            optimizer.step()
            min_wh = torch.min(bbox_relu[:, :, [2, 3]], dim=2).values
            mask_out = mask_out * (min_wh > 0.01)

    bbox_out = torch.relu(bbox_param.detach())
    bbox_out[:, :, [0, 2]] *= 4 / 10
    bbox_out[:, :, [1, 3]] *= 6 / 10
    return bbox_out, mask_out

conversion

Checkpoint conversion helpers for vendor LACE weights.

load_vendor_state_dict

load_vendor_state_dict(
    path: str | Path,
) -> dict[str, Shaped[torch.Tensor, "..."]]

Load a PyTorch checkpoint as a state dictionary.

Parameters:

Name Type Description Default
path str | Path

Path to a vendor checkpoint or state-dict file.

required

Returns:

Type Description
dict[str, Shaped[Tensor, '...']]

Mapping from parameter names to tensors.

Raises:

Type Description
TypeError

If the checkpoint does not contain a state-dict-like object.

Source code in models/lace/src/lace/conversion.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def load_vendor_state_dict(path: str | Path) -> dict[str, Shaped[torch.Tensor, "..."]]:
    """Load a PyTorch checkpoint as a state dictionary.

    Args:
        path: Path to a vendor checkpoint or state-dict file.

    Returns:
        Mapping from parameter names to tensors.

    Raises:
        TypeError: If the checkpoint does not contain a state-dict-like object.
    """
    loaded = torch.load(path, map_location="cpu")
    if isinstance(loaded, dict) and "state_dict" in loaded:
        loaded = loaded["state_dict"]
    if not isinstance(loaded, dict):
        raise TypeError(f"Expected a state_dict-like checkpoint at {path}")

    return dict(loaded)

convert_state_dict

convert_state_dict(
    state_dict: Mapping[str, Shaped[Tensor, "..."]],
) -> dict[str, Shaped[torch.Tensor, "..."]]

Convert vendor parameter names to the Diffusers module names.

Parameters:

Name Type Description Default
state_dict Mapping[str, Shaped[Tensor, '...']]

Vendor state dictionary.

required

Returns:

Type Description
dict[str, Shaped[Tensor, '...']]

Converted state dictionary with distributed prefixes and vendor

dict[str, Shaped[Tensor, '...']]

positional buffers removed.

Source code in models/lace/src/lace/conversion.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def convert_state_dict(
    state_dict: Mapping[str, Shaped[torch.Tensor, "..."]],
) -> dict[str, Shaped[torch.Tensor, "..."]]:
    """Convert vendor parameter names to the Diffusers module names.

    Args:
        state_dict: Vendor state dictionary.

    Returns:
        Converted state dictionary with distributed prefixes and vendor
        positional buffers removed.
    """
    return {
        key.removeprefix("module."): value
        for key, value in state_dict.items()
        if key != "pos_embed" and key.removeprefix("module.") != "pos_embed"
    }

build_pipeline_from_vendor_checkpoint

build_pipeline_from_vendor_checkpoint(
    dataset: DatasetName | str,
    checkpoint_path: str | Path,
    ddim_num_steps: int = 100,
) -> LacePipeline

Build a LACE pipeline from a vendor checkpoint.

Parameters:

Name Type Description Default
dataset DatasetName | str

LACE dataset name or alias.

required
checkpoint_path str | Path

Path to the vendor checkpoint.

required
ddim_num_steps int

Number of DDIM inference steps configured on the scheduler.

100

Returns:

Type Description
LacePipeline

Pipeline containing converted model, scheduler, and processor.

Raises:

Type Description
TypeError

If the checkpoint payload is not a state dictionary.

ValueError

If converted keys do not match the model architecture.

Source code in models/lace/src/lace/conversion.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def build_pipeline_from_vendor_checkpoint(
    dataset: DatasetName | str,
    checkpoint_path: str | Path,
    ddim_num_steps: int = 100,
) -> LacePipeline:
    """Build a LACE pipeline from a vendor checkpoint.

    Args:
        dataset: LACE dataset name or alias.
        checkpoint_path: Path to the vendor checkpoint.
        ddim_num_steps: Number of DDIM inference steps configured on the scheduler.

    Returns:
        Pipeline containing converted model, scheduler, and processor.

    Raises:
        TypeError: If the checkpoint payload is not a state dictionary.
        ValueError: If converted keys do not match the model architecture.
    """
    model = LaceTransformerModel(**default_model_config(dataset))
    converted = convert_state_dict(load_vendor_state_dict(checkpoint_path))
    expected = set(model.state_dict())
    actual = set(converted)
    missing = sorted(expected - actual)
    unexpected = sorted(actual - expected)
    if missing or unexpected:
        raise ValueError(
            f"State dict mismatch: missing={missing[:10]}, unexpected={unexpected[:10]}"
        )

    model.load_state_dict(converted, strict=True)
    processor = LaceProcessor.from_dataset(dataset)
    scheduler = LaceScheduler(ddim_num_steps=ddim_num_steps)
    return LacePipeline(model=model, scheduler=scheduler, processor=processor)

model_card

Model-card generation for converted LACE checkpoints.

lace_model_card

lace_model_card(
    dataset: DatasetName | str,
    *,
    parity_metrics: list[ParityMetric] | None = None,
) -> ModelCard

Create the Hugging Face model card for a LACE checkpoint.

Parameters:

Name Type Description Default
dataset DatasetName | str

LACE dataset name or alias.

required
parity_metrics list[ParityMetric] | None

Optional parity metrics to embed in the evaluation section.

None

Returns:

Type Description
ModelCard

Rendered Hugging Face Hub model card.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Source code in models/lace/src/lace/model_card.py
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
def lace_model_card(
    dataset: DatasetName | str,
    *,
    parity_metrics: list[ParityMetric] | None = None,
) -> ModelCard:
    """Create the Hugging Face model card for a LACE checkpoint.

    Args:
        dataset: LACE dataset name or alias.
        parity_metrics: Optional parity metrics to embed in the evaluation section.

    Returns:
        Rendered Hugging Face Hub model card.

    Raises:
        ValueError: If the dataset is unsupported.
    """
    dataset_key = normalize_dataset(dataset)
    dataset_name = str(dataset_key)
    model_id = _MODEL_IDS[dataset_key]
    metrics = parity_metrics
    if metrics is None:
        metrics = (
            [_PARITY_METRICS[dataset_key]] if dataset_key in _PARITY_METRICS else []
        )
    how_to_use = f"""
from lace import LacePipeline

pipe = LacePipeline.from_pretrained("{model_id}")
out = pipe(batch_size=1, seed=0, num_inference_steps=100)
print(out.bbox, out.labels, out.mask)
"""
    details = (
        "Diffusers-format conversion of the LACE checkpoint for "
        f"`{dataset_name}`. LACE is a continuous diffusion layout generation model "
        "from the paper 'Towards Aligned Layout Generation via Diffusion Model "
        "with Aesthetic Constraints' (https://arxiv.org/abs/2402.04754). "
        "The pipeline generates normalized center `xywh` layout boxes, "
        "category labels, and masks."
    )
    if dataset_key is DatasetName.rico13:
        details += (
            " The public checkpoint archive used for local parity "
            "verification does not include `rico13_best.pt`; Rico13 parity "
            "metrics are therefore not reported here."
        )
    return build_layout_model_card(
        model_id=model_id,
        model_name=f"LACE {dataset_name}",
        dataset_ids=[_DATASET_IDS[dataset_key]],
        license="mit",
        library_name="diffusers",
        pipeline_tag="other",
        tags=[
            "layout-generation",
            "lace",
            "diffusers",
            dataset_name,
        ],
        model_details=details,
        intended_uses=(
            "Use this checkpoint for research and evaluation of document and UI "
            "layout generation workflows."
        ),
        limitations=(
            "The converted checkpoint follows the original LACE release and is "
            "intended for layout synthesis, not for image rendering or OCR. "
            "Generated layouts may require downstream filtering for task-specific "
            "aesthetic or overlap constraints."
        ),
        how_to_use=how_to_use,
        training_data=(
            f"The original checkpoint was trained on `{_DATASET_IDS[dataset_key]}` "
            "as released by the original LACE project."
        ),
        parity_metrics=metrics,
        citation_bibtex=_LACE_BIBTEX,
        original_implementation_url="https://github.com/puar-playground/LACE",
    )

write_lace_model_card

write_lace_model_card(
    output_dir: str | Path,
    dataset: DatasetName | str,
    *,
    parity_metrics: list[ParityMetric] | None = None,
) -> Path

Write the LACE model card into a model directory.

Parameters:

Name Type Description Default
output_dir str | Path

Directory where README.md is written.

required
dataset DatasetName | str

LACE dataset name or alias.

required
parity_metrics list[ParityMetric] | None

Optional parity metrics to embed in the evaluation section.

None

Returns:

Type Description
Path

Path to the written README.md.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Source code in models/lace/src/lace/model_card.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def write_lace_model_card(
    output_dir: str | Path,
    dataset: DatasetName | str,
    *,
    parity_metrics: list[ParityMetric] | None = None,
) -> Path:
    """Write the LACE model card into a model directory.

    Args:
        output_dir: Directory where ``README.md`` is written.
        dataset: LACE dataset name or alias.
        parity_metrics: Optional parity metrics to embed in the evaluation section.

    Returns:
        Path to the written ``README.md``.

    Raises:
        ValueError: If the dataset is unsupported.
    """
    path = Path(output_dir) / "README.md"
    path.write_text(
        str(lace_model_card(dataset, parity_metrics=parity_metrics)),
        encoding="utf-8",
    )
    return path

modeling_lace

Transformer denoiser used by converted LACE checkpoints.

LaceModelOutput dataclass

Bases: BaseOutput

Output returned by the LACE transformer.

Attributes:

Name Type Description
sample Float[Tensor, 'batch elements channels']

Predicted noise tensor with the same shape as the input sample.

Source code in models/lace/src/lace/modeling_lace.py
46
47
48
49
50
51
52
53
54
@dataclass
class LaceModelOutput(BaseOutput):
    """Output returned by the LACE transformer.

    Attributes:
        sample: Predicted noise tensor with the same shape as the input sample.
    """

    sample: Float[torch.Tensor, "batch elements channels"]

LaceTransformerModel

Bases: ModelMixin, ConfigMixin

Transformer denoiser for continuous LACE layout tensors.

Parameters:

Name Type Description Default
seq_dim int

Number of channels per layout element.

required
max_seq_length int

Maximum number of layout elements.

25
num_layers int

Number of transformer blocks.

4
dim_transformer int

Hidden dimension.

512
nhead int

Number of attention heads.

16
dim_feedforward int

Feed-forward hidden dimension.

2048
diffusion_step int

Maximum diffusion timestep.

1000
timestep_type TimestepEmbeddingType | str | None

Timestep-conditioned normalization variant.

adalayernorm
Source code in models/lace/src/lace/modeling_lace.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
class LaceTransformerModel(ModelMixin, ConfigMixin):
    """Transformer denoiser for continuous LACE layout tensors.

    Args:
        seq_dim: Number of channels per layout element.
        max_seq_length: Maximum number of layout elements.
        num_layers: Number of transformer blocks.
        dim_transformer: Hidden dimension.
        nhead: Number of attention heads.
        dim_feedforward: Feed-forward hidden dimension.
        diffusion_step: Maximum diffusion timestep.
        timestep_type: Timestep-conditioned normalization variant.
    """

    config_name = "model_config.json"

    @register_to_config
    def __init__(
        self,
        *,
        seq_dim: int,
        max_seq_length: int = 25,
        num_layers: int = 4,
        dim_transformer: int = 512,
        nhead: int = 16,
        dim_feedforward: int = 2048,
        diffusion_step: int = 1000,
        timestep_type: TimestepEmbeddingType
        | str
        | None = TimestepEmbeddingType.adalayernorm,
    ) -> None:
        """Initialize a LACE transformer denoiser."""
        super().__init__()
        self.seq_dim = seq_dim
        self.max_seq_length = max_seq_length
        self.pos_encoder = SinusoidalPosEmb(max_seq_length, dim_transformer)
        self.layer_in = nn.Linear(seq_dim, dim_transformer)
        encoder_layer = Block(
            d_model=dim_transformer,
            nhead=nhead,
            dim_feedforward=dim_feedforward,
            diffusion_step=diffusion_step,
            timestep_type=timestep_type,
        )
        self.layers = clone_module_list(encoder_layer, num_layers)
        self.num_layers = num_layers
        self.layer_out = nn.Linear(dim_transformer, seq_dim)

    def forward(
        self,
        sample: Float[torch.Tensor, "batch elements channels"],
        timestep: Int[torch.Tensor, "batch"],
        attention_mask: Bool[torch.Tensor, "batch elements"] | None = None,
        return_dict: bool = True,
    ) -> LaceModelOutput | tuple[Float[torch.Tensor, "batch elements channels"]]:
        """Predict denoising residuals for a layout sample.

        Args:
            sample: Noisy layout tensor.
            timestep: Diffusion timestep per sample.
            attention_mask: Optional valid-element mask.
            return_dict: Whether to return ``LaceModelOutput``.

        Returns:
            Output dataclass or a one-item tuple containing the prediction.
        """
        output = F.softplus(self.layer_in(sample))
        pos_i = torch.arange(output.shape[1], device=output.device)
        output = output + self.pos_encoder(pos_i)
        key_padding_mask = None if attention_mask is None else ~attention_mask.bool()
        for i, layer in enumerate(self.layers):
            output = layer(
                output,
                src_key_padding_mask=key_padding_mask,
                timestep=timestep,
            )
            if i < self.num_layers - 1:
                output = F.softplus(output)
        output = self.layer_out(output)
        if not return_dict:
            return (output,)
        return LaceModelOutput(sample=output)

__init__

__init__(
    *,
    seq_dim: int,
    max_seq_length: int = 25,
    num_layers: int = 4,
    dim_transformer: int = 512,
    nhead: int = 16,
    dim_feedforward: int = 2048,
    diffusion_step: int = 1000,
    timestep_type: TimestepEmbeddingType
    | str
    | None = TimestepEmbeddingType.adalayernorm,
) -> None

Initialize a LACE transformer denoiser.

Source code in models/lace/src/lace/modeling_lace.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@register_to_config
def __init__(
    self,
    *,
    seq_dim: int,
    max_seq_length: int = 25,
    num_layers: int = 4,
    dim_transformer: int = 512,
    nhead: int = 16,
    dim_feedforward: int = 2048,
    diffusion_step: int = 1000,
    timestep_type: TimestepEmbeddingType
    | str
    | None = TimestepEmbeddingType.adalayernorm,
) -> None:
    """Initialize a LACE transformer denoiser."""
    super().__init__()
    self.seq_dim = seq_dim
    self.max_seq_length = max_seq_length
    self.pos_encoder = SinusoidalPosEmb(max_seq_length, dim_transformer)
    self.layer_in = nn.Linear(seq_dim, dim_transformer)
    encoder_layer = Block(
        d_model=dim_transformer,
        nhead=nhead,
        dim_feedforward=dim_feedforward,
        diffusion_step=diffusion_step,
        timestep_type=timestep_type,
    )
    self.layers = clone_module_list(encoder_layer, num_layers)
    self.num_layers = num_layers
    self.layer_out = nn.Linear(dim_transformer, seq_dim)

forward

forward(
    sample: Float[Tensor, "batch elements channels"],
    timestep: Int[Tensor, "batch"],
    attention_mask: Bool[Tensor, "batch elements"]
    | None = None,
    return_dict: bool = True,
) -> (
    LaceModelOutput
    | tuple[Float[torch.Tensor, "batch elements channels"]]
)

Predict denoising residuals for a layout sample.

Parameters:

Name Type Description Default
sample Float[Tensor, 'batch elements channels']

Noisy layout tensor.

required
timestep Int[Tensor, 'batch']

Diffusion timestep per sample.

required
attention_mask Bool[Tensor, 'batch elements'] | None

Optional valid-element mask.

None
return_dict bool

Whether to return LaceModelOutput.

True

Returns:

Type Description
LaceModelOutput | tuple[Float[Tensor, 'batch elements channels']]

Output dataclass or a one-item tuple containing the prediction.

Source code in models/lace/src/lace/modeling_lace.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def forward(
    self,
    sample: Float[torch.Tensor, "batch elements channels"],
    timestep: Int[torch.Tensor, "batch"],
    attention_mask: Bool[torch.Tensor, "batch elements"] | None = None,
    return_dict: bool = True,
) -> LaceModelOutput | tuple[Float[torch.Tensor, "batch elements channels"]]:
    """Predict denoising residuals for a layout sample.

    Args:
        sample: Noisy layout tensor.
        timestep: Diffusion timestep per sample.
        attention_mask: Optional valid-element mask.
        return_dict: Whether to return ``LaceModelOutput``.

    Returns:
        Output dataclass or a one-item tuple containing the prediction.
    """
    output = F.softplus(self.layer_in(sample))
    pos_i = torch.arange(output.shape[1], device=output.device)
    output = output + self.pos_encoder(pos_i)
    key_padding_mask = None if attention_mask is None else ~attention_mask.bool()
    for i, layer in enumerate(self.layers):
        output = layer(
            output,
            src_key_padding_mask=key_padding_mask,
            timestep=timestep,
        )
        if i < self.num_layers - 1:
            output = F.softplus(output)
    output = self.layer_out(output)
    if not return_dict:
        return (output,)
    return LaceModelOutput(sample=output)

pipeline_lace

Diffusers pipeline for LACE layout generation.

PipelineOutputType

Bases: StrEnum

Supported LACE pipeline output containers.

Source code in models/lace/src/lace/pipeline_lace.py
33
34
35
36
37
class PipelineOutputType(StrEnum):
    """Supported LACE pipeline output containers."""

    dataclass = auto()
    dict = auto()

LaceConditionAlias

Bases: StrEnum

LACE-specific public condition aliases not in the shared registry.

Source code in models/lace/src/lace/pipeline_lace.py
40
41
42
43
44
45
class LaceConditionAlias(StrEnum):
    """LACE-specific public condition aliases not in the shared registry."""

    none = auto()
    category = auto()
    label_size_plus = "label+size"

LacePipeline

Bases: DiffusionPipeline

Generate layouts with a converted LACE checkpoint.

Parameters:

Name Type Description Default
model LaceTransformerModel

LACE transformer denoiser.

required
scheduler LaceScheduler

DDIM-style scheduler.

required
processor LaceProcessor

Processor that encodes and decodes layout tensors.

required

Examples:

>>> from lace import LaceProcessor, LaceScheduler, LaceTransformerModel
>>> model = LaceTransformerModel(seq_dim=10, max_seq_length=2, num_layers=1, dim_transformer=8, nhead=2, dim_feedforward=16)
>>> pipe = LacePipeline(model=model, scheduler=LaceScheduler(ddim_num_steps=1), processor=LaceProcessor.from_dataset("publaynet"))
>>> pipe.processor.max_seq_length
25
Source code in models/lace/src/lace/pipeline_lace.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
class LacePipeline(DiffusionPipeline):
    """Generate layouts with a converted LACE checkpoint.

    Args:
        model: LACE transformer denoiser.
        scheduler: DDIM-style scheduler.
        processor: Processor that encodes and decodes layout tensors.

    Examples:
        >>> from lace import LaceProcessor, LaceScheduler, LaceTransformerModel
        >>> model = LaceTransformerModel(seq_dim=10, max_seq_length=2, num_layers=1, dim_transformer=8, nhead=2, dim_feedforward=16)
        >>> pipe = LacePipeline(model=model, scheduler=LaceScheduler(ddim_num_steps=1), processor=LaceProcessor.from_dataset("publaynet"))
        >>> pipe.processor.max_seq_length
        25
    """

    model_cpu_offload_seq = "model"
    _optional_components = ["processor"]

    def __init__(
        self,
        model: LaceTransformerModel,
        scheduler: LaceScheduler,
        processor: LaceProcessor,
    ) -> None:
        """Attach the converted LACE denoiser, scheduler, and processor."""
        super().__init__()
        self.register_modules(model=model, scheduler=scheduler)
        self.model = model
        self.scheduler = scheduler
        self.processor = processor
        self.model.eval()

    @property
    def components(
        self,
    ) -> dict[str, LaceTransformerModel | LaceScheduler | LaceProcessor]:
        """Expose modules and processor metadata for Diffusers serialization."""
        return dict(
            model=self.model, scheduler=self.scheduler, processor=self.processor
        )

    @torch.no_grad()
    def __call__(
        self,
        *,
        batch_size: int = 1,
        num_inference_steps: int | None = None,
        generator: torch.Generator | None = None,
        seed: int | None = None,
        condition_type: ConditionType | str | None = ConditionType.unconditional,
        bbox: Float[torch.Tensor, "batch elements 4"] | None = None,
        labels: Int[torch.Tensor, "batch elements"] | None = None,
        mask: Bool[torch.Tensor, "batch elements"] | None = None,
        box_format: BoxFormat | str = BoxFormat.xywh,
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
        completion_ratio: float = 0.2,
        refinement_noise: float = 0.1,
        beautify: bool = False,
        beautify_overlap_weight: float | None = None,
        beautify_alignment_weight: float = 1.0,
        output_type: PipelineOutputType | str = PipelineOutputType.dataclass,
        return_intermediates: bool = False,
    ) -> LayoutGenerationOutput | dict[str, Shaped[torch.Tensor, "..."]]:
        """Run LACE denoising and return generated layouts.

        Args:
            batch_size: Number of layouts to generate for unconditional calls.
            num_inference_steps: Number of DDIM steps. Uses scheduler default if
                omitted.
            generator: Optional torch generator. Takes precedence over ``seed``.
            seed: Convenience seed used only when ``generator`` is absent.
            condition_type: Conditioning mode or alias.
            bbox: Conditioning boxes for non-unconditional modes.
            labels: Conditioning labels for non-unconditional modes.
            mask: Optional conditioning mask.
            box_format: Input box format for conditioning boxes.
            normalized: Whether conditioning boxes are normalized.
            canvas_size: Pixel canvas size required when ``normalized`` is false.
            completion_ratio: Maximum random completion fraction.
            refinement_noise: Noise scale for refinement conditioning.
            beautify: Whether to run the aesthetic post-optimization.
            beautify_overlap_weight: Optional overlap penalty override.
            beautify_alignment_weight: Alignment penalty weight.
            output_type: ``"dataclass"`` or ``"dict"``.
            return_intermediates: Whether to return the denoising trajectory.

        Returns:
            Layout output dataclass or a dictionary.

        Raises:
            ValueError: If a condition/output mode is unsupported or required
                conditioning tensors are missing.
        """
        generator = resolve_torch_generator(generator=generator, seed=seed)
        canonical = normalize_condition_type(condition_type)
        encoded = None
        if canonical is not ConditionType.unconditional:
            if bbox is None or labels is None:
                raise ValueError(
                    f"bbox and labels are required for condition_type={condition_type}"
                )

            encoded = self.processor(
                bbox=bbox,
                labels=labels,
                mask=mask,
                box_format=box_format,
                normalized=normalized,
                canvas_size=canvas_size,
            )
            batch_size = encoded[LACE_LAYOUT_KEY].shape[0]
        self.scheduler.set_timesteps(num_inference_steps, device=self.device)
        sample = self.scheduler.initial_sample(
            batch_size,
            self.processor.max_seq_length,
            self.processor.seq_dim,
            device=self.device,
            generator=generator,
        )
        if canonical is ConditionType.refinement:
            assert encoded is not None
            noise = randn(
                encoded[LACE_BBOX_KEY].shape,
                dtype=encoded[LACE_BBOX_KEY].dtype,
                device=encoded[LACE_BBOX_KEY].device,
                generator=generator,
            )
            noisy_bbox = (encoded[LACE_BBOX_KEY] + refinement_noise * noise).clamp(0, 1)
            sample = self.processor.encode(
                noisy_bbox.to(self.device),
                encoded[LACE_LABELS_KEY].to(self.device),
                encoded[LACE_MASK_KEY].to(self.device),
            )
        real_layout = (
            None if encoded is None else encoded[LACE_LAYOUT_KEY].to(self.device)
        )
        fix_mask = self._build_fix_mask(
            canonical, real_layout, completion_ratio, generator
        )
        trajectory = [] if return_intermediates else None
        step_indices = (
            self.scheduler.refinement_indices()
            if canonical is ConditionType.refinement
            else list(range(len(self.scheduler.timesteps) - 1, -1, -1))
        )
        for index in step_indices:
            step = self.scheduler.ddim_timesteps[index]
            timestep = torch.full(
                (batch_size,), int(step.item()), device=self.device, dtype=torch.long
            )
            if real_layout is not None and fix_mask is not None:
                sample[fix_mask] = real_layout[fix_mask]
            model_output = self.model(sample=sample, timestep=timestep).sample
            out = self.scheduler.step(
                model_output, timestep, sample, index=index, generator=generator
            )
            sample = out.prev_sample
            if real_layout is not None and fix_mask is not None:
                sample[fix_mask] = real_layout[fix_mask]
            if trajectory is not None:
                trajectory.append(sample.detach().cpu())
        decoded = self.processor.decode(sample.detach().cpu())
        bbox_out = decoded.bbox
        mask_out = decoded.mask
        if beautify:
            overlap = beautify_overlap_weight
            if overlap is None:
                overlap = (
                    1.0
                    if normalize_dataset(self.processor.dataset)
                    is DatasetName.publaynet
                    else 0.0
                )
            bbox_out, mask_out = beautify_layout(
                bbox_out,
                mask_out,
                overlap_weight=overlap,
                alignment_weight=beautify_alignment_weight,
            )
        output = LayoutGenerationOutput(
            bbox=bbox_out,
            labels=decoded.labels,
            mask=mask_out,
            id2label=decoded.id2label,
            trajectory=trajectory,
            intermediates={"condition_type": str(canonical)}
            if return_intermediates
            else None,
        )
        out_type = normalize_output_type(output_type)
        if out_type is PipelineOutputType.dict:
            return dict(output)
        if out_type is PipelineOutputType.dataclass:
            return output
        assert_never(out_type)

    generate = __call__

    def save_pretrained(self, save_directory: str | Path) -> None:
        """Save pipeline components.

        Args:
            save_directory: Output directory.
        """
        super().save_pretrained(save_directory)
        self.processor.save_pretrained(save_directory)

    @classmethod
    def from_pretrained(
        cls,
        pretrained_model_name_or_path: str | Path,
        processor: LaceProcessor | None = None,
    ) -> "LacePipeline":
        """Load a saved LACE pipeline.

        Args:
            pretrained_model_name_or_path: Local path or Hub id.
            processor: Optional processor override.

        Returns:
            Loaded pipeline with the serialized processor attached.
        """
        loaded_processor = (
            LaceProcessor.from_pretrained(pretrained_model_name_or_path)
            if processor is None
            else processor
        )
        pipe = super().from_pretrained(pretrained_model_name_or_path)
        pipe.processor = loaded_processor
        return pipe

    def _build_fix_mask(
        self,
        condition_type: ConditionType,
        real_layout: Float[torch.Tensor, "batch elements channels"] | None,
        completion_ratio: float,
        generator: torch.Generator | None,
    ) -> Bool[torch.Tensor, "batch elements channels"] | None:
        """Build the fixed-channel mask for conditional generation."""
        if real_layout is None or condition_type is ConditionType.refinement:
            return None

        batch_size, seq_len, seq_dim = real_layout.shape
        num_class = seq_dim - 4

        if condition_type is ConditionType.label:
            fix_mask = torch.zeros_like(real_layout, dtype=torch.bool)
            fix_mask[:, :, :num_class] = True
            return fix_mask

        if condition_type is ConditionType.label_size:
            fix_mask = torch.zeros_like(real_layout, dtype=torch.bool)
            fix_indices = list(range(num_class)) + [num_class + 2, num_class + 3]
            fix_mask[:, :, fix_indices] = True
            return fix_mask

        if condition_type is ConditionType.completion:
            labels = real_layout[:, :, :num_class].argmax(dim=2)
            real_mask = labels != (num_class - 1)
            cutoff = rand((), device=real_layout.device, generator=generator).item()
            element_mask = (
                rand(
                    batch_size,
                    seq_len,
                    device=real_layout.device,
                    generator=generator,
                )
                <= cutoff * completion_ratio
            ) & real_mask
            return element_mask.unsqueeze(-1).expand(-1, -1, seq_dim)

        if condition_type is ConditionType.unconditional:
            return None
        raise ValueError(f"Unsupported LACE condition_type: {condition_type}")

components property

components: dict[
    str,
    LaceTransformerModel | LaceScheduler | LaceProcessor,
]

Expose modules and processor metadata for Diffusers serialization.

__init__

__init__(
    model: LaceTransformerModel,
    scheduler: LaceScheduler,
    processor: LaceProcessor,
) -> None

Attach the converted LACE denoiser, scheduler, and processor.

Source code in models/lace/src/lace/pipeline_lace.py
144
145
146
147
148
149
150
151
152
153
154
155
156
def __init__(
    self,
    model: LaceTransformerModel,
    scheduler: LaceScheduler,
    processor: LaceProcessor,
) -> None:
    """Attach the converted LACE denoiser, scheduler, and processor."""
    super().__init__()
    self.register_modules(model=model, scheduler=scheduler)
    self.model = model
    self.scheduler = scheduler
    self.processor = processor
    self.model.eval()

__call__

__call__(
    *,
    batch_size: int = 1,
    num_inference_steps: int | None = None,
    generator: Generator | None = None,
    seed: int | None = None,
    condition_type: ConditionType
    | str
    | None = ConditionType.unconditional,
    bbox: Float[Tensor, "batch elements 4"] | None = None,
    labels: Int[Tensor, "batch elements"] | None = None,
    mask: Bool[Tensor, "batch elements"] | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    completion_ratio: float = 0.2,
    refinement_noise: float = 0.1,
    beautify: bool = False,
    beautify_overlap_weight: float | None = None,
    beautify_alignment_weight: float = 1.0,
    output_type: PipelineOutputType
    | str = PipelineOutputType.dataclass,
    return_intermediates: bool = False,
) -> (
    LayoutGenerationOutput
    | dict[str, Shaped[torch.Tensor, "..."]]
)

Run LACE denoising and return generated layouts.

Parameters:

Name Type Description Default
batch_size int

Number of layouts to generate for unconditional calls.

1
num_inference_steps int | None

Number of DDIM steps. Uses scheduler default if omitted.

None
generator Generator | None

Optional torch generator. Takes precedence over seed.

None
seed int | None

Convenience seed used only when generator is absent.

None
condition_type ConditionType | str | None

Conditioning mode or alias.

unconditional
bbox Float[Tensor, 'batch elements 4'] | None

Conditioning boxes for non-unconditional modes.

None
labels Int[Tensor, 'batch elements'] | None

Conditioning labels for non-unconditional modes.

None
mask Bool[Tensor, 'batch elements'] | None

Optional conditioning mask.

None
box_format BoxFormat | str

Input box format for conditioning boxes.

xywh
normalized bool

Whether conditioning boxes are normalized.

True
canvas_size tuple[int, int] | None

Pixel canvas size required when normalized is false.

None
completion_ratio float

Maximum random completion fraction.

0.2
refinement_noise float

Noise scale for refinement conditioning.

0.1
beautify bool

Whether to run the aesthetic post-optimization.

False
beautify_overlap_weight float | None

Optional overlap penalty override.

None
beautify_alignment_weight float

Alignment penalty weight.

1.0
output_type PipelineOutputType | str

"dataclass" or "dict".

dataclass
return_intermediates bool

Whether to return the denoising trajectory.

False

Returns:

Type Description
LayoutGenerationOutput | dict[str, Shaped[Tensor, '...']]

Layout output dataclass or a dictionary.

Raises:

Type Description
ValueError

If a condition/output mode is unsupported or required conditioning tensors are missing.

Source code in models/lace/src/lace/pipeline_lace.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
@torch.no_grad()
def __call__(
    self,
    *,
    batch_size: int = 1,
    num_inference_steps: int | None = None,
    generator: torch.Generator | None = None,
    seed: int | None = None,
    condition_type: ConditionType | str | None = ConditionType.unconditional,
    bbox: Float[torch.Tensor, "batch elements 4"] | None = None,
    labels: Int[torch.Tensor, "batch elements"] | None = None,
    mask: Bool[torch.Tensor, "batch elements"] | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
    completion_ratio: float = 0.2,
    refinement_noise: float = 0.1,
    beautify: bool = False,
    beautify_overlap_weight: float | None = None,
    beautify_alignment_weight: float = 1.0,
    output_type: PipelineOutputType | str = PipelineOutputType.dataclass,
    return_intermediates: bool = False,
) -> LayoutGenerationOutput | dict[str, Shaped[torch.Tensor, "..."]]:
    """Run LACE denoising and return generated layouts.

    Args:
        batch_size: Number of layouts to generate for unconditional calls.
        num_inference_steps: Number of DDIM steps. Uses scheduler default if
            omitted.
        generator: Optional torch generator. Takes precedence over ``seed``.
        seed: Convenience seed used only when ``generator`` is absent.
        condition_type: Conditioning mode or alias.
        bbox: Conditioning boxes for non-unconditional modes.
        labels: Conditioning labels for non-unconditional modes.
        mask: Optional conditioning mask.
        box_format: Input box format for conditioning boxes.
        normalized: Whether conditioning boxes are normalized.
        canvas_size: Pixel canvas size required when ``normalized`` is false.
        completion_ratio: Maximum random completion fraction.
        refinement_noise: Noise scale for refinement conditioning.
        beautify: Whether to run the aesthetic post-optimization.
        beautify_overlap_weight: Optional overlap penalty override.
        beautify_alignment_weight: Alignment penalty weight.
        output_type: ``"dataclass"`` or ``"dict"``.
        return_intermediates: Whether to return the denoising trajectory.

    Returns:
        Layout output dataclass or a dictionary.

    Raises:
        ValueError: If a condition/output mode is unsupported or required
            conditioning tensors are missing.
    """
    generator = resolve_torch_generator(generator=generator, seed=seed)
    canonical = normalize_condition_type(condition_type)
    encoded = None
    if canonical is not ConditionType.unconditional:
        if bbox is None or labels is None:
            raise ValueError(
                f"bbox and labels are required for condition_type={condition_type}"
            )

        encoded = self.processor(
            bbox=bbox,
            labels=labels,
            mask=mask,
            box_format=box_format,
            normalized=normalized,
            canvas_size=canvas_size,
        )
        batch_size = encoded[LACE_LAYOUT_KEY].shape[0]
    self.scheduler.set_timesteps(num_inference_steps, device=self.device)
    sample = self.scheduler.initial_sample(
        batch_size,
        self.processor.max_seq_length,
        self.processor.seq_dim,
        device=self.device,
        generator=generator,
    )
    if canonical is ConditionType.refinement:
        assert encoded is not None
        noise = randn(
            encoded[LACE_BBOX_KEY].shape,
            dtype=encoded[LACE_BBOX_KEY].dtype,
            device=encoded[LACE_BBOX_KEY].device,
            generator=generator,
        )
        noisy_bbox = (encoded[LACE_BBOX_KEY] + refinement_noise * noise).clamp(0, 1)
        sample = self.processor.encode(
            noisy_bbox.to(self.device),
            encoded[LACE_LABELS_KEY].to(self.device),
            encoded[LACE_MASK_KEY].to(self.device),
        )
    real_layout = (
        None if encoded is None else encoded[LACE_LAYOUT_KEY].to(self.device)
    )
    fix_mask = self._build_fix_mask(
        canonical, real_layout, completion_ratio, generator
    )
    trajectory = [] if return_intermediates else None
    step_indices = (
        self.scheduler.refinement_indices()
        if canonical is ConditionType.refinement
        else list(range(len(self.scheduler.timesteps) - 1, -1, -1))
    )
    for index in step_indices:
        step = self.scheduler.ddim_timesteps[index]
        timestep = torch.full(
            (batch_size,), int(step.item()), device=self.device, dtype=torch.long
        )
        if real_layout is not None and fix_mask is not None:
            sample[fix_mask] = real_layout[fix_mask]
        model_output = self.model(sample=sample, timestep=timestep).sample
        out = self.scheduler.step(
            model_output, timestep, sample, index=index, generator=generator
        )
        sample = out.prev_sample
        if real_layout is not None and fix_mask is not None:
            sample[fix_mask] = real_layout[fix_mask]
        if trajectory is not None:
            trajectory.append(sample.detach().cpu())
    decoded = self.processor.decode(sample.detach().cpu())
    bbox_out = decoded.bbox
    mask_out = decoded.mask
    if beautify:
        overlap = beautify_overlap_weight
        if overlap is None:
            overlap = (
                1.0
                if normalize_dataset(self.processor.dataset)
                is DatasetName.publaynet
                else 0.0
            )
        bbox_out, mask_out = beautify_layout(
            bbox_out,
            mask_out,
            overlap_weight=overlap,
            alignment_weight=beautify_alignment_weight,
        )
    output = LayoutGenerationOutput(
        bbox=bbox_out,
        labels=decoded.labels,
        mask=mask_out,
        id2label=decoded.id2label,
        trajectory=trajectory,
        intermediates={"condition_type": str(canonical)}
        if return_intermediates
        else None,
    )
    out_type = normalize_output_type(output_type)
    if out_type is PipelineOutputType.dict:
        return dict(output)
    if out_type is PipelineOutputType.dataclass:
        return output
    assert_never(out_type)

save_pretrained

save_pretrained(save_directory: str | Path) -> None

Save pipeline components.

Parameters:

Name Type Description Default
save_directory str | Path

Output directory.

required
Source code in models/lace/src/lace/pipeline_lace.py
325
326
327
328
329
330
331
332
def save_pretrained(self, save_directory: str | Path) -> None:
    """Save pipeline components.

    Args:
        save_directory: Output directory.
    """
    super().save_pretrained(save_directory)
    self.processor.save_pretrained(save_directory)

from_pretrained classmethod

from_pretrained(
    pretrained_model_name_or_path: str | Path,
    processor: LaceProcessor | None = None,
) -> "LacePipeline"

Load a saved LACE pipeline.

Parameters:

Name Type Description Default
pretrained_model_name_or_path str | Path

Local path or Hub id.

required
processor LaceProcessor | None

Optional processor override.

None

Returns:

Type Description
'LacePipeline'

Loaded pipeline with the serialized processor attached.

Source code in models/lace/src/lace/pipeline_lace.py
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
@classmethod
def from_pretrained(
    cls,
    pretrained_model_name_or_path: str | Path,
    processor: LaceProcessor | None = None,
) -> "LacePipeline":
    """Load a saved LACE pipeline.

    Args:
        pretrained_model_name_or_path: Local path or Hub id.
        processor: Optional processor override.

    Returns:
        Loaded pipeline with the serialized processor attached.
    """
    loaded_processor = (
        LaceProcessor.from_pretrained(pretrained_model_name_or_path)
        if processor is None
        else processor
    )
    pipe = super().from_pretrained(pretrained_model_name_or_path)
    pipe.processor = loaded_processor
    return pipe

normalize_condition_type

normalize_condition_type(
    condition_type: ConditionType | str | None,
) -> ConditionType

Normalize public condition aliases.

Parameters:

Name Type Description Default
condition_type ConditionType | str | None

Canonical condition enum, string alias, or None.

required

Returns:

Type Description
ConditionType

Canonical condition enum.

Raises:

Type Description
ValueError

If the condition type is unsupported.

Examples:

>>> normalize_condition_type("cwh") is ConditionType.label_size
True
Source code in models/lace/src/lace/pipeline_lace.py
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def normalize_condition_type(
    condition_type: ConditionType | str | None,
) -> ConditionType:
    """Normalize public condition aliases.

    Args:
        condition_type: Canonical condition enum, string alias, or ``None``.

    Returns:
        Canonical condition enum.

    Raises:
        ValueError: If the condition type is unsupported.

    Examples:
        >>> normalize_condition_type("cwh") is ConditionType.label_size
        True
    """
    if isinstance(condition_type, ConditionType):
        canonical = condition_type
    elif condition_type is None:
        canonical = ConditionType.unconditional
    else:
        try:
            canonical = normalize_shared_condition_type(condition_type)
        except ValueError:
            key = condition_type.lower().replace("-", "_")
            try:
                canonical = _LACE_CONDITION_ALIASES[LaceConditionAlias(key)]
            except ValueError as exc:
                raise ValueError(
                    f"Unsupported LACE condition_type: {condition_type}"
                ) from exc

    if canonical not in _SUPPORTED_CONDITION_TYPES:
        raise ValueError(f"Unsupported LACE condition_type: {condition_type}")

    return canonical

normalize_output_type

normalize_output_type(
    output_type: PipelineOutputType | str,
) -> PipelineOutputType

Normalize public output type aliases.

Parameters:

Name Type Description Default
output_type PipelineOutputType | str

Output enum or string value.

required

Returns:

Type Description
PipelineOutputType

Canonical output type.

Raises:

Type Description
ValueError

If the output type is unsupported.

Source code in models/lace/src/lace/pipeline_lace.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def normalize_output_type(output_type: PipelineOutputType | str) -> PipelineOutputType:
    """Normalize public output type aliases.

    Args:
        output_type: Output enum or string value.

    Returns:
        Canonical output type.

    Raises:
        ValueError: If the output type is unsupported.
    """
    if isinstance(output_type, PipelineOutputType):
        return output_type
    try:
        return PipelineOutputType(output_type)
    except ValueError as exc:
        raise ValueError(f"Unsupported output_type: {output_type}") from exc

processing_lace

Processor for encoding and decoding LACE layout tensors.

LaceProcessor

Bases: ProcessorMixin

Encode public layout tensors into the continuous LACE sequence format.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset name serialized with the processor config.

required
labels list[str]

Ordered category labels without the padding label.

required
max_seq_length int

Maximum number of layout elements.

25

Examples:

>>> processor = LaceProcessor.from_dataset("publaynet")
>>> processor.seq_dim
10
Source code in models/lace/src/lace/processing_lace.py
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
class LaceProcessor(ProcessorMixin):
    """Encode public layout tensors into the continuous LACE sequence format.

    Args:
        dataset: Canonical dataset name serialized with the processor config.
        labels: Ordered category labels without the padding label.
        max_seq_length: Maximum number of layout elements.

    Examples:
        >>> processor = LaceProcessor.from_dataset("publaynet")
        >>> processor.seq_dim
        10
    """

    config_name = "processor_config.json"

    def __init__(
        self,
        dataset: DatasetName | str,
        labels: list[str],
        max_seq_length: int = 25,
    ) -> None:
        """Initialize processor metadata.

        Args:
            dataset: Canonical dataset name.
            labels: Ordered category labels without padding.
            max_seq_length: Maximum number of layout elements.
        """
        super().__init__()
        self.dataset = str(normalize_dataset(dataset))
        self.labels = tuple(labels)
        self.max_seq_length = max_seq_length

    @classmethod
    def from_dataset(cls, dataset: DatasetName | str) -> "LaceProcessor":
        """Create a processor from built-in dataset metadata.

        Args:
            dataset: LACE dataset name or alias.

        Returns:
            Processor configured for the dataset.

        Raises:
            ValueError: If the dataset is unsupported.

        Examples:
            >>> LaceProcessor.from_dataset("rico13").pad_label_id
            13
        """
        spec = get_dataset_spec(dataset)
        return cls(
            dataset=str(spec.dataset),
            labels=[str(label) for label in spec.labels],
            max_seq_length=spec.max_seq_length,
        )

    @property
    def id2label(self) -> dict[int, str]:
        """Return the category id to label mapping."""
        return dict(enumerate(self.labels))

    @property
    def pad_label_id(self) -> int:
        """Return the padding label id."""
        return len(self.labels)

    @property
    def num_classes_with_pad(self) -> int:
        """Return the label-channel count including padding."""
        return len(self.labels) + 1

    @property
    def seq_dim(self) -> int:
        """Return the latent per-element feature size."""
        return self.num_classes_with_pad + 4

    def __call__(
        self,
        *,
        bbox: Float[torch.Tensor, "batch elements 4"]
        | Float[np.ndarray, "batch elements 4"]
        | Sequence[ArrayLikeInput],
        labels: Int[torch.Tensor, "batch elements"]
        | Int[np.ndarray, "batch elements"]
        | Sequence[ArrayLikeInput],
        mask: Bool[torch.Tensor, "batch elements"]
        | Bool[np.ndarray, "batch elements"]
        | Sequence[ArrayLikeInput]
        | None = None,
        box_format: BoxFormat | str = BoxFormat.xywh,
        normalized: bool = True,
        canvas_size: tuple[int, int] | None = None,
    ) -> dict[str, Shaped[torch.Tensor, "..."]]:
        """Encode a public layout batch.

        Args:
            bbox: Boxes in ``box_format``.
            labels: Integer category labels.
            mask: Optional valid-element mask.
            box_format: Input box format.
            normalized: Whether input coordinates are already normalized.
            canvas_size: Pixel canvas size required when ``normalized`` is false.

        Returns:
            Dictionary containing encoded layout, normalized boxes, labels, and mask.

        Raises:
            ValueError: If pixel boxes are passed without ``canvas_size`` or if
                ``box_format`` is unsupported.

        Examples:
            >>> processor = LaceProcessor.from_dataset("publaynet")
            >>> out = processor(bbox=[[[0.5, 0.5, 0.2, 0.2]]], labels=[[0]])
            >>> tuple(out["layout"].shape)
            (1, 25, 10)
        """
        bbox_t, labels_t, mask_t = prepare_layout_tensors(
            bbox=bbox,
            labels=labels,
            mask=mask,
            box_format=box_format,
            normalized=normalized,
            canvas_size=canvas_size,
        )
        bbox_t, labels_t, mask_t = self.pad(bbox_t, labels_t, mask_t)
        return {
            LACE_LAYOUT_KEY: self.encode(bbox_t, labels_t, mask_t),
            LACE_BBOX_KEY: bbox_t,
            LACE_LABELS_KEY: labels_t,
            LACE_MASK_KEY: mask_t,
        }

    def pad(
        self,
        bbox: Float[torch.Tensor, "batch elements 4"],
        labels: Int[torch.Tensor, "batch elements"],
        mask: Bool[torch.Tensor, "batch elements"] | None = None,
        max_seq_length: int | None = None,
    ) -> tuple[
        Float[torch.Tensor, "batch padded_elements 4"],
        Int[torch.Tensor, "batch padded_elements"],
        Bool[torch.Tensor, "batch padded_elements"],
    ]:
        """Pad a batch to ``max_seq_length``.

        Args:
            bbox: Normalized boxes with shape ``(batch, seq, 4)``.
            labels: Integer labels with shape ``(batch, seq)``.
            mask: Optional valid-element mask.
            max_seq_length: Optional override for output length.

        Returns:
            Padded boxes, labels, and mask.

        Raises:
            ValueError: If the input has too many elements.
        """
        max_len = max_seq_length or self.max_seq_length
        if bbox.shape[1] > max_len:
            raise ValueError(f"LACE supports at most {max_len} elements")

        if mask is None:
            mask = torch.ones(labels.shape, dtype=torch.bool, device=labels.device)
        pad_count = max_len - bbox.shape[1]
        if pad_count:
            bbox_pad = torch.zeros(
                bbox.shape[0], pad_count, 4, dtype=bbox.dtype, device=bbox.device
            )
            label_pad = torch.full(
                (labels.shape[0], pad_count),
                self.pad_label_id,
                dtype=labels.dtype,
                device=labels.device,
            )
            mask_pad = torch.zeros(
                mask.shape[0], pad_count, dtype=torch.bool, device=mask.device
            )
            bbox = torch.cat((bbox, bbox_pad), dim=1)
            labels = torch.cat((labels, label_pad), dim=1)
            mask = torch.cat((mask, mask_pad), dim=1)
        labels = labels.clone()
        labels[~mask] = self.pad_label_id
        return bbox, labels, mask

    def encode(
        self,
        bbox: Float[torch.Tensor, "batch elements 4"],
        labels: Int[torch.Tensor, "batch elements"],
        mask: Bool[torch.Tensor, "batch elements"] | None = None,
    ) -> Float[torch.Tensor, "batch padded_elements channels"]:
        """Encode normalized boxes and labels into the LACE latent range.

        Args:
            bbox: Normalized center ``xywh`` boxes.
            labels: Integer labels.
            mask: Optional valid-element mask.

        Returns:
            Tensor with one-hot labels followed by box channels in ``[-1, 1]``.
        """
        bbox, labels, mask = self.pad(bbox, labels, mask)
        bbox_in = 2 * (bbox.clamp(0.0, 1.0) - 0.5)
        labels = labels.clamp(0, self.pad_label_id)
        labels[~mask] = self.pad_label_id
        one_hot = torch.nn.functional.one_hot(
            labels, num_classes=self.num_classes_with_pad
        ).to(dtype=bbox.dtype, device=bbox.device)
        return torch.cat((one_hot, bbox_in), dim=-1)

    def decode(
        self, layout: Float[torch.Tensor, "batch elements channels"], clamp: bool = True
    ) -> LayoutGenerationOutput:
        """Decode a LACE layout tensor into public output fields.

        Args:
            layout: Tensor with one-hot label channels and box channels.
            clamp: Whether to clamp latent box channels before conversion.

        Returns:
            Layout generation output with boxes in normalized center ``xywh``.
        """
        decoded = layout.clone()
        bbox_latent = decoded[:, :, self.num_classes_with_pad :]
        if clamp:
            bbox_latent = bbox_latent.clamp(-1.0, 1.0)
        bbox = bbox_latent / 2 + 0.5
        labels = decoded[:, :, : self.num_classes_with_pad].argmax(dim=2).long()
        mask = labels != self.pad_label_id
        return LayoutGenerationOutput(
            bbox=bbox,
            labels=labels,
            mask=mask,
            id2label=self.id2label,
            intermediates={"dataset": self.dataset},
        )

    def save_pretrained(  # ty: ignore[invalid-method-override]
        self, save_directory: str | Path
    ) -> None:
        """Save processor config to a Diffusers directory.

        Args:
            save_directory: Directory where ``processor_config.json`` is written.
        """
        super().save_pretrained(save_directory)

    @classmethod
    def from_pretrained(  # ty: ignore[invalid-method-override]
        cls,
        pretrained_model_name_or_path: str | Path,
        cache_dir: str | Path | None = None,
        force_download: bool = False,
        local_files_only: bool = False,
        token: str | bool | None = None,
        revision: str = "main",
    ) -> "LaceProcessor":
        """Load processor config from a Diffusers directory.

        Args:
            pretrained_model_name_or_path: Directory or Hub id containing
                ``processor_config.json``.
            cache_dir: Optional Hugging Face cache directory.
            force_download: Whether to force a fresh download.
            local_files_only: Whether to avoid network access.
            token: Optional Hugging Face token.
            revision: Hub revision to load.

        Returns:
            Loaded processor.
        """
        return super().from_pretrained(
            pretrained_model_name_or_path,
            cache_dir=cache_dir,
            force_download=force_download,
            local_files_only=local_files_only,
            token=token,
            revision=revision,
        )

id2label property

id2label: dict[int, str]

Return the category id to label mapping.

pad_label_id property

pad_label_id: int

Return the padding label id.

num_classes_with_pad property

num_classes_with_pad: int

Return the label-channel count including padding.

seq_dim property

seq_dim: int

Return the latent per-element feature size.

__init__

__init__(
    dataset: DatasetName | str,
    labels: list[str],
    max_seq_length: int = 25,
) -> None

Initialize processor metadata.

Parameters:

Name Type Description Default
dataset DatasetName | str

Canonical dataset name.

required
labels list[str]

Ordered category labels without padding.

required
max_seq_length int

Maximum number of layout elements.

25
Source code in models/lace/src/lace/processing_lace.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(
    self,
    dataset: DatasetName | str,
    labels: list[str],
    max_seq_length: int = 25,
) -> None:
    """Initialize processor metadata.

    Args:
        dataset: Canonical dataset name.
        labels: Ordered category labels without padding.
        max_seq_length: Maximum number of layout elements.
    """
    super().__init__()
    self.dataset = str(normalize_dataset(dataset))
    self.labels = tuple(labels)
    self.max_seq_length = max_seq_length

from_dataset classmethod

from_dataset(dataset: DatasetName | str) -> 'LaceProcessor'

Create a processor from built-in dataset metadata.

Parameters:

Name Type Description Default
dataset DatasetName | str

LACE dataset name or alias.

required

Returns:

Type Description
'LaceProcessor'

Processor configured for the dataset.

Raises:

Type Description
ValueError

If the dataset is unsupported.

Examples:

>>> LaceProcessor.from_dataset("rico13").pad_label_id
13
Source code in models/lace/src/lace/processing_lace.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@classmethod
def from_dataset(cls, dataset: DatasetName | str) -> "LaceProcessor":
    """Create a processor from built-in dataset metadata.

    Args:
        dataset: LACE dataset name or alias.

    Returns:
        Processor configured for the dataset.

    Raises:
        ValueError: If the dataset is unsupported.

    Examples:
        >>> LaceProcessor.from_dataset("rico13").pad_label_id
        13
    """
    spec = get_dataset_spec(dataset)
    return cls(
        dataset=str(spec.dataset),
        labels=[str(label) for label in spec.labels],
        max_seq_length=spec.max_seq_length,
    )

__call__

__call__(
    *,
    bbox: Float[Tensor, "batch elements 4"]
    | Float[ndarray, "batch elements 4"]
    | Sequence[ArrayLikeInput],
    labels: Int[Tensor, "batch elements"]
    | Int[ndarray, "batch elements"]
    | Sequence[ArrayLikeInput],
    mask: Bool[Tensor, "batch elements"]
    | Bool[ndarray, "batch elements"]
    | Sequence[ArrayLikeInput]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
) -> dict[str, Shaped[torch.Tensor, "..."]]

Encode a public layout batch.

Parameters:

Name Type Description Default
bbox Float[Tensor, 'batch elements 4'] | Float[ndarray, 'batch elements 4'] | Sequence[ArrayLikeInput]

Boxes in box_format.

required
labels Int[Tensor, 'batch elements'] | Int[ndarray, 'batch elements'] | Sequence[ArrayLikeInput]

Integer category labels.

required
mask Bool[Tensor, 'batch elements'] | Bool[ndarray, 'batch elements'] | Sequence[ArrayLikeInput] | None

Optional valid-element mask.

None
box_format BoxFormat | str

Input box format.

xywh
normalized bool

Whether input coordinates are already normalized.

True
canvas_size tuple[int, int] | None

Pixel canvas size required when normalized is false.

None

Returns:

Type Description
dict[str, Shaped[Tensor, '...']]

Dictionary containing encoded layout, normalized boxes, labels, and mask.

Raises:

Type Description
ValueError

If pixel boxes are passed without canvas_size or if box_format is unsupported.

Examples:

>>> processor = LaceProcessor.from_dataset("publaynet")
>>> out = processor(bbox=[[[0.5, 0.5, 0.2, 0.2]]], labels=[[0]])
>>> tuple(out["layout"].shape)
(1, 25, 10)
Source code in models/lace/src/lace/processing_lace.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def __call__(
    self,
    *,
    bbox: Float[torch.Tensor, "batch elements 4"]
    | Float[np.ndarray, "batch elements 4"]
    | Sequence[ArrayLikeInput],
    labels: Int[torch.Tensor, "batch elements"]
    | Int[np.ndarray, "batch elements"]
    | Sequence[ArrayLikeInput],
    mask: Bool[torch.Tensor, "batch elements"]
    | Bool[np.ndarray, "batch elements"]
    | Sequence[ArrayLikeInput]
    | None = None,
    box_format: BoxFormat | str = BoxFormat.xywh,
    normalized: bool = True,
    canvas_size: tuple[int, int] | None = None,
) -> dict[str, Shaped[torch.Tensor, "..."]]:
    """Encode a public layout batch.

    Args:
        bbox: Boxes in ``box_format``.
        labels: Integer category labels.
        mask: Optional valid-element mask.
        box_format: Input box format.
        normalized: Whether input coordinates are already normalized.
        canvas_size: Pixel canvas size required when ``normalized`` is false.

    Returns:
        Dictionary containing encoded layout, normalized boxes, labels, and mask.

    Raises:
        ValueError: If pixel boxes are passed without ``canvas_size`` or if
            ``box_format`` is unsupported.

    Examples:
        >>> processor = LaceProcessor.from_dataset("publaynet")
        >>> out = processor(bbox=[[[0.5, 0.5, 0.2, 0.2]]], labels=[[0]])
        >>> tuple(out["layout"].shape)
        (1, 25, 10)
    """
    bbox_t, labels_t, mask_t = prepare_layout_tensors(
        bbox=bbox,
        labels=labels,
        mask=mask,
        box_format=box_format,
        normalized=normalized,
        canvas_size=canvas_size,
    )
    bbox_t, labels_t, mask_t = self.pad(bbox_t, labels_t, mask_t)
    return {
        LACE_LAYOUT_KEY: self.encode(bbox_t, labels_t, mask_t),
        LACE_BBOX_KEY: bbox_t,
        LACE_LABELS_KEY: labels_t,
        LACE_MASK_KEY: mask_t,
    }

pad

pad(
    bbox: Float[Tensor, "batch elements 4"],
    labels: Int[Tensor, "batch elements"],
    mask: Bool[Tensor, "batch elements"] | None = None,
    max_seq_length: int | None = None,
) -> tuple[
    Float[torch.Tensor, "batch padded_elements 4"],
    Int[torch.Tensor, "batch padded_elements"],
    Bool[torch.Tensor, "batch padded_elements"],
]

Pad a batch to max_seq_length.

Parameters:

Name Type Description Default
bbox Float[Tensor, 'batch elements 4']

Normalized boxes with shape (batch, seq, 4).

required
labels Int[Tensor, 'batch elements']

Integer labels with shape (batch, seq).

required
mask Bool[Tensor, 'batch elements'] | None

Optional valid-element mask.

None
max_seq_length int | None

Optional override for output length.

None

Returns:

Type Description
tuple[Float[Tensor, 'batch padded_elements 4'], Int[Tensor, 'batch padded_elements'], Bool[Tensor, 'batch padded_elements']]

Padded boxes, labels, and mask.

Raises:

Type Description
ValueError

If the input has too many elements.

Source code in models/lace/src/lace/processing_lace.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def pad(
    self,
    bbox: Float[torch.Tensor, "batch elements 4"],
    labels: Int[torch.Tensor, "batch elements"],
    mask: Bool[torch.Tensor, "batch elements"] | None = None,
    max_seq_length: int | None = None,
) -> tuple[
    Float[torch.Tensor, "batch padded_elements 4"],
    Int[torch.Tensor, "batch padded_elements"],
    Bool[torch.Tensor, "batch padded_elements"],
]:
    """Pad a batch to ``max_seq_length``.

    Args:
        bbox: Normalized boxes with shape ``(batch, seq, 4)``.
        labels: Integer labels with shape ``(batch, seq)``.
        mask: Optional valid-element mask.
        max_seq_length: Optional override for output length.

    Returns:
        Padded boxes, labels, and mask.

    Raises:
        ValueError: If the input has too many elements.
    """
    max_len = max_seq_length or self.max_seq_length
    if bbox.shape[1] > max_len:
        raise ValueError(f"LACE supports at most {max_len} elements")

    if mask is None:
        mask = torch.ones(labels.shape, dtype=torch.bool, device=labels.device)
    pad_count = max_len - bbox.shape[1]
    if pad_count:
        bbox_pad = torch.zeros(
            bbox.shape[0], pad_count, 4, dtype=bbox.dtype, device=bbox.device
        )
        label_pad = torch.full(
            (labels.shape[0], pad_count),
            self.pad_label_id,
            dtype=labels.dtype,
            device=labels.device,
        )
        mask_pad = torch.zeros(
            mask.shape[0], pad_count, dtype=torch.bool, device=mask.device
        )
        bbox = torch.cat((bbox, bbox_pad), dim=1)
        labels = torch.cat((labels, label_pad), dim=1)
        mask = torch.cat((mask, mask_pad), dim=1)
    labels = labels.clone()
    labels[~mask] = self.pad_label_id
    return bbox, labels, mask

encode

encode(
    bbox: Float[Tensor, "batch elements 4"],
    labels: Int[Tensor, "batch elements"],
    mask: Bool[Tensor, "batch elements"] | None = None,
) -> Float[torch.Tensor, "batch padded_elements channels"]

Encode normalized boxes and labels into the LACE latent range.

Parameters:

Name Type Description Default
bbox Float[Tensor, 'batch elements 4']

Normalized center xywh boxes.

required
labels Int[Tensor, 'batch elements']

Integer labels.

required
mask Bool[Tensor, 'batch elements'] | None

Optional valid-element mask.

None

Returns:

Type Description
Float[Tensor, 'batch padded_elements channels']

Tensor with one-hot labels followed by box channels in [-1, 1].

Source code in models/lace/src/lace/processing_lace.py
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def encode(
    self,
    bbox: Float[torch.Tensor, "batch elements 4"],
    labels: Int[torch.Tensor, "batch elements"],
    mask: Bool[torch.Tensor, "batch elements"] | None = None,
) -> Float[torch.Tensor, "batch padded_elements channels"]:
    """Encode normalized boxes and labels into the LACE latent range.

    Args:
        bbox: Normalized center ``xywh`` boxes.
        labels: Integer labels.
        mask: Optional valid-element mask.

    Returns:
        Tensor with one-hot labels followed by box channels in ``[-1, 1]``.
    """
    bbox, labels, mask = self.pad(bbox, labels, mask)
    bbox_in = 2 * (bbox.clamp(0.0, 1.0) - 0.5)
    labels = labels.clamp(0, self.pad_label_id)
    labels[~mask] = self.pad_label_id
    one_hot = torch.nn.functional.one_hot(
        labels, num_classes=self.num_classes_with_pad
    ).to(dtype=bbox.dtype, device=bbox.device)
    return torch.cat((one_hot, bbox_in), dim=-1)

decode

decode(
    layout: Float[Tensor, "batch elements channels"],
    clamp: bool = True,
) -> LayoutGenerationOutput

Decode a LACE layout tensor into public output fields.

Parameters:

Name Type Description Default
layout Float[Tensor, 'batch elements channels']

Tensor with one-hot label channels and box channels.

required
clamp bool

Whether to clamp latent box channels before conversion.

True

Returns:

Type Description
LayoutGenerationOutput

Layout generation output with boxes in normalized center xywh.

Source code in models/lace/src/lace/processing_lace.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
def decode(
    self, layout: Float[torch.Tensor, "batch elements channels"], clamp: bool = True
) -> LayoutGenerationOutput:
    """Decode a LACE layout tensor into public output fields.

    Args:
        layout: Tensor with one-hot label channels and box channels.
        clamp: Whether to clamp latent box channels before conversion.

    Returns:
        Layout generation output with boxes in normalized center ``xywh``.
    """
    decoded = layout.clone()
    bbox_latent = decoded[:, :, self.num_classes_with_pad :]
    if clamp:
        bbox_latent = bbox_latent.clamp(-1.0, 1.0)
    bbox = bbox_latent / 2 + 0.5
    labels = decoded[:, :, : self.num_classes_with_pad].argmax(dim=2).long()
    mask = labels != self.pad_label_id
    return LayoutGenerationOutput(
        bbox=bbox,
        labels=labels,
        mask=mask,
        id2label=self.id2label,
        intermediates={"dataset": self.dataset},
    )

save_pretrained

save_pretrained(save_directory: str | Path) -> None

Save processor config to a Diffusers directory.

Parameters:

Name Type Description Default
save_directory str | Path

Directory where processor_config.json is written.

required
Source code in models/lace/src/lace/processing_lace.py
268
269
270
271
272
273
274
275
276
def save_pretrained(  # ty: ignore[invalid-method-override]
    self, save_directory: str | Path
) -> None:
    """Save processor config to a Diffusers directory.

    Args:
        save_directory: Directory where ``processor_config.json`` is written.
    """
    super().save_pretrained(save_directory)

from_pretrained classmethod

from_pretrained(
    pretrained_model_name_or_path: str | Path,
    cache_dir: str | Path | None = None,
    force_download: bool = False,
    local_files_only: bool = False,
    token: str | bool | None = None,
    revision: str = "main",
) -> "LaceProcessor"

Load processor config from a Diffusers directory.

Parameters:

Name Type Description Default
pretrained_model_name_or_path str | Path

Directory or Hub id containing processor_config.json.

required
cache_dir str | Path | None

Optional Hugging Face cache directory.

None
force_download bool

Whether to force a fresh download.

False
local_files_only bool

Whether to avoid network access.

False
token str | bool | None

Optional Hugging Face token.

None
revision str

Hub revision to load.

'main'

Returns:

Type Description
'LaceProcessor'

Loaded processor.

Source code in models/lace/src/lace/processing_lace.py
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
@classmethod
def from_pretrained(  # ty: ignore[invalid-method-override]
    cls,
    pretrained_model_name_or_path: str | Path,
    cache_dir: str | Path | None = None,
    force_download: bool = False,
    local_files_only: bool = False,
    token: str | bool | None = None,
    revision: str = "main",
) -> "LaceProcessor":
    """Load processor config from a Diffusers directory.

    Args:
        pretrained_model_name_or_path: Directory or Hub id containing
            ``processor_config.json``.
        cache_dir: Optional Hugging Face cache directory.
        force_download: Whether to force a fresh download.
        local_files_only: Whether to avoid network access.
        token: Optional Hugging Face token.
        revision: Hub revision to load.

    Returns:
        Loaded processor.
    """
    return super().from_pretrained(
        pretrained_model_name_or_path,
        cache_dir=cache_dir,
        force_download=force_download,
        local_files_only=local_files_only,
        token=token,
        revision=revision,
    )

scheduling_lace

DDIM-style scheduler utilities for LACE layout diffusion.

LaceSchedulerOutput dataclass

Bases: BaseOutput

Output returned by one reverse diffusion step.

Attributes:

Name Type Description
prev_sample Float[Tensor, 'batch elements channels']

Sample for the next denoising iteration.

pred_original_sample Float[Tensor, 'batch elements channels']

Scheduler estimate of the clean layout tensor.

Source code in models/lace/src/lace/scheduling_lace.py
61
62
63
64
65
66
67
68
69
70
71
@dataclass
class LaceSchedulerOutput(BaseOutput):
    """Output returned by one reverse diffusion step.

    Attributes:
        prev_sample: Sample for the next denoising iteration.
        pred_original_sample: Scheduler estimate of the clean layout tensor.
    """

    prev_sample: Float[torch.Tensor, "batch elements channels"]
    pred_original_sample: Float[torch.Tensor, "batch elements channels"]

LaceScheduler

Bases: SchedulerMixin, ConfigMixin

Scheduler for the converted LACE diffusion process.

Parameters:

Name Type Description Default
num_train_timesteps int

Number of timesteps used during training.

1000
beta_schedule BetaSchedule | str

Beta schedule enum or string value.

cosine
ddim_num_steps int

Default number of inference steps.

100
ddim_discretize DDIMDiscretization | str

DDIM discretization enum or string value.

uniform
eta float

Stochasticity parameter used by DDIM.

0.0
Source code in models/lace/src/lace/scheduling_lace.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
class LaceScheduler(SchedulerMixin, ConfigMixin):
    """Scheduler for the converted LACE diffusion process.

    Args:
        num_train_timesteps: Number of timesteps used during training.
        beta_schedule: Beta schedule enum or string value.
        ddim_num_steps: Default number of inference steps.
        ddim_discretize: DDIM discretization enum or string value.
        eta: Stochasticity parameter used by DDIM.
    """

    config_name = "scheduler_config.json"
    order = 1

    @register_to_config
    def __init__(
        self,
        *,
        num_train_timesteps: int = 1000,
        beta_schedule: BetaSchedule | str = BetaSchedule.cosine,
        ddim_num_steps: int = 100,
        ddim_discretize: DDIMDiscretization | str = DDIMDiscretization.uniform,
        eta: float = 0.0,
    ) -> None:
        """Initialize scheduler state and default timesteps."""
        self.num_train_timesteps = num_train_timesteps
        canonical_beta = normalize_beta_schedule(beta_schedule)
        canonical_ddim = normalize_ddim_discretization(ddim_discretize)
        self.beta_schedule = str(canonical_beta)
        self.ddim_num_steps = ddim_num_steps
        self.ddim_discretize = str(canonical_ddim)
        self.eta = eta
        betas = (
            _reference_cosine_betas(num_train_timesteps)
            if canonical_beta in (BetaSchedule.cosine, BetaSchedule.cosine_reverse)
            else make_beta_schedule(
                canonical_beta,
                num_timesteps=num_train_timesteps,
                start=0.0001,
                end=0.02,
            )
        ).float()
        alphas = 1.0 - betas
        self.alphas_cumprod = alphas.cumprod(dim=0)
        self.timesteps = torch.empty(0, dtype=torch.long)
        self.set_timesteps(ddim_num_steps)

    def set_timesteps(
        self, num_inference_steps: int | None = None, device: torch.device | None = None
    ) -> None:
        """Set the inference timesteps.

        Args:
            num_inference_steps: Number of denoising steps. Uses the configured
                default when omitted.
            device: Optional device for scheduler tensors.
        """
        steps = num_inference_steps or self.ddim_num_steps
        ddim = make_ddim_timesteps(
            self.ddim_discretize, steps, self.num_train_timesteps
        )
        self.ddim_timesteps = torch.as_tensor(ddim, dtype=torch.long, device=device)
        self.timesteps = torch.flip(self.ddim_timesteps, dims=(0,))
        if device is not None and self.beta_schedule in {
            str(BetaSchedule.cosine),
            str(BetaSchedule.cosine_reverse),
        }:
            alphas_cumprod = _alphas_cumprod_on_device(
                self.num_train_timesteps, torch.device(device)
            )
        else:
            alphas_cumprod = self.alphas_cumprod.to(device)

        self.ddim_alphas = alphas_cumprod[self.ddim_timesteps]
        self.ddim_alphas_prev = torch.as_tensor(
            [alphas_cumprod[0].item()]
            + alphas_cumprod[self.ddim_timesteps[:-1]].tolist(),
            dtype=torch.float32,
            device=device,
        )
        self.ddim_sigmas = self.eta * torch.sqrt(
            (1 - self.ddim_alphas_prev)
            / (1 - self.ddim_alphas)
            * (1 - self.ddim_alphas / self.ddim_alphas_prev)
        )
        self.sqrt_one_minus_alphas = torch.sqrt(1.0 - self.ddim_alphas)

    def alphas_cumprod_for_device(
        self, device: torch.device | str
    ) -> Float[torch.Tensor, "timesteps"]:
        """Return training cumulative alphas computed on ``device``."""
        target_device = torch.device(device)
        if self.beta_schedule in {
            str(BetaSchedule.cosine),
            str(BetaSchedule.cosine_reverse),
        }:
            return _alphas_cumprod_on_device(self.num_train_timesteps, target_device)

        return self.alphas_cumprod.to(target_device)

    def add_noise(
        self,
        original_samples: Float[torch.Tensor, "batch elements channels"],
        noise: Float[torch.Tensor, "batch elements channels"],
        timesteps: Int[torch.Tensor, "batch"],
    ) -> Float[torch.Tensor, "batch elements channels"]:
        """Add forward-process noise to clean samples.

        Args:
            original_samples: Clean layout tensor.
            noise: Noise tensor with the same shape.
            timesteps: Per-sample timestep ids.

        Returns:
            Noisy samples at the requested timesteps.
        """
        alphas = self.alphas_cumprod.to(original_samples.device)
        sqrt_alpha = torch.gather(alphas.sqrt(), 0, timesteps).reshape(-1, 1, 1)
        sqrt_one_minus = torch.gather(torch.sqrt(1.0 - alphas), 0, timesteps).reshape(
            -1, 1, 1
        )
        return sqrt_alpha * original_samples + sqrt_one_minus * noise

    def initial_sample(
        self,
        batch_size: int,
        seq_len: int,
        seq_dim: int,
        *,
        device: torch.device,
        generator: torch.Generator | None = None,
        stochastic: bool = True,
    ) -> Float[torch.Tensor, "batch elements channels"]:
        """Create the initial denoising sample.

        Args:
            batch_size: Number of layouts.
            seq_len: Number of elements per layout.
            seq_dim: Number of channels per element.
            device: Device for the output tensor.
            generator: Optional torch generator.
            stochastic: Whether to sample noise or return zeros.

        Returns:
            Initial sample tensor.
        """
        if not stochastic:
            return torch.zeros(batch_size, seq_len, seq_dim, device=device)

        return randn(batch_size, seq_len, seq_dim, device=device, generator=generator)

    def step(
        self,
        model_output: Float[torch.Tensor, "batch elements channels"],
        timestep: Int[torch.Tensor, "batch"],
        sample: Float[torch.Tensor, "batch elements channels"],
        index: int,
        generator: torch.Generator | None = None,
    ) -> LaceSchedulerOutput:
        """Take one reverse diffusion step.

        Args:
            model_output: Predicted noise from the denoiser.
            timestep: Public timestep tensor, kept for scheduler compatibility.
            sample: Current sample.
            index: Index into the scheduler timestep buffers.
            generator: Optional torch generator for stochastic DDIM noise.

        Returns:
            Previous sample and predicted clean sample.
        """
        del timestep
        alpha_t = torch.full(
            model_output.shape,
            float(self.ddim_alphas[index].item()),
            device=sample.device,
            dtype=sample.dtype,
        )
        alpha_prev = torch.full(
            model_output.shape,
            float(self.ddim_alphas_prev[index].item()),
            device=sample.device,
            dtype=sample.dtype,
        )
        sigma_t = torch.full(
            model_output.shape,
            float(self.ddim_sigmas[index].item()),
            device=sample.device,
            dtype=sample.dtype,
        )
        sqrt_one_minus = torch.full(
            model_output.shape,
            float(self.sqrt_one_minus_alphas[index].item()),
            device=sample.device,
            dtype=sample.dtype,
        )
        direction = (1.0 - alpha_prev - sigma_t**2).sqrt() * model_output
        noise = sigma_t * randn(
            sample.shape,
            dtype=sample.dtype,
            device=sample.device,
            generator=generator,
        )
        pred_original = (sample - sqrt_one_minus * model_output) / alpha_t.sqrt()
        prev_sample = alpha_prev.sqrt() * pred_original + direction + noise
        return LaceSchedulerOutput(
            prev_sample=prev_sample, pred_original_sample=pred_original
        )

    def refinement_indices(self, max_timestep: int = 201) -> list[int]:
        """Return scheduler indices used by LACE refinement sampling.

        Args:
            max_timestep: Maximum one-indexed DDIM timestep included.

        Returns:
            Descending list of scheduler buffer indices.
        """
        total = int(torch.sum(self.ddim_timesteps <= max_timestep).item())
        return list(range(total - 1, -1, -1))

__init__

__init__(
    *,
    num_train_timesteps: int = 1000,
    beta_schedule: BetaSchedule | str = BetaSchedule.cosine,
    ddim_num_steps: int = 100,
    ddim_discretize: DDIMDiscretization
    | str = DDIMDiscretization.uniform,
    eta: float = 0.0,
) -> None

Initialize scheduler state and default timesteps.

Source code in models/lace/src/lace/scheduling_lace.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@register_to_config
def __init__(
    self,
    *,
    num_train_timesteps: int = 1000,
    beta_schedule: BetaSchedule | str = BetaSchedule.cosine,
    ddim_num_steps: int = 100,
    ddim_discretize: DDIMDiscretization | str = DDIMDiscretization.uniform,
    eta: float = 0.0,
) -> None:
    """Initialize scheduler state and default timesteps."""
    self.num_train_timesteps = num_train_timesteps
    canonical_beta = normalize_beta_schedule(beta_schedule)
    canonical_ddim = normalize_ddim_discretization(ddim_discretize)
    self.beta_schedule = str(canonical_beta)
    self.ddim_num_steps = ddim_num_steps
    self.ddim_discretize = str(canonical_ddim)
    self.eta = eta
    betas = (
        _reference_cosine_betas(num_train_timesteps)
        if canonical_beta in (BetaSchedule.cosine, BetaSchedule.cosine_reverse)
        else make_beta_schedule(
            canonical_beta,
            num_timesteps=num_train_timesteps,
            start=0.0001,
            end=0.02,
        )
    ).float()
    alphas = 1.0 - betas
    self.alphas_cumprod = alphas.cumprod(dim=0)
    self.timesteps = torch.empty(0, dtype=torch.long)
    self.set_timesteps(ddim_num_steps)

set_timesteps

set_timesteps(
    num_inference_steps: int | None = None,
    device: device | None = None,
) -> None

Set the inference timesteps.

Parameters:

Name Type Description Default
num_inference_steps int | None

Number of denoising steps. Uses the configured default when omitted.

None
device device | None

Optional device for scheduler tensors.

None
Source code in models/lace/src/lace/scheduling_lace.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def set_timesteps(
    self, num_inference_steps: int | None = None, device: torch.device | None = None
) -> None:
    """Set the inference timesteps.

    Args:
        num_inference_steps: Number of denoising steps. Uses the configured
            default when omitted.
        device: Optional device for scheduler tensors.
    """
    steps = num_inference_steps or self.ddim_num_steps
    ddim = make_ddim_timesteps(
        self.ddim_discretize, steps, self.num_train_timesteps
    )
    self.ddim_timesteps = torch.as_tensor(ddim, dtype=torch.long, device=device)
    self.timesteps = torch.flip(self.ddim_timesteps, dims=(0,))
    if device is not None and self.beta_schedule in {
        str(BetaSchedule.cosine),
        str(BetaSchedule.cosine_reverse),
    }:
        alphas_cumprod = _alphas_cumprod_on_device(
            self.num_train_timesteps, torch.device(device)
        )
    else:
        alphas_cumprod = self.alphas_cumprod.to(device)

    self.ddim_alphas = alphas_cumprod[self.ddim_timesteps]
    self.ddim_alphas_prev = torch.as_tensor(
        [alphas_cumprod[0].item()]
        + alphas_cumprod[self.ddim_timesteps[:-1]].tolist(),
        dtype=torch.float32,
        device=device,
    )
    self.ddim_sigmas = self.eta * torch.sqrt(
        (1 - self.ddim_alphas_prev)
        / (1 - self.ddim_alphas)
        * (1 - self.ddim_alphas / self.ddim_alphas_prev)
    )
    self.sqrt_one_minus_alphas = torch.sqrt(1.0 - self.ddim_alphas)

alphas_cumprod_for_device

alphas_cumprod_for_device(
    device: device | str,
) -> Float[torch.Tensor, "timesteps"]

Return training cumulative alphas computed on device.

Source code in models/lace/src/lace/scheduling_lace.py
161
162
163
164
165
166
167
168
169
170
171
172
def alphas_cumprod_for_device(
    self, device: torch.device | str
) -> Float[torch.Tensor, "timesteps"]:
    """Return training cumulative alphas computed on ``device``."""
    target_device = torch.device(device)
    if self.beta_schedule in {
        str(BetaSchedule.cosine),
        str(BetaSchedule.cosine_reverse),
    }:
        return _alphas_cumprod_on_device(self.num_train_timesteps, target_device)

    return self.alphas_cumprod.to(target_device)

add_noise

add_noise(
    original_samples: Float[
        Tensor, "batch elements channels"
    ],
    noise: Float[Tensor, "batch elements channels"],
    timesteps: Int[Tensor, "batch"],
) -> Float[torch.Tensor, "batch elements channels"]

Add forward-process noise to clean samples.

Parameters:

Name Type Description Default
original_samples Float[Tensor, 'batch elements channels']

Clean layout tensor.

required
noise Float[Tensor, 'batch elements channels']

Noise tensor with the same shape.

required
timesteps Int[Tensor, 'batch']

Per-sample timestep ids.

required

Returns:

Type Description
Float[Tensor, 'batch elements channels']

Noisy samples at the requested timesteps.

Source code in models/lace/src/lace/scheduling_lace.py
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
def add_noise(
    self,
    original_samples: Float[torch.Tensor, "batch elements channels"],
    noise: Float[torch.Tensor, "batch elements channels"],
    timesteps: Int[torch.Tensor, "batch"],
) -> Float[torch.Tensor, "batch elements channels"]:
    """Add forward-process noise to clean samples.

    Args:
        original_samples: Clean layout tensor.
        noise: Noise tensor with the same shape.
        timesteps: Per-sample timestep ids.

    Returns:
        Noisy samples at the requested timesteps.
    """
    alphas = self.alphas_cumprod.to(original_samples.device)
    sqrt_alpha = torch.gather(alphas.sqrt(), 0, timesteps).reshape(-1, 1, 1)
    sqrt_one_minus = torch.gather(torch.sqrt(1.0 - alphas), 0, timesteps).reshape(
        -1, 1, 1
    )
    return sqrt_alpha * original_samples + sqrt_one_minus * noise

initial_sample

initial_sample(
    batch_size: int,
    seq_len: int,
    seq_dim: int,
    *,
    device: device,
    generator: Generator | None = None,
    stochastic: bool = True,
) -> Float[torch.Tensor, "batch elements channels"]

Create the initial denoising sample.

Parameters:

Name Type Description Default
batch_size int

Number of layouts.

required
seq_len int

Number of elements per layout.

required
seq_dim int

Number of channels per element.

required
device device

Device for the output tensor.

required
generator Generator | None

Optional torch generator.

None
stochastic bool

Whether to sample noise or return zeros.

True

Returns:

Type Description
Float[Tensor, 'batch elements channels']

Initial sample tensor.

Source code in models/lace/src/lace/scheduling_lace.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
def initial_sample(
    self,
    batch_size: int,
    seq_len: int,
    seq_dim: int,
    *,
    device: torch.device,
    generator: torch.Generator | None = None,
    stochastic: bool = True,
) -> Float[torch.Tensor, "batch elements channels"]:
    """Create the initial denoising sample.

    Args:
        batch_size: Number of layouts.
        seq_len: Number of elements per layout.
        seq_dim: Number of channels per element.
        device: Device for the output tensor.
        generator: Optional torch generator.
        stochastic: Whether to sample noise or return zeros.

    Returns:
        Initial sample tensor.
    """
    if not stochastic:
        return torch.zeros(batch_size, seq_len, seq_dim, device=device)

    return randn(batch_size, seq_len, seq_dim, device=device, generator=generator)

step

step(
    model_output: Float[Tensor, "batch elements channels"],
    timestep: Int[Tensor, "batch"],
    sample: Float[Tensor, "batch elements channels"],
    index: int,
    generator: Generator | None = None,
) -> LaceSchedulerOutput

Take one reverse diffusion step.

Parameters:

Name Type Description Default
model_output Float[Tensor, 'batch elements channels']

Predicted noise from the denoiser.

required
timestep Int[Tensor, 'batch']

Public timestep tensor, kept for scheduler compatibility.

required
sample Float[Tensor, 'batch elements channels']

Current sample.

required
index int

Index into the scheduler timestep buffers.

required
generator Generator | None

Optional torch generator for stochastic DDIM noise.

None

Returns:

Type Description
LaceSchedulerOutput

Previous sample and predicted clean sample.

Source code in models/lace/src/lace/scheduling_lace.py
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
def step(
    self,
    model_output: Float[torch.Tensor, "batch elements channels"],
    timestep: Int[torch.Tensor, "batch"],
    sample: Float[torch.Tensor, "batch elements channels"],
    index: int,
    generator: torch.Generator | None = None,
) -> LaceSchedulerOutput:
    """Take one reverse diffusion step.

    Args:
        model_output: Predicted noise from the denoiser.
        timestep: Public timestep tensor, kept for scheduler compatibility.
        sample: Current sample.
        index: Index into the scheduler timestep buffers.
        generator: Optional torch generator for stochastic DDIM noise.

    Returns:
        Previous sample and predicted clean sample.
    """
    del timestep
    alpha_t = torch.full(
        model_output.shape,
        float(self.ddim_alphas[index].item()),
        device=sample.device,
        dtype=sample.dtype,
    )
    alpha_prev = torch.full(
        model_output.shape,
        float(self.ddim_alphas_prev[index].item()),
        device=sample.device,
        dtype=sample.dtype,
    )
    sigma_t = torch.full(
        model_output.shape,
        float(self.ddim_sigmas[index].item()),
        device=sample.device,
        dtype=sample.dtype,
    )
    sqrt_one_minus = torch.full(
        model_output.shape,
        float(self.sqrt_one_minus_alphas[index].item()),
        device=sample.device,
        dtype=sample.dtype,
    )
    direction = (1.0 - alpha_prev - sigma_t**2).sqrt() * model_output
    noise = sigma_t * randn(
        sample.shape,
        dtype=sample.dtype,
        device=sample.device,
        generator=generator,
    )
    pred_original = (sample - sqrt_one_minus * model_output) / alpha_t.sqrt()
    prev_sample = alpha_prev.sqrt() * pred_original + direction + noise
    return LaceSchedulerOutput(
        prev_sample=prev_sample, pred_original_sample=pred_original
    )

refinement_indices

refinement_indices(max_timestep: int = 201) -> list[int]

Return scheduler indices used by LACE refinement sampling.

Parameters:

Name Type Description Default
max_timestep int

Maximum one-indexed DDIM timestep included.

201

Returns:

Type Description
list[int]

Descending list of scheduler buffer indices.

Source code in models/lace/src/lace/scheduling_lace.py
283
284
285
286
287
288
289
290
291
292
293
def refinement_indices(self, max_timestep: int = 201) -> list[int]:
    """Return scheduler indices used by LACE refinement sampling.

    Args:
        max_timestep: Maximum one-indexed DDIM timestep included.

    Returns:
        Descending list of scheduler buffer indices.
    """
    total = int(torch.sum(self.ddim_timesteps <= max_timestep).item())
    return list(range(total - 1, -1, -1))

training

Package-local LACE training entry points.

config

Configuration types for package-local LACE training.

LaceSeedMode

Bases: StrEnum

Regular and deterministic seed policies.

Source code in models/lace/src/lace/training/config.py
12
13
14
15
16
class LaceSeedMode(StrEnum):
    """Regular and deterministic seed policies."""

    default = auto()
    deterministic = auto()

datamodule

Lightning data module for processed LACE layouts.

LaceDataModule

Bases: LightningDataModule

Load processed train, validation, and test streams for LACE.

Source code in models/lace/src/lace/training/datamodule.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
class LaceDataModule(LightningDataModule):
    """Load processed train, validation, and test streams for LACE."""

    def __init__(
        self,
        *,
        processed_data_dir: str | Path = ".cache/lace/processed",
        dataset_name: LaceTrainingDatasetName = "publaynet",
        batch_size: int = 256,
        max_seq_length: int = 25,
        num_workers: int = 4,
        pin_memory: bool = True,
        loader_seed: int | None = None,
    ) -> None:
        """Initialize loader settings matching the original entry point."""
        super().__init__()
        self.processed_data_dir = Path(processed_data_dir)
        self.dataset_name = dataset_name
        self.batch_size = batch_size
        self.max_seq_length = max_seq_length
        self.num_workers = num_workers
        self.pin_memory = pin_memory
        self.loader_seed = loader_seed
        self.train_dataset: LaceProcessedDataset | None = None
        self.val_dataset: LaceProcessedDataset | None = None
        self.test_dataset: LaceProcessedDataset | None = None

    def setup(self, stage: str | None = None) -> None:
        """Open requested processed splits."""
        if stage in {None, "fit"}:
            self.train_dataset = self._dataset("train")
            self.val_dataset = self._dataset("validation")

        if stage in {None, "test"}:
            self.test_dataset = self._dataset("test")

    def train_dataloader(
        self,
    ) -> DataLoader[dict[str, Shaped[torch.Tensor, "..."] | list[str]]]:
        """Return the shuffled training stream."""
        return self._loader(self._ensure_dataset("train"), shuffle=True)

    def val_dataloader(
        self,
    ) -> DataLoader[dict[str, Shaped[torch.Tensor, "..."] | list[str]]]:
        """Return the deterministic validation stream."""
        return self._loader(self._ensure_dataset("validation"), shuffle=False)

    def test_dataloader(
        self,
    ) -> DataLoader[dict[str, Shaped[torch.Tensor, "..."] | list[str]]]:
        """Return the deterministic test stream."""
        return self._loader(self._ensure_dataset("test"), shuffle=False)

    def _dataset(self, split: LaceTrainingSplit) -> LaceProcessedDataset:
        return LaceProcessedDataset(
            processed_data_dir=self.processed_data_dir,
            dataset_name=self.dataset_name,
            split=split,
            max_seq_length=self.max_seq_length,
        )

    def _ensure_dataset(self, split: LaceTrainingSplit) -> LaceProcessedDataset:
        dataset = {
            "train": self.train_dataset,
            "validation": self.val_dataset,
            "test": self.test_dataset,
        }[split]
        if dataset is None:
            self.setup("test" if split == "test" else "fit")
            dataset = {
                "train": self.train_dataset,
                "validation": self.val_dataset,
                "test": self.test_dataset,
            }[split]

        if dataset is None:
            raise RuntimeError(f"Dataset {split} has not been initialized")

        return dataset

    def _loader(
        self,
        dataset: LaceProcessedDataset | None,
        *,
        shuffle: bool,
    ) -> DataLoader[dict[str, Shaped[torch.Tensor, "..."] | list[str]]]:
        if dataset is None:
            raise RuntimeError("Dataset has not been initialized")

        loader = DataLoader(
            dataset,
            batch_size=self.batch_size,
            shuffle=shuffle,
            num_workers=self.num_workers,
            pin_memory=self.pin_memory,
            collate_fn=partial(collate_lace_batch, max_seq_length=self.max_seq_length),
            generator=(
                torch.Generator().manual_seed(self.loader_seed)
                if self.loader_seed is not None
                else None
            ),
        )
        return cast(
            DataLoader[dict[str, Shaped[torch.Tensor, "..."] | list[str]]], loader
        )
__init__
__init__(
    *,
    processed_data_dir: str
    | Path = ".cache/lace/processed",
    dataset_name: LaceTrainingDatasetName = "publaynet",
    batch_size: int = 256,
    max_seq_length: int = 25,
    num_workers: int = 4,
    pin_memory: bool = True,
    loader_seed: int | None = None,
) -> None

Initialize loader settings matching the original entry point.

Source code in models/lace/src/lace/training/datamodule.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __init__(
    self,
    *,
    processed_data_dir: str | Path = ".cache/lace/processed",
    dataset_name: LaceTrainingDatasetName = "publaynet",
    batch_size: int = 256,
    max_seq_length: int = 25,
    num_workers: int = 4,
    pin_memory: bool = True,
    loader_seed: int | None = None,
) -> None:
    """Initialize loader settings matching the original entry point."""
    super().__init__()
    self.processed_data_dir = Path(processed_data_dir)
    self.dataset_name = dataset_name
    self.batch_size = batch_size
    self.max_seq_length = max_seq_length
    self.num_workers = num_workers
    self.pin_memory = pin_memory
    self.loader_seed = loader_seed
    self.train_dataset: LaceProcessedDataset | None = None
    self.val_dataset: LaceProcessedDataset | None = None
    self.test_dataset: LaceProcessedDataset | None = None
setup
setup(stage: str | None = None) -> None

Open requested processed splits.

Source code in models/lace/src/lace/training/datamodule.py
45
46
47
48
49
50
51
52
def setup(self, stage: str | None = None) -> None:
    """Open requested processed splits."""
    if stage in {None, "fit"}:
        self.train_dataset = self._dataset("train")
        self.val_dataset = self._dataset("validation")

    if stage in {None, "test"}:
        self.test_dataset = self._dataset("test")
train_dataloader
train_dataloader() -> DataLoader[
    dict[str, Shaped[torch.Tensor, "..."] | list[str]]
]

Return the shuffled training stream.

Source code in models/lace/src/lace/training/datamodule.py
54
55
56
57
58
def train_dataloader(
    self,
) -> DataLoader[dict[str, Shaped[torch.Tensor, "..."] | list[str]]]:
    """Return the shuffled training stream."""
    return self._loader(self._ensure_dataset("train"), shuffle=True)
val_dataloader
val_dataloader() -> DataLoader[
    dict[str, Shaped[torch.Tensor, "..."] | list[str]]
]

Return the deterministic validation stream.

Source code in models/lace/src/lace/training/datamodule.py
60
61
62
63
64
def val_dataloader(
    self,
) -> DataLoader[dict[str, Shaped[torch.Tensor, "..."] | list[str]]]:
    """Return the deterministic validation stream."""
    return self._loader(self._ensure_dataset("validation"), shuffle=False)
test_dataloader
test_dataloader() -> DataLoader[
    dict[str, Shaped[torch.Tensor, "..."] | list[str]]
]

Return the deterministic test stream.

Source code in models/lace/src/lace/training/datamodule.py
66
67
68
69
70
def test_dataloader(
    self,
) -> DataLoader[dict[str, Shaped[torch.Tensor, "..."] | list[str]]]:
    """Return the deterministic test stream."""
    return self._loader(self._ensure_dataset("test"), shuffle=False)

dataset

Processed LayoutDM-style datasets used by the LACE training loop.

LaceProcessedDataset

Bases: Dataset[dict[str, Shaped[Tensor, '...'] | str]]

Read one processed LACE split without reprocessing source annotations.

The file layout and row slicing match the original InMemoryDataset. The dataset returns variable-length rows; the package datamodule pads them in the same batch-local order as the original sparse-to-dense loader.

Source code in models/lace/src/lace/training/dataset.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
class LaceProcessedDataset(Dataset[dict[str, Shaped[torch.Tensor, "..."] | str]]):
    """Read one processed LACE split without reprocessing source annotations.

    The file layout and row slicing match the original InMemoryDataset.  The
    dataset returns variable-length rows; the package datamodule pads them in
    the same batch-local order as the original sparse-to-dense loader.
    """

    def __init__(
        self,
        *,
        processed_data_dir: str | Path,
        dataset_name: LaceTrainingDatasetName,
        split: LaceTrainingSplit,
        max_seq_length: int = 25,
    ) -> None:
        """Open a processed split from ``<dataset>-max<S>/processed``."""
        self.dataset_name = dataset_name
        self.split = split
        self.max_seq_length = max_seq_length
        processed_root = Path(processed_data_dir)

        path = processed_root / f"{_PROCESSED_SPLITS[split]}.pt"
        if not path.exists():
            raise FileNotFoundError(path)

        loaded = torch.load(path, map_location="cpu", weights_only=False)

        data, slices = loaded
        self.data = cast(_ProcessedData, data)
        self.slices = cast(Mapping[str, Int[torch.Tensor, "rows"]], slices)
        x_slices = self.slices.get("x")
        if not isinstance(x_slices, torch.Tensor):
            raise TypeError("processed LACE split must contain x slices")

        self.length = int(x_slices.numel() - 1)

    def __len__(self) -> int:
        """Return the number of layouts in the split."""
        return self.length

    def __getitem__(self, index: int) -> dict[str, Shaped[torch.Tensor, "..."] | str]:
        """Return one raw layout row with its source identifier."""
        x_slices = self.slices["x"]
        y_slices = self.slices["y"]
        x_start, x_end = int(x_slices[index]), int(x_slices[index + 1])
        y_start, y_end = int(y_slices[index]), int(y_slices[index + 1])
        bbox = self.data.x[x_start:x_end].float()
        labels = self.data.y[y_start:y_end].long()
        sample_id = self._sample_id(index)
        result: dict[str, Shaped[torch.Tensor, "..."] | str] = {
            "bbox": bbox,
            "labels": labels,
            "mask": torch.ones(labels.shape, dtype=torch.bool),
        }
        if sample_id is not None:
            result["id"] = sample_id

        return result

    def _sample_id(self, index: int) -> str | None:
        attr = getattr(self.data, "attr", None)
        if not isinstance(attr, Mapping):
            return None

        names = attr.get("name")
        if isinstance(names, (list, tuple)) and index < len(names):
            return str(names[index])

        if isinstance(names, str) and index == 0:
            return names

        return None
__init__
__init__(
    *,
    processed_data_dir: str | Path,
    dataset_name: LaceTrainingDatasetName,
    split: LaceTrainingSplit,
    max_seq_length: int = 25,
) -> None

Open a processed split from <dataset>-max<S>/processed.

Source code in models/lace/src/lace/training/dataset.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def __init__(
    self,
    *,
    processed_data_dir: str | Path,
    dataset_name: LaceTrainingDatasetName,
    split: LaceTrainingSplit,
    max_seq_length: int = 25,
) -> None:
    """Open a processed split from ``<dataset>-max<S>/processed``."""
    self.dataset_name = dataset_name
    self.split = split
    self.max_seq_length = max_seq_length
    processed_root = Path(processed_data_dir)

    path = processed_root / f"{_PROCESSED_SPLITS[split]}.pt"
    if not path.exists():
        raise FileNotFoundError(path)

    loaded = torch.load(path, map_location="cpu", weights_only=False)

    data, slices = loaded
    self.data = cast(_ProcessedData, data)
    self.slices = cast(Mapping[str, Int[torch.Tensor, "rows"]], slices)
    x_slices = self.slices.get("x")
    if not isinstance(x_slices, torch.Tensor):
        raise TypeError("processed LACE split must contain x slices")

    self.length = int(x_slices.numel() - 1)
__len__
__len__() -> int

Return the number of layouts in the split.

Source code in models/lace/src/lace/training/dataset.py
66
67
68
def __len__(self) -> int:
    """Return the number of layouts in the split."""
    return self.length
__getitem__
__getitem__(
    index: int,
) -> dict[str, Shaped[torch.Tensor, "..."] | str]

Return one raw layout row with its source identifier.

Source code in models/lace/src/lace/training/dataset.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __getitem__(self, index: int) -> dict[str, Shaped[torch.Tensor, "..."] | str]:
    """Return one raw layout row with its source identifier."""
    x_slices = self.slices["x"]
    y_slices = self.slices["y"]
    x_start, x_end = int(x_slices[index]), int(x_slices[index + 1])
    y_start, y_end = int(y_slices[index]), int(y_slices[index + 1])
    bbox = self.data.x[x_start:x_end].float()
    labels = self.data.y[y_start:y_end].long()
    sample_id = self._sample_id(index)
    result: dict[str, Shaped[torch.Tensor, "..."] | str] = {
        "bbox": bbox,
        "labels": labels,
        "mask": torch.ones(labels.shape, dtype=torch.bool),
    }
    if sample_id is not None:
        result["id"] = sample_id

    return result

collate_lace_batch

collate_lace_batch(
    batch: list[dict[str, Shaped[Tensor, "..."] | str]],
    *,
    max_seq_length: int = 25,
) -> dict[str, Shaped[torch.Tensor, "..."] | list[str]]

Pad a batch to the reference fixed maximum sequence length.

Source code in models/lace/src/lace/training/dataset.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def collate_lace_batch(
    batch: list[dict[str, Shaped[torch.Tensor, "..."] | str]],
    *,
    max_seq_length: int = 25,
) -> dict[str, Shaped[torch.Tensor, "..."] | list[str]]:
    """Pad a batch to the reference fixed maximum sequence length."""
    if not batch:
        raise ValueError("LACE batches cannot be empty")

    target = max_seq_length
    bboxes: list[Float[torch.Tensor, "elements 4"]] = []
    labels: list[Int[torch.Tensor, "elements"]] = []
    masks: list[Bool[torch.Tensor, "elements"]] = []
    ids: list[str] = []
    for item in batch:
        bbox = cast(Float[torch.Tensor, "elements 4"], item["bbox"])
        label = cast(Int[torch.Tensor, "elements"], item["labels"])
        length = min(int(label.shape[0]), target)

        bbox_pad = torch.zeros(target, 4, dtype=torch.float32)
        label_pad = torch.zeros(target, dtype=torch.long)
        mask_pad = torch.zeros(target, dtype=torch.bool)

        bbox_pad[:length] = bbox[:length]
        label_pad[:length] = label[:length]
        mask_pad[:length] = True

        bboxes.append(bbox_pad)
        labels.append(label_pad)
        masks.append(mask_pad)
        if isinstance(item.get("id"), str):
            ids.append(cast(str, item["id"]))

    result: dict[str, Shaped[torch.Tensor, "..."] | list[str]] = {
        "bbox": torch.stack(bboxes),
        "labels": torch.stack(labels),
        "mask": torch.stack(masks),
    }
    if len(ids) == len(batch):
        result["id"] = ids

    return result

ema

Exponential moving-average state used by the LACE recipe.

LaceEMA

Track trainable parameters with the original decay rule.

Source code in models/lace/src/lace/training/ema.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class LaceEMA:
    """Track trainable parameters with the original decay rule."""

    def __init__(self, *, mu: float = 0.9999) -> None:
        """Initialize the decay coefficient and empty shadow state."""
        self.mu = mu
        self.shadow: dict[str, Shaped[torch.Tensor, "..."]] = {}

    def register(self, module: nn.Module) -> None:
        """Clone every trainable parameter before the first update."""
        self.shadow = {
            name: parameter.detach().clone()
            for name, parameter in module.named_parameters()
            if parameter.requires_grad
        }

    def update(self, module: nn.Module) -> None:
        """Apply one post-optimizer EMA update."""
        if not self.shadow:
            self.register(module)

        for name, parameter in module.named_parameters():
            if parameter.requires_grad:
                updated = (1 - self.mu) * parameter.detach() + self.mu * self.shadow[
                    name
                ]
                self.shadow[name].copy_(updated)

    def state_dict(self) -> dict[str, Shaped[torch.Tensor, "..."]]:
        """Return detached EMA tensors."""
        return {name: tensor.detach().clone() for name, tensor in self.shadow.items()}

    def load_state_dict(
        self, state_dict: Mapping[str, Shaped[torch.Tensor, "..."]]
    ) -> None:
        """Load independent EMA tensors."""
        self.shadow = {
            name: tensor.detach().clone() for name, tensor in state_dict.items()
        }
__init__
__init__(*, mu: float = 0.9999) -> None

Initialize the decay coefficient and empty shadow state.

Source code in models/lace/src/lace/training/ema.py
15
16
17
18
def __init__(self, *, mu: float = 0.9999) -> None:
    """Initialize the decay coefficient and empty shadow state."""
    self.mu = mu
    self.shadow: dict[str, Shaped[torch.Tensor, "..."]] = {}
register
register(module: Module) -> None

Clone every trainable parameter before the first update.

Source code in models/lace/src/lace/training/ema.py
20
21
22
23
24
25
26
def register(self, module: nn.Module) -> None:
    """Clone every trainable parameter before the first update."""
    self.shadow = {
        name: parameter.detach().clone()
        for name, parameter in module.named_parameters()
        if parameter.requires_grad
    }
update
update(module: Module) -> None

Apply one post-optimizer EMA update.

Source code in models/lace/src/lace/training/ema.py
28
29
30
31
32
33
34
35
36
37
38
def update(self, module: nn.Module) -> None:
    """Apply one post-optimizer EMA update."""
    if not self.shadow:
        self.register(module)

    for name, parameter in module.named_parameters():
        if parameter.requires_grad:
            updated = (1 - self.mu) * parameter.detach() + self.mu * self.shadow[
                name
            ]
            self.shadow[name].copy_(updated)
state_dict
state_dict() -> dict[str, Shaped[torch.Tensor, '...']]

Return detached EMA tensors.

Source code in models/lace/src/lace/training/ema.py
40
41
42
def state_dict(self) -> dict[str, Shaped[torch.Tensor, "..."]]:
    """Return detached EMA tensors."""
    return {name: tensor.detach().clone() for name, tensor in self.shadow.items()}
load_state_dict
load_state_dict(
    state_dict: Mapping[str, Shaped[Tensor, "..."]],
) -> None

Load independent EMA tensors.

Source code in models/lace/src/lace/training/ema.py
44
45
46
47
48
49
50
def load_state_dict(
    self, state_dict: Mapping[str, Shaped[torch.Tensor, "..."]]
) -> None:
    """Load independent EMA tensors."""
    self.shadow = {
        name: tensor.detach().clone() for name, tensor in state_dict.items()
    }

lightning_module

Lightning training module matching the effective LACE recipe.

LaceTrainingModule

Bases: LightningModule

Lightning wrapper for the original plain-PyTorch training loop.

Source code in models/lace/src/lace/training/lightning_module.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
class LaceTrainingModule(LightningModule):
    """Lightning wrapper for the original plain-PyTorch training loop."""

    def __init__(
        self,
        *,
        dataset_name: LaceTrainingDatasetName = "publaynet",
        model: LaceTransformerModel | None = None,
        learning_rate: float = 1e-5,
        batch_size: int = 256,
        sample_t_max: int = 999,
        feature_dim: int = 2048,
        dim_transformer: int = 1024,
        nhead: int = 16,
        num_layers: int = 4,
        ema_mu: float = 0.9999,
        seed_mode: LaceSeedMode | str = LaceSeedMode.default,
        seed: int = 42975,
    ) -> None:
        """Initialize model, diffusion buffers, and active recipe values."""
        super().__init__()

        spec = get_dataset_spec(dataset_name)
        config = default_model_config(dataset_name)
        config["dim_transformer"] = dim_transformer
        config["dim_feedforward"] = feature_dim
        config["nhead"] = nhead
        config["num_layers"] = num_layers

        self.dataset_name = dataset_name
        self.num_classes = spec.num_classes_with_pad
        self.seq_dim = spec.seq_dim
        self.batch_size = batch_size
        self.learning_rate = learning_rate
        self.sample_t_max = sample_t_max
        self.ema_mu = ema_mu
        self.seed_mode = LaceSeedMode(seed_mode)
        self.seed = seed

        self.model = model or LaceTransformerModel(**config)
        self.ema_helper = LaceEMA(mu=ema_mu)
        self.ema_helper.register(self.model)

        schedule = LaceScheduler(num_train_timesteps=1000, ddim_num_steps=200)
        model_device = next(self.model.parameters()).device
        alphas_cumprod = schedule.alphas_cumprod_for_device(model_device)

        self.register_buffer("alphas_bar_sqrt", alphas_cumprod.sqrt())
        self.register_buffer("one_minus_alphas_bar_sqrt", (1 - alphas_cumprod).sqrt())
        self.latest_step_trace: dict[str, Shaped[torch.Tensor, "..."]] = {}
        self.latest_ema_state: dict[str, Shaped[torch.Tensor, "..."]] = {}
        self.save_hyperparameters(ignore=("model",))

    def on_fit_start(self) -> None:
        """Apply the configured seed policy at the training boundary."""
        apply_lace_seed_mode(self.seed_mode, seed=self.seed)
        model_device = next(self.model.parameters()).device
        self.ema_helper.shadow = {
            name: value.to(model_device)
            for name, value in self.ema_helper.shadow.items()
        }
        alphas_cumprod = LaceScheduler(
            num_train_timesteps=1000
        ).alphas_cumprod_for_device(cast(torch.Tensor, self.alphas_bar_sqrt).device)
        cast(torch.Tensor, self.alphas_bar_sqrt).copy_(alphas_cumprod.sqrt())
        cast(torch.Tensor, self.one_minus_alphas_bar_sqrt).copy_(
            (1 - alphas_cumprod).sqrt()
        )

    def configure_optimizers(self) -> OptimizerLRScheduler:
        """Return the reference unscheduled Adam optimizer."""
        return torch.optim.Adam(
            self.model.parameters(),
            lr=self.learning_rate,
            weight_decay=0.0,
            betas=(0.9, 0.999),
            amsgrad=False,
            eps=1e-8,
        )

    def on_train_batch_end(
        self,
        outputs: Float[torch.Tensor, ""]
        | Mapping[str, Shaped[torch.Tensor, "..."]]
        | None,
        batch: Mapping[str, Shaped[torch.Tensor, "..."] | list[str]],
        batch_idx: int,
    ) -> None:
        """Update EMA after Lightning completes the optimizer step."""
        del outputs, batch, batch_idx
        self.ema_helper.update(self.model)
        self.latest_ema_state = self.ema_helper.state_dict()

    def forward_t(
        self,
        layout_input: Float[torch.Tensor, "batch elements channels"],
        mask: Bool[torch.Tensor, "batch elements"],
        *,
        timestep: Int[torch.Tensor, "batch"],
    ) -> tuple[
        Float[torch.Tensor, "four_batch elements channels"],
        Float[torch.Tensor, "four_batch elements channels"],
        Float[torch.Tensor, "four_batch elements channels"],
        Int[torch.Tensor, "four_batch"],
    ]:
        """Run the four effective conditioning branches in reference order."""
        noise = torch.randn_like(layout_input)
        sqrt_alpha = cast(torch.Tensor, self.alphas_bar_sqrt)[timestep].reshape(
            -1, 1, 1
        )
        sqrt_one_minus = cast(torch.Tensor, self.one_minus_alphas_bar_sqrt)[
            timestep
        ].reshape(-1, 1, 1)
        noisy = sqrt_alpha * layout_input + sqrt_one_minus * noise
        conditioned_labels = layout_input.clone()
        conditioned_labels[:, :, self.num_classes :] = noisy[:, :, self.num_classes :]
        conditioned_centers = layout_input.clone()
        conditioned_centers[:, :, self.num_classes : self.num_classes + 2] = noisy[
            :, :, self.num_classes : self.num_classes + 2
        ]
        fixed = rand_fix(layout_input.shape[0], mask)
        conditioned_complete = noisy.clone()
        conditioned_complete[fixed] = layout_input[fixed]
        all_inputs = torch.cat(
            (noisy, conditioned_labels, conditioned_centers, conditioned_complete),
            dim=0,
        )
        all_noise = torch.cat((noise,) * 4, dim=0)
        all_timesteps = torch.cat((timestep,) * 4, dim=0)
        output = self.model(sample=all_inputs, timestep=all_timesteps).sample
        sqrt_one_minus_all = cast(torch.Tensor, self.one_minus_alphas_bar_sqrt)[
            all_timesteps
        ].reshape(-1, 1, 1)
        sqrt_alpha_all = (1 - sqrt_one_minus_all.square()).sqrt()
        reconstructed = (
            1 / sqrt_alpha_all * (all_inputs - output * sqrt_one_minus_all)
        ).to(all_inputs.device)
        return output, all_noise, reconstructed, all_timesteps

    def training_step(
        self,
        batch: dict[str, Shaped[torch.Tensor, "..."] | list[str]],
        batch_idx: int,
    ) -> Float[torch.Tensor, ""]:
        """Run one package-local diffusion and constraint step."""
        del batch_idx
        bbox = cast(Float[torch.Tensor, "batch elements 4"], batch["bbox"]).float()
        labels = cast(Int[torch.Tensor, "batch elements"], batch["labels"]).long()
        mask = cast(Bool[torch.Tensor, "batch elements"], batch["mask"]).bool()
        labels = labels.clone()
        labels[~mask] = self.num_classes - 1
        bbox_in = 2 * (bbox - 0.5)
        one_hot = nn.functional.one_hot(labels, num_classes=self.num_classes).to(
            dtype=bbox_in.dtype
        )
        layout_input = torch.cat((one_hot, bbox_in), dim=2)
        timestep = torch.randint(
            low=0,
            high=self.sample_t_max,
            size=(bbox.shape[0],),
            device=bbox.device,
        )
        output, noise, reconstructed, all_timesteps = self.forward_t(
            layout_input, mask, timestep=timestep
        )
        losses = lace_losses(
            layout_input=layout_input,
            model_output=output,
            noise=noise,
            reconstructed=reconstructed,
            bbox=torch.cat((bbox,) * 4, dim=0),
            mask=torch.cat((mask,) * 4, dim=0),
            timesteps=all_timesteps,
            num_classes=self.num_classes,
        )
        self.log("train_loss", losses["train_loss"], on_step=True, on_epoch=True)
        self.latest_step_trace = {
            "bbox": bbox.detach(),
            "labels": labels.detach(),
            "mask": mask.detach(),
            "layout_input": layout_input.detach(),
            "timestep": timestep.detach(),
            "noise": noise.detach(),
            "model_output": output.detach(),
            "reconstructed": reconstructed.detach(),
            "all_timesteps": all_timesteps.detach(),
            **{key: value.detach() for key, value in losses.items()},
        }
        return losses["train_loss"]
__init__
__init__(
    *,
    dataset_name: LaceTrainingDatasetName = "publaynet",
    model: LaceTransformerModel | None = None,
    learning_rate: float = 1e-05,
    batch_size: int = 256,
    sample_t_max: int = 999,
    feature_dim: int = 2048,
    dim_transformer: int = 1024,
    nhead: int = 16,
    num_layers: int = 4,
    ema_mu: float = 0.9999,
    seed_mode: LaceSeedMode | str = LaceSeedMode.default,
    seed: int = 42975,
) -> None

Initialize model, diffusion buffers, and active recipe values.

Source code in models/lace/src/lace/training/lightning_module.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def __init__(
    self,
    *,
    dataset_name: LaceTrainingDatasetName = "publaynet",
    model: LaceTransformerModel | None = None,
    learning_rate: float = 1e-5,
    batch_size: int = 256,
    sample_t_max: int = 999,
    feature_dim: int = 2048,
    dim_transformer: int = 1024,
    nhead: int = 16,
    num_layers: int = 4,
    ema_mu: float = 0.9999,
    seed_mode: LaceSeedMode | str = LaceSeedMode.default,
    seed: int = 42975,
) -> None:
    """Initialize model, diffusion buffers, and active recipe values."""
    super().__init__()

    spec = get_dataset_spec(dataset_name)
    config = default_model_config(dataset_name)
    config["dim_transformer"] = dim_transformer
    config["dim_feedforward"] = feature_dim
    config["nhead"] = nhead
    config["num_layers"] = num_layers

    self.dataset_name = dataset_name
    self.num_classes = spec.num_classes_with_pad
    self.seq_dim = spec.seq_dim
    self.batch_size = batch_size
    self.learning_rate = learning_rate
    self.sample_t_max = sample_t_max
    self.ema_mu = ema_mu
    self.seed_mode = LaceSeedMode(seed_mode)
    self.seed = seed

    self.model = model or LaceTransformerModel(**config)
    self.ema_helper = LaceEMA(mu=ema_mu)
    self.ema_helper.register(self.model)

    schedule = LaceScheduler(num_train_timesteps=1000, ddim_num_steps=200)
    model_device = next(self.model.parameters()).device
    alphas_cumprod = schedule.alphas_cumprod_for_device(model_device)

    self.register_buffer("alphas_bar_sqrt", alphas_cumprod.sqrt())
    self.register_buffer("one_minus_alphas_bar_sqrt", (1 - alphas_cumprod).sqrt())
    self.latest_step_trace: dict[str, Shaped[torch.Tensor, "..."]] = {}
    self.latest_ema_state: dict[str, Shaped[torch.Tensor, "..."]] = {}
    self.save_hyperparameters(ignore=("model",))
on_fit_start
on_fit_start() -> None

Apply the configured seed policy at the training boundary.

Source code in models/lace/src/lace/training/lightning_module.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def on_fit_start(self) -> None:
    """Apply the configured seed policy at the training boundary."""
    apply_lace_seed_mode(self.seed_mode, seed=self.seed)
    model_device = next(self.model.parameters()).device
    self.ema_helper.shadow = {
        name: value.to(model_device)
        for name, value in self.ema_helper.shadow.items()
    }
    alphas_cumprod = LaceScheduler(
        num_train_timesteps=1000
    ).alphas_cumprod_for_device(cast(torch.Tensor, self.alphas_bar_sqrt).device)
    cast(torch.Tensor, self.alphas_bar_sqrt).copy_(alphas_cumprod.sqrt())
    cast(torch.Tensor, self.one_minus_alphas_bar_sqrt).copy_(
        (1 - alphas_cumprod).sqrt()
    )
configure_optimizers
configure_optimizers() -> OptimizerLRScheduler

Return the reference unscheduled Adam optimizer.

Source code in models/lace/src/lace/training/lightning_module.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def configure_optimizers(self) -> OptimizerLRScheduler:
    """Return the reference unscheduled Adam optimizer."""
    return torch.optim.Adam(
        self.model.parameters(),
        lr=self.learning_rate,
        weight_decay=0.0,
        betas=(0.9, 0.999),
        amsgrad=False,
        eps=1e-8,
    )
on_train_batch_end
on_train_batch_end(
    outputs: Float[Tensor, ""]
    | Mapping[str, Shaped[Tensor, "..."]]
    | None,
    batch: Mapping[str, Shaped[Tensor, "..."] | list[str]],
    batch_idx: int,
) -> None

Update EMA after Lightning completes the optimizer step.

Source code in models/lace/src/lace/training/lightning_module.py
103
104
105
106
107
108
109
110
111
112
113
114
def on_train_batch_end(
    self,
    outputs: Float[torch.Tensor, ""]
    | Mapping[str, Shaped[torch.Tensor, "..."]]
    | None,
    batch: Mapping[str, Shaped[torch.Tensor, "..."] | list[str]],
    batch_idx: int,
) -> None:
    """Update EMA after Lightning completes the optimizer step."""
    del outputs, batch, batch_idx
    self.ema_helper.update(self.model)
    self.latest_ema_state = self.ema_helper.state_dict()
forward_t
forward_t(
    layout_input: Float[Tensor, "batch elements channels"],
    mask: Bool[Tensor, "batch elements"],
    *,
    timestep: Int[Tensor, "batch"],
) -> tuple[
    Float[torch.Tensor, "four_batch elements channels"],
    Float[torch.Tensor, "four_batch elements channels"],
    Float[torch.Tensor, "four_batch elements channels"],
    Int[torch.Tensor, "four_batch"],
]

Run the four effective conditioning branches in reference order.

Source code in models/lace/src/lace/training/lightning_module.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def forward_t(
    self,
    layout_input: Float[torch.Tensor, "batch elements channels"],
    mask: Bool[torch.Tensor, "batch elements"],
    *,
    timestep: Int[torch.Tensor, "batch"],
) -> tuple[
    Float[torch.Tensor, "four_batch elements channels"],
    Float[torch.Tensor, "four_batch elements channels"],
    Float[torch.Tensor, "four_batch elements channels"],
    Int[torch.Tensor, "four_batch"],
]:
    """Run the four effective conditioning branches in reference order."""
    noise = torch.randn_like(layout_input)
    sqrt_alpha = cast(torch.Tensor, self.alphas_bar_sqrt)[timestep].reshape(
        -1, 1, 1
    )
    sqrt_one_minus = cast(torch.Tensor, self.one_minus_alphas_bar_sqrt)[
        timestep
    ].reshape(-1, 1, 1)
    noisy = sqrt_alpha * layout_input + sqrt_one_minus * noise
    conditioned_labels = layout_input.clone()
    conditioned_labels[:, :, self.num_classes :] = noisy[:, :, self.num_classes :]
    conditioned_centers = layout_input.clone()
    conditioned_centers[:, :, self.num_classes : self.num_classes + 2] = noisy[
        :, :, self.num_classes : self.num_classes + 2
    ]
    fixed = rand_fix(layout_input.shape[0], mask)
    conditioned_complete = noisy.clone()
    conditioned_complete[fixed] = layout_input[fixed]
    all_inputs = torch.cat(
        (noisy, conditioned_labels, conditioned_centers, conditioned_complete),
        dim=0,
    )
    all_noise = torch.cat((noise,) * 4, dim=0)
    all_timesteps = torch.cat((timestep,) * 4, dim=0)
    output = self.model(sample=all_inputs, timestep=all_timesteps).sample
    sqrt_one_minus_all = cast(torch.Tensor, self.one_minus_alphas_bar_sqrt)[
        all_timesteps
    ].reshape(-1, 1, 1)
    sqrt_alpha_all = (1 - sqrt_one_minus_all.square()).sqrt()
    reconstructed = (
        1 / sqrt_alpha_all * (all_inputs - output * sqrt_one_minus_all)
    ).to(all_inputs.device)
    return output, all_noise, reconstructed, all_timesteps
training_step
training_step(
    batch: dict[str, Shaped[Tensor, "..."] | list[str]],
    batch_idx: int,
) -> Float[torch.Tensor, ""]

Run one package-local diffusion and constraint step.

Source code in models/lace/src/lace/training/lightning_module.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
def training_step(
    self,
    batch: dict[str, Shaped[torch.Tensor, "..."] | list[str]],
    batch_idx: int,
) -> Float[torch.Tensor, ""]:
    """Run one package-local diffusion and constraint step."""
    del batch_idx
    bbox = cast(Float[torch.Tensor, "batch elements 4"], batch["bbox"]).float()
    labels = cast(Int[torch.Tensor, "batch elements"], batch["labels"]).long()
    mask = cast(Bool[torch.Tensor, "batch elements"], batch["mask"]).bool()
    labels = labels.clone()
    labels[~mask] = self.num_classes - 1
    bbox_in = 2 * (bbox - 0.5)
    one_hot = nn.functional.one_hot(labels, num_classes=self.num_classes).to(
        dtype=bbox_in.dtype
    )
    layout_input = torch.cat((one_hot, bbox_in), dim=2)
    timestep = torch.randint(
        low=0,
        high=self.sample_t_max,
        size=(bbox.shape[0],),
        device=bbox.device,
    )
    output, noise, reconstructed, all_timesteps = self.forward_t(
        layout_input, mask, timestep=timestep
    )
    losses = lace_losses(
        layout_input=layout_input,
        model_output=output,
        noise=noise,
        reconstructed=reconstructed,
        bbox=torch.cat((bbox,) * 4, dim=0),
        mask=torch.cat((mask,) * 4, dim=0),
        timesteps=all_timesteps,
        num_classes=self.num_classes,
    )
    self.log("train_loss", losses["train_loss"], on_step=True, on_epoch=True)
    self.latest_step_trace = {
        "bbox": bbox.detach(),
        "labels": labels.detach(),
        "mask": mask.detach(),
        "layout_input": layout_input.detach(),
        "timestep": timestep.detach(),
        "noise": noise.detach(),
        "model_output": output.detach(),
        "reconstructed": reconstructed.detach(),
        "all_timesteps": all_timesteps.detach(),
        **{key: value.detach() for key, value in losses.items()},
    }
    return losses["train_loss"]

losses

Package-local copies of the effective LACE constraint losses.

xywh_to_ltrb_reference

xywh_to_ltrb_reference(
    bbox: Float[Tensor, "batch elements 4"],
) -> Float[torch.Tensor, "batch elements 4"]

Convert center boxes with the reference allocation and assignments.

Source code in models/lace/src/lace/training/losses.py
11
12
13
14
15
16
17
18
19
20
def xywh_to_ltrb_reference(
    bbox: Float[torch.Tensor, "batch elements 4"],
) -> Float[torch.Tensor, "batch elements 4"]:
    """Convert center boxes with the reference allocation and assignments."""
    bbox_ltrb = torch.zeros(bbox.shape).to(bbox.device)
    bbox_xy = torch.abs(bbox[:, :, :2])
    bbox_wh = torch.abs(bbox[:, :, 2:])
    bbox_ltrb[:, :, :2] = bbox_xy - 0.5 * bbox_wh
    bbox_ltrb[:, :, 2:] = bbox_xy + 0.5 * bbox_wh
    return bbox_ltrb

pairwise_iou

pairwise_iou(
    bbox: Float[Tensor, "batch elements 4"],
    mask: Float[Tensor, "batch elements"],
) -> Float[torch.Tensor, "batch elements elements"]

Return the reference pairwise intersection-over-union matrix.

Source code in models/lace/src/lace/training/losses.py
57
58
59
60
61
62
def pairwise_iou(
    bbox: Float[torch.Tensor, "batch elements 4"],
    mask: Float[torch.Tensor, "batch elements"],
) -> Float[torch.Tensor, "batch elements elements"]:
    """Return the reference pairwise intersection-over-union matrix."""
    return _piou_ltrb(xywh_to_ltrb_reference(bbox), mask)

alignment_matrix

alignment_matrix(
    bbox: Float[Tensor, "batch elements 4"],
    mask: Bool[Tensor, "batch elements"],
) -> Float[torch.Tensor, "batch elements 6 elements"]

Return the reference raw coordinate-alignment matrix.

Source code in models/lace/src/lace/training/losses.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def alignment_matrix(
    bbox: Float[torch.Tensor, "batch elements 4"],
    mask: Bool[torch.Tensor, "batch elements"],
) -> Float[torch.Tensor, "batch elements 6 elements"]:
    """Return the reference raw coordinate-alignment matrix."""
    bbox = bbox.permute(2, 0, 1)
    xl, yt, xr, yb = _xywh_to_ltrb_split(bbox)
    xc, yc = bbox[0], bbox[1]
    x = torch.stack([xl, xc, xr, yt, yc, yb], dim=1)
    x = x.unsqueeze(-1) - x.unsqueeze(-2)
    idx = torch.arange(x.size(2), device=x.device)
    x[:, :, idx, idx] = 1.0
    x = x.abs().permute(0, 2, 1, 3)
    x[~mask] = 1.0
    return x

constraint_temporal_weight

constraint_temporal_weight(
    timestep: Int[Tensor, "batch"],
    *,
    num_timesteps: int = 1000,
    end: float = 0.01,
) -> Float[torch.Tensor, "batch"]

Return the reference constant temporal constraint weight.

Source code in models/lace/src/lace/training/losses.py
 99
100
101
102
103
104
105
106
107
def constraint_temporal_weight(
    timestep: Int[torch.Tensor, "batch"],
    *,
    num_timesteps: int = 1000,
    end: float = 1e-2,
) -> Float[torch.Tensor, "batch"]:
    """Return the reference constant temporal constraint weight."""
    weight = 1 - 4 * end * torch.ones(num_timesteps)
    return weight.cumprod(dim=0).to(timestep.device)[timestep]

rand_fix

rand_fix(
    batch_size: int,
    mask: Bool[Tensor, "batch elements"],
    *,
    ratio: float = 0.2,
) -> Bool[torch.Tensor, "batch elements"]

Sample the reference random completion mask.

Source code in models/lace/src/lace/training/losses.py
110
111
112
113
114
115
116
117
118
119
120
121
def rand_fix(
    batch_size: int,
    mask: Bool[torch.Tensor, "batch elements"],
    *,
    ratio: float = 0.2,
) -> Bool[torch.Tensor, "batch elements"]:
    """Sample the reference random completion mask."""
    n_elements = mask.shape[1]
    indices = (torch.rand(batch_size, n_elements) <= torch.rand(1).item() * ratio).to(
        mask.device
    )
    return indices * mask.bool()

lace_losses

lace_losses(
    *,
    layout_input: Float[Tensor, "batch elements channels"],
    model_output: Float[
        Tensor, "four_batch elements channels"
    ],
    noise: Float[Tensor, "four_batch elements channels"],
    reconstructed: Float[
        Tensor, "four_batch elements channels"
    ],
    bbox: Float[Tensor, "four_batch elements 4"],
    mask: Bool[Tensor, "four_batch elements"],
    timesteps: Int[Tensor, "four_batch"],
    num_classes: int,
) -> dict[str, Float[torch.Tensor, ""]]

Compute the reference-ordered diffusion, constraint, and reconstruction losses.

Source code in models/lace/src/lace/training/losses.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def lace_losses(
    *,
    layout_input: Float[torch.Tensor, "batch elements channels"],
    model_output: Float[torch.Tensor, "four_batch elements channels"],
    noise: Float[torch.Tensor, "four_batch elements channels"],
    reconstructed: Float[torch.Tensor, "four_batch elements channels"],
    bbox: Float[torch.Tensor, "four_batch elements 4"],
    mask: Bool[torch.Tensor, "four_batch elements"],
    timesteps: Int[torch.Tensor, "four_batch"],
    num_classes: int,
) -> dict[str, Float[torch.Tensor, ""]]:
    """Compute the reference-ordered diffusion, constraint, and reconstruction losses."""
    bbox_rep = reconstructed[:, :, num_classes:].clamp(-1, 1) / 2 + 0.5
    _, align_loss = layout_alignment(bbox_rep, mask)
    align_loss = 20 * align_loss
    piou = pairwise_iou(bbox_rep, mask.float())
    pdist = torch.cdist(bbox_rep[:, :, :2], bbox_rep[:, :, :2], p=2)
    overlap_loss = piou.mean(dim=(1, 2)) + (piou.ne(0) * torch.exp(-pdist)).mean(
        dim=(1, 2)
    )
    reconstruct_loss = F.mse_loss(
        torch.cat((layout_input,) * 4, dim=0)[:, :, num_classes:],
        reconstructed[:, :, num_classes:],
    )
    weight = constraint_temporal_weight(timesteps)
    constraint_loss = torch.mean((align_loss + overlap_loss) * weight)
    diffusion_loss = F.mse_loss(noise, model_output)
    total = diffusion_loss + constraint_loss + reconstruct_loss
    return {
        "diffusion_loss": diffusion_loss,
        "alignment_loss": align_loss.mean(),
        "overlap_loss": overlap_loss.mean(),
        "constraint_loss": constraint_loss,
        "reconstruct_loss": reconstruct_loss,
        "train_loss": total,
    }

layout_alignment

layout_alignment(
    bbox: Float[Tensor, "batch elements 4"],
    mask: Bool[Tensor, "batch elements"],
) -> tuple[
    Float[torch.Tensor, "batch"],
    Float[torch.Tensor, "batch"],
]

Return the reference local alignment score and normalized score.

Source code in models/lace/src/lace/training/losses.py
162
163
164
165
166
167
168
169
170
171
172
173
174
def layout_alignment(
    bbox: Float[torch.Tensor, "batch elements 4"],
    mask: Bool[torch.Tensor, "batch elements"],
) -> tuple[Float[torch.Tensor, "batch"], Float[torch.Tensor, "batch"]]:
    """Return the reference local alignment score and normalized score."""
    coords = alignment_matrix(bbox, mask)
    values = coords.min(-1).values.min(-1).values
    values.masked_fill_(values.eq(1.0), 0.0)
    values = -torch.log(1 - values)
    score = reduce(values, "b s -> b", "sum")
    normalized = score / reduce(mask, "b s -> b", "sum")
    normalized[torch.isnan(normalized)] = 0.0
    return score, normalized

seed

Deterministic controls for LACE training comparisons.

apply_lace_seed_mode

apply_lace_seed_mode(
    seed_mode: LaceSeedMode | str, *, seed: int = 42975
) -> None

Apply the requested regular or deterministic seed policy.

Source code in models/lace/src/lace/training/seed.py
11
12
13
14
15
16
17
18
19
20
21
def apply_lace_seed_mode(seed_mode: LaceSeedMode | str, *, seed: int = 42975) -> None:
    """Apply the requested regular or deterministic seed policy."""
    mode = LaceSeedMode(seed_mode)
    if mode is LaceSeedMode.default:
        torch.manual_seed(seed)
        if torch.cuda.is_available():
            torch.cuda.manual_seed_all(seed)

        return

    apply_determinism(DeterminismConfig(seed=seed))

trace

Opt-in training trace callback for package evidence runs.

LaceTrainingTraceCallback

Bases: Callback

Write optimizer-boundary records when a trace path is configured.

Source code in models/lace/src/lace/training/trace.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
class LaceTrainingTraceCallback(Callback):
    """Write optimizer-boundary records when a trace path is configured."""

    def __init__(self) -> None:
        """Initialize callback state without reading process configuration."""
        self.trace_path: Path | None = None
        self._batch_ids: list[str] = []
        self._record: dict[str, object] | None = None
        self._pre_clip_hashes: dict[str, str] = {}
        self._pre_clip_norm = 0.0
        self.records: list[dict[str, object]] = []

    def setup(
        self, trainer: Trainer, pl_module: LightningModule, stage: str | None = None
    ) -> None:
        """Read the opt-in trace path when Lightning attaches the callback."""
        del trainer, pl_module, stage
        trace_path = os.environ.get("LACE_TRAINING_TRACE_PATH")
        self.trace_path = Path(trace_path) if trace_path else None

    def on_train_batch_start(
        self,
        trainer: Trainer,
        pl_module: LightningModule,
        batch: Mapping[str, Shaped[torch.Tensor, "..."] | list[str]],
        batch_idx: int,
    ) -> None:
        """Capture the current batch identifiers before its optimizer step."""
        del trainer, batch_idx
        if self.trace_path is None:
            return

        self._finalize_record(pl_module)
        ids = batch.get("id", [])
        if not isinstance(ids, list) or not all(isinstance(item, str) for item in ids):
            raise TypeError("LACE training batches must carry string ids")

        self._batch_ids = cast(list[str], ids)

    def on_after_backward(self, trainer: Trainer, pl_module: LightningModule) -> None:
        """Capture gradients before Lightning applies clipping."""
        del trainer
        if self.trace_path is None:
            return

        parameters = dict(pl_module.named_parameters())
        self._pre_clip_hashes = _tensor_hashes(
            {
                name: parameter.grad.detach()
                for name, parameter in parameters.items()
                if parameter.grad is not None
            }
        )
        self._pre_clip_norm = gradient_norm(parameters)

    def on_before_optimizer_step(
        self,
        trainer: Trainer,
        pl_module: LightningModule,
        optimizer: torch.optim.Optimizer,
    ) -> None:
        """Capture the loss and optimizer settings before the update."""
        del trainer
        if self.trace_path is None:
            return

        trace = getattr(pl_module, "latest_step_trace", {})
        self._record = {
            "batch_ids": list(self._batch_ids),
            "loss": float(cast(torch.Tensor, trace["train_loss"]).item()),
            "gradient_hashes": self._pre_clip_hashes,
            "gradient_norm": self._pre_clip_norm,
            "learning_rate": optimizer.param_groups[0]["lr"],
        }

    def on_train_batch_end(
        self,
        trainer: Trainer,
        pl_module: LightningModule,
        outputs: Shaped[torch.Tensor, ""]
        | Mapping[str, Shaped[torch.Tensor, "..."] | list[str]]
        | None,
        batch: Mapping[str, Shaped[torch.Tensor, "..."] | list[str]],
        batch_idx: int,
    ) -> None:
        """Capture clipped gradients and post-step state."""
        del trainer, outputs, batch, batch_idx
        if self.trace_path is None:
            return

        if self._record is None:
            raise RuntimeError("LACE training trace missed an optimizer boundary")

        optimizer = cast(torch.optim.Optimizer, pl_module.optimizers())
        model = cast(nn.Module, getattr(pl_module, "model"))
        parameters = dict(pl_module.named_parameters())
        gradients = {
            name: parameter.grad.detach()
            for name, parameter in parameters.items()
            if parameter.grad is not None
        }
        optimizer_state = _optimizer_state(optimizer)
        self._record.update(
            {
                "clipped_gradient_hashes": _tensor_hashes(gradients),
                "clipped_gradient_norm": gradient_norm(parameters),
                "optimizer_state_hashes": _tensor_hashes(optimizer_state),
                "optimizer_state_l2_norm": _mapping_l2_norm(optimizer_state),
                "parameter_l2_norm": _mapping_l2_norm(model.state_dict()),
                "parameter_hashes": _tensor_hashes(model.state_dict()),
            }
        )

    def on_train_end(self, trainer: Trainer, pl_module: LightningModule) -> None:
        """Write records and the bounded trainer state to the trace path."""
        if self.trace_path is None:
            return

        self._finalize_record(pl_module)
        self.trace_path.parent.mkdir(parents=True, exist_ok=True)
        logger = trainer.logger
        logger_path = getattr(logger, "log_dir", None) if logger else None
        payload = {
            "records": self.records,
            "trainer_state": {
                "global_step": trainer.global_step,
                "current_epoch": trainer.current_epoch,
                "max_steps": trainer.max_steps,
                "num_training_batches": trainer.num_training_batches,
                "logger_path": str(logger_path) if logger_path else None,
            },
        }
        self.trace_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")

    def _finalize_record(self, pl_module: LightningModule) -> None:
        if self._record is None:
            return

        ema_state = cast(
            Mapping[str, torch.Tensor], getattr(pl_module, "latest_ema_state")
        )
        self.records.append({**self._record, "ema_hashes": _tensor_hashes(ema_state)})
        self._record = None
__init__
__init__() -> None

Initialize callback state without reading process configuration.

Source code in models/lace/src/lace/training/trace.py
62
63
64
65
66
67
68
69
def __init__(self) -> None:
    """Initialize callback state without reading process configuration."""
    self.trace_path: Path | None = None
    self._batch_ids: list[str] = []
    self._record: dict[str, object] | None = None
    self._pre_clip_hashes: dict[str, str] = {}
    self._pre_clip_norm = 0.0
    self.records: list[dict[str, object]] = []
setup
setup(
    trainer: Trainer,
    pl_module: LightningModule,
    stage: str | None = None,
) -> None

Read the opt-in trace path when Lightning attaches the callback.

Source code in models/lace/src/lace/training/trace.py
71
72
73
74
75
76
77
def setup(
    self, trainer: Trainer, pl_module: LightningModule, stage: str | None = None
) -> None:
    """Read the opt-in trace path when Lightning attaches the callback."""
    del trainer, pl_module, stage
    trace_path = os.environ.get("LACE_TRAINING_TRACE_PATH")
    self.trace_path = Path(trace_path) if trace_path else None
on_train_batch_start
on_train_batch_start(
    trainer: Trainer,
    pl_module: LightningModule,
    batch: Mapping[str, Shaped[Tensor, "..."] | list[str]],
    batch_idx: int,
) -> None

Capture the current batch identifiers before its optimizer step.

Source code in models/lace/src/lace/training/trace.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def on_train_batch_start(
    self,
    trainer: Trainer,
    pl_module: LightningModule,
    batch: Mapping[str, Shaped[torch.Tensor, "..."] | list[str]],
    batch_idx: int,
) -> None:
    """Capture the current batch identifiers before its optimizer step."""
    del trainer, batch_idx
    if self.trace_path is None:
        return

    self._finalize_record(pl_module)
    ids = batch.get("id", [])
    if not isinstance(ids, list) or not all(isinstance(item, str) for item in ids):
        raise TypeError("LACE training batches must carry string ids")

    self._batch_ids = cast(list[str], ids)
on_after_backward
on_after_backward(
    trainer: Trainer, pl_module: LightningModule
) -> None

Capture gradients before Lightning applies clipping.

Source code in models/lace/src/lace/training/trace.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def on_after_backward(self, trainer: Trainer, pl_module: LightningModule) -> None:
    """Capture gradients before Lightning applies clipping."""
    del trainer
    if self.trace_path is None:
        return

    parameters = dict(pl_module.named_parameters())
    self._pre_clip_hashes = _tensor_hashes(
        {
            name: parameter.grad.detach()
            for name, parameter in parameters.items()
            if parameter.grad is not None
        }
    )
    self._pre_clip_norm = gradient_norm(parameters)
on_before_optimizer_step
on_before_optimizer_step(
    trainer: Trainer,
    pl_module: LightningModule,
    optimizer: Optimizer,
) -> None

Capture the loss and optimizer settings before the update.

Source code in models/lace/src/lace/training/trace.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
def on_before_optimizer_step(
    self,
    trainer: Trainer,
    pl_module: LightningModule,
    optimizer: torch.optim.Optimizer,
) -> None:
    """Capture the loss and optimizer settings before the update."""
    del trainer
    if self.trace_path is None:
        return

    trace = getattr(pl_module, "latest_step_trace", {})
    self._record = {
        "batch_ids": list(self._batch_ids),
        "loss": float(cast(torch.Tensor, trace["train_loss"]).item()),
        "gradient_hashes": self._pre_clip_hashes,
        "gradient_norm": self._pre_clip_norm,
        "learning_rate": optimizer.param_groups[0]["lr"],
    }
on_train_batch_end
on_train_batch_end(
    trainer: Trainer,
    pl_module: LightningModule,
    outputs: Shaped[Tensor, ""]
    | Mapping[str, Shaped[Tensor, "..."] | list[str]]
    | None,
    batch: Mapping[str, Shaped[Tensor, "..."] | list[str]],
    batch_idx: int,
) -> None

Capture clipped gradients and post-step state.

Source code in models/lace/src/lace/training/trace.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def on_train_batch_end(
    self,
    trainer: Trainer,
    pl_module: LightningModule,
    outputs: Shaped[torch.Tensor, ""]
    | Mapping[str, Shaped[torch.Tensor, "..."] | list[str]]
    | None,
    batch: Mapping[str, Shaped[torch.Tensor, "..."] | list[str]],
    batch_idx: int,
) -> None:
    """Capture clipped gradients and post-step state."""
    del trainer, outputs, batch, batch_idx
    if self.trace_path is None:
        return

    if self._record is None:
        raise RuntimeError("LACE training trace missed an optimizer boundary")

    optimizer = cast(torch.optim.Optimizer, pl_module.optimizers())
    model = cast(nn.Module, getattr(pl_module, "model"))
    parameters = dict(pl_module.named_parameters())
    gradients = {
        name: parameter.grad.detach()
        for name, parameter in parameters.items()
        if parameter.grad is not None
    }
    optimizer_state = _optimizer_state(optimizer)
    self._record.update(
        {
            "clipped_gradient_hashes": _tensor_hashes(gradients),
            "clipped_gradient_norm": gradient_norm(parameters),
            "optimizer_state_hashes": _tensor_hashes(optimizer_state),
            "optimizer_state_l2_norm": _mapping_l2_norm(optimizer_state),
            "parameter_l2_norm": _mapping_l2_norm(model.state_dict()),
            "parameter_hashes": _tensor_hashes(model.state_dict()),
        }
    )
on_train_end
on_train_end(
    trainer: Trainer, pl_module: LightningModule
) -> None

Write records and the bounded trainer state to the trace path.

Source code in models/lace/src/lace/training/trace.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def on_train_end(self, trainer: Trainer, pl_module: LightningModule) -> None:
    """Write records and the bounded trainer state to the trace path."""
    if self.trace_path is None:
        return

    self._finalize_record(pl_module)
    self.trace_path.parent.mkdir(parents=True, exist_ok=True)
    logger = trainer.logger
    logger_path = getattr(logger, "log_dir", None) if logger else None
    payload = {
        "records": self.records,
        "trainer_state": {
            "global_step": trainer.global_step,
            "current_epoch": trainer.current_epoch,
            "max_steps": trainer.max_steps,
            "num_training_batches": trainer.num_training_batches,
            "logger_path": str(logger_path) if logger_path else None,
        },
    }
    self.trace_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")

gradient_norm

gradient_norm(parameters: Mapping[str, Parameter]) -> float

Return the shared float64 gradient norm for named parameters.

Source code in models/lace/src/lace/training/trace.py
22
23
24
25
26
27
28
29
30
31
def gradient_norm(parameters: Mapping[str, nn.Parameter]) -> float:
    """Return the shared float64 gradient norm for named parameters."""
    total = torch.zeros((), dtype=torch.float64)
    for name in sorted(parameters):
        parameter = parameters[name]
        if parameter.grad is not None:
            gradient = parameter.grad.detach().to(device="cpu", dtype=torch.float64)
            total += gradient.square().sum()

    return float(total.sqrt().item())