aloe-arch / conv_stem.py
rmaser's picture
Upload ALOE codebase
eb70c60 verified
Raw
History Blame Contribute Delete
4.84 kB
# Vendored B-cos conv stem for the self-contained ALOE HF package.
# Mirrors the BcosConverter ``ConvStem`` / legacy ``DINOv3ViTEmbeddings.patch_embeddings.stem``
# printout: ``BcosConv2d_unnormed β†’ GNLayerNormUncentered2dNoBias β†’ ReLU`` per stage
# (``GNLayerNormUncentered2dNoBias`` ≑ ``NoBias(GNLayerNormUncentered2d)`` here).
# Checkpoint keys (e.g. ``embeddings.conv_stem.stem.N.linear.weight``) load directly.
#
# Architecture (from BcosConverter / src/modules/conv_stem.py):
# in_channels β†’ [outc_0, outc_1, …, outc_K] conv layers
# Each layer: BcosUnnormedConv2d 3Γ—3, stride=2 if outc > prev, else stride=1
# + ``NoBias(GNLayerNormUncentered2d)`` β€” same as BcosConverter ``DEFAULT_NORM_LAYER``.
# + ReLU β€” same as ``DEFAULT_ACT_LAYER = nn.ReLU``.
# Final output has outc_K channels at reduced spatial resolution.
# The patch embedding (BcosUnnormedLinear) then handles the rest.
from __future__ import annotations
from typing import Type
import torch
import torch.nn as nn
from .configuration_aloe_vision import AloeVisionConfig
from .bcos_core import BcosUnnormedConv2d, DetachableReLU, NoBias, select_bcos_unnormed_conv2d
from .groupnorm_uncentered import GNLayerNormUncentered2d
def conv_stem_downsampling_factor(schedule: "list[int] | tuple[int, ...] | None", in_channels: int) -> int:
"""
Total spatial downsampling of a stem built from ``schedule``, without building it.
Mirrors :class:`AloeConvStem` exactly: a block gets ``stride=2`` when its output
channel count grows relative to the previous stage, else ``stride=1``.
Export code needs this to declare the true patch size (a conv stem sets the stride,
so the teacher's ``patch_size`` is meaningless for the student) before a model exists.
"""
factor = 1
prev = int(in_channels)
for out_ch in schedule or ():
if int(out_ch) > prev:
factor *= 2
prev = int(out_ch)
return factor
class _ConvBlock(nn.Module):
"""Single conv+norm+act block in the conv stem."""
def __init__(
self,
in_ch: int,
out_ch: int,
b: float,
stride: int,
*,
conv_cls: Type[BcosUnnormedConv2d] = BcosUnnormedConv2d,
) -> None:
super().__init__()
self.conv = conv_cls(in_ch, out_ch, kernel_size=3, stride=stride, padding=1, b=b)
self.norm = NoBias(GNLayerNormUncentered2d)(out_ch)
self.act = DetachableReLU()
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.act(self.norm(self.conv(x)))
class AloeConvStem(nn.Module):
"""
B-cos conv stem β€” mirrors the BcosConverter ``ConvStem`` architecture.
Transforms a 6-channel input (3-channel RGB expanded via channel doubling)
through a series of 3Γ—3 conv layers (stride=2 when channels grow, else 1)
down to a lower spatial resolution. The final spatial patches are then
handled by the regular patch-embedding ``BcosUnnormedLinear`` in
:class:`AloeVisionEmbeddings`.
Weight key format (for weight migration):
``embeddings.conv_stem.stem.{idx}.conv.linear.weight`` (conv weight)
``embeddings.conv_stem.stem.{idx}.norm.weight`` (affine scale; bias removed)
Built from ``config.aloe_add_conv_stem`` (channel schedule),
``config.aloe_in_channels``, ``config.aloe_b_conv``, and
``config.aloe_bcos_impl`` (v1 vs v2 conv scaling).
"""
def __init__(self, config: AloeVisionConfig) -> None:
super().__init__()
schedule = config.aloe_add_conv_stem
if not schedule:
raise ValueError("AloeConvStem requires config.aloe_add_conv_stem")
channel_schedule = list(schedule)
in_channels = config.aloe_in_channels
b = float(config.aloe_b_conv)
ConvCls = select_bcos_unnormed_conv2d(config)
blocks: list[nn.Module] = []
prev = in_channels
for out_ch in channel_schedule:
stride = 2 if out_ch > prev else 1
blocks.append(_ConvBlock(prev, out_ch, b=b, stride=stride, conv_cls=ConvCls))
prev = out_ch
self.stem = nn.Sequential(*blocks)
self.out_channels = prev
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""Return ``(B, out_channels, H', W')`` feature map."""
return self.stem(x)
@property
def downsampling_factor(self) -> int:
"""Total spatial downsampling factor accumulated by stride-2 layers."""
factor = 1
for block in self.stem:
if block.conv.linear.stride[0] == 2:
factor *= 2
return factor
@property
def logical_patch_size(self) -> int:
"""Input pixels per output cell β€” the stride an exported config should declare."""
return self.downsampling_factor