Adding partial convolution option in Samudra#1348
Conversation
There was a problem hiding this comment.
Thank you for adding this! These are very promising results, so we should definitely try to get this merged without too much delay.
For the PR description, please follow https://github.com/ai2cm/ace/blob/main/.github/pull_request_template.md. Plots are good for PR conversation, but not so good in the description which becomes the body of the commit message once the PR is squashed-and-merged. Also, I recommend a more formal style for the description, e.g. avoiding first person ("Adds xyz feature to" vs "I'm adding this feature because"). The exact content of your current description would be good to copy paste into a comment in the PR discussion before making edits.
Asking @elynnwu if she can help review.
| return x.permute(0, 3, 1, 2).contiguous() | ||
|
|
||
|
|
||
| class PartialConv2d(torch.nn.Module): |
There was a problem hiding this comment.
What is the provenance of this class? The idea seems to come from https://github.com/NVIDIA/partialconv/blob/master/models/partialconv2d.py but the implementation looks pretty different.
Assuming this is related to the NVIDIA project, please include a link to https://arxiv.org/abs/1804.07723 in the docstring.
Or if there is some other provenance, we give attribution somehow.
| n_in_channels: int, | ||
| n_out_channels: int, | ||
| dataset_info: DatasetInfo, | ||
| in_names: list[str] | None = None, |
There was a problem hiding this comment.
Hmm, I think we can avoid this change. We have dataset_info here which has an attribute spatial_mask_provider, so ideally we should be able to construct the mask tensor with the right network channel order. The hard part is ensuring that the order is the same as the packer order.
@mcgibbon any thoughts?
| x = x.permute(0, 2, 3, 1) | ||
| x = self.norm(x) | ||
| return x.permute(0, 3, 1, 2).contiguous() |
There was a problem hiding this comment.
We should be able to avoid the permute here, which can be quite slow. See e.g.
There was a problem hiding this comment.
@mcgibbon how do you feel about models outside of core/models/conditional_sfno reusing some pieces? Or would it be better to copy-paste the implementation?
| norm_kwargs=self.norm_kwargs, | ||
| upscale_factor=self.upscale_factor, | ||
| checkpoint_strategy=self.checkpoint_strategy, | ||
| if i == 0: |
There was a problem hiding this comment.
Why is it that only the first layer uses partial convolutions? Do the masks not flow through the layers?
There was a problem hiding this comment.
This seems somewhat incongruous with having MaskedAvgPool2d for every down layer, but maybe there's a good reason to do it this way.
| output = self.skip(residual_input) + y | ||
|
|
||
| # Latent features are only valid over actual ocean columns. | ||
| return output * ocean_column_mask |
There was a problem hiding this comment.
If I understand correctly, ocean_column_mask is just the surface mask. The invalid area of the surface mask (i.e., land) is always a subset of the invalid area of deeper layers, so does output * ocean_column_mask actually change the output in any way?
| from fme.core.dataset_info import DatasetInfo | ||
|
|
||
|
|
||
| class Samudra(torch.nn.Module): |
There was a problem hiding this comment.
With this amount of changes to an existing model, I'm tempted to create a new class (e.g., MaskAwareSamudra) rather than risk introducing potentially subtle changes to existing checkpoints.
@elynnwu thoughts?
There was a problem hiding this comment.
Yes, it seems like there're a lot of changes here that might be worth splitting it off. The one concern for doing that is we'll eventually diverge too much.
I'm wondering if we should do a bit more ablations to nail down exactly what is causing this noisy behavior. I've previously also had this issue that eventually (mostly) went away due to the way masks are defined and training longer.
There was a problem hiding this comment.
If possible, maybe we should try a these ablations:
- Input masking only. Multiply the input by the per-channel wet mask (
x * channel_mask) before the existing layer 0. No architecture change at all — layer 0 stays a plainConvNeXtBlock, pooling stays plainAvgPool - Input masking + mask-as-feature. Same as above, plus concatenate the per-channel mask as extra input channels (so layer 0's
in_channelsdoubles, but it's still an ordinary convolution — no partial-conv renormalization, no masked pooling)
This will tell us if the effect is coming from masking the input, mask-conditioning, or the full partial-conv in this PR.
There was a problem hiding this comment.
Thanks @elynnwu. Just to clarify on 1. Is this different to what the original code is doing already? I was under the impression that inputs/outputs are already being masked before/after each forward pass?
| convblock.append( | ||
| torch.nn.LayerNorm(in_channels * upscale_factor, **self.norm_kwargs) | ||
| ) | ||
| convblock.append(ChannelLayerNorm(in_channels * upscale_factor)) |
There was a problem hiding this comment.
This changes state-dict keys (convblock.N.weight → convblock.N.norm.weight), which breaks loading any existing norm="layer" Samudra checkpoint. This also drops **self.norm_kwargs, is this intended? Our baseline Samudra uses instance norm, so it may not be as critical.
There was a problem hiding this comment.
Good point on the backwards compat. I think the code crashed when i tried layer norm with the initial torch.nn.LayerNorm(in_channels * upscale_factor, **self.norm_kwargs) strategy and it seemed I needed the permute step. Given we're just using instance norm anyway, maybe its best if I restore this to its original implementation?
| from fme.core.dataset_info import DatasetInfo | ||
|
|
||
|
|
||
| class Samudra(torch.nn.Module): |
There was a problem hiding this comment.
Yes, it seems like there're a lot of changes here that might be worth splitting it off. The one concern for doing that is we'll eventually diverge too much.
I'm wondering if we should do a bit more ablations to nail down exactly what is causing this noisy behavior. I've previously also had this issue that eventually (mostly) went away due to the way masks are defined and training longer.
| from fme.core.dataset_info import DatasetInfo | ||
|
|
||
|
|
||
| class Samudra(torch.nn.Module): |
There was a problem hiding this comment.
If possible, maybe we should try a these ablations:
- Input masking only. Multiply the input by the per-channel wet mask (
x * channel_mask) before the existing layer 0. No architecture change at all — layer 0 stays a plainConvNeXtBlock, pooling stays plainAvgPool - Input masking + mask-as-feature. Same as above, plus concatenate the per-channel mask as extra input channels (so layer 0's
in_channelsdoubles, but it's still an ordinary convolution — no partial-conv renormalization, no masked pooling)
This will tell us if the effect is coming from masking the input, mask-conditioning, or the full partial-conv in this PR.
|
Thanks @jpdunc23 and @elynnwu for looking through all of this! I'm pasting my original PR description with the figures here, following recommendation from @jpdunc23. I will look into your comments and ablation suggestions asap! "In my training of uncoupled Samudra (using OM4 data), I was finding that my resultant bias maps were quite noisy. The noise was most prevalent near the surface, but could be seen at all depths. I was wondering if this noise was being introduced by convolutions and average pooling being computed across ocean and land grid cells at all depths, which then causes noise to mix across the channels. I therefore added a partial convolution operation to Samudra, which can be turned on in the config by To add the partial convolution functionality, I had to edit the
|


This PR adds a partial convolution option to the Samudra model, motivated by the need to reduce grid-scale noise in Samudra rollouts (see comment below). The core change are to:
fme.ace.models.ocean.m2lines.samudrafme.ace.models.ocean.m2lines.layersfme.ace.registry.m2linesThe partial convolution option has also been included in
fme.ace.models.ocean.m2lines.test_samudra.The PR relies on
in_namesin theModuleConfigandModuleSelectorbuild methods. This was needed so that masks can be aligned with individual channels in the input tensor. Therefore, the PR also includes updates to:fme.ace.models.registryfme.core.models.mlpfme.core.registryfme.core.step.single_module