Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/chronos/chronos2/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,12 @@ def forward(
q=self.num_quantiles,
h=num_output_patches * self.chronos_config.output_patch_size,
)
quantile_preds = self.instance_norm.inverse(quantile_preds, loc_scale)
# Preserve precision when unscaling large-magnitude forecasts.
quantile_preds = self.instance_norm.inverse(
quantile_preds,
loc_scale,
output_dtype=torch.float32,
)
quantile_preds = rearrange(
quantile_preds,
"b (q h) -> b q h",
Expand Down
10 changes: 8 additions & 2 deletions src/chronos/chronos_bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ def forward(

return scaled_x.to(orig_dtype), (loc, scale)

def inverse(self, x: torch.Tensor, loc_scale: tuple[torch.Tensor, torch.Tensor]) -> torch.Tensor:
def inverse(
self,
x: torch.Tensor,
loc_scale: tuple[torch.Tensor, torch.Tensor],
output_dtype: torch.dtype | None = None,
) -> torch.Tensor:
orig_dtype = x.dtype
x = x.to(torch.float32)
loc, scale = loc_scale
Expand All @@ -131,7 +136,7 @@ def inverse(self, x: torch.Tensor, loc_scale: tuple[torch.Tensor, torch.Tensor])

x = x * scale + loc

return x.to(orig_dtype)
return x.to(orig_dtype if output_dtype is None else output_dtype)

@abdulfatir abdulfatir Sep 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there ever a usecase where we actually want the inverse to be cast back to orig_dtype? I am wondering if we should just always return fp32. Simply return x.



class ResidualBlock(nn.Module):
Expand Down Expand Up @@ -385,6 +390,7 @@ def forward(
quantile_preds = self.instance_norm.inverse(
quantile_preds.view(batch_size, -1),
loc_scale,
output_dtype=torch.float32,
).view(*quantile_preds_shape)

return ChronosBoltOutput(
Expand Down
8 changes: 8 additions & 0 deletions test/test_chronos2.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ def test_chronos2_encoder_accepts_config_without_is_decoder():
assert len(encoder.block) == config.num_layers


def test_when_chronos2_model_uses_bfloat16_then_unscaled_predictions_use_float32(pipeline):
pipeline.model.to(torch.bfloat16)

output = pipeline.model(context=torch.rand(1, 16), num_output_patches=1)

assert output.quantile_preds.dtype == torch.float32


def test_base_chronos2_pipeline_loads_from_s3():
BaseChronosPipeline.from_pretrained("s3://autogluon/chronos-2", device_map="cpu")

Expand Down
27 changes: 27 additions & 0 deletions test/test_chronos_bolt.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ def pipeline() -> ChronosBoltPipeline:
return BaseChronosPipeline.from_pretrained(DUMMY_MODEL_PATH, device_map="cpu")


def test_when_chronos_bolt_model_uses_bfloat16_then_unscaled_predictions_use_float32():
pipeline = ChronosBoltPipeline.from_pretrained(DUMMY_MODEL_PATH, device_map="cpu", torch_dtype=torch.bfloat16)

output = pipeline.model(context=torch.rand(1, 16))

assert output.quantile_preds.dtype == torch.float32


def test_base_chronos_pipeline_loads_from_huggingface():
BaseChronosPipeline.from_pretrained("amazon/chronos-bolt-tiny", device_map="cpu")

Expand Down Expand Up @@ -355,6 +363,25 @@ def test_when_instancenorm_applied_and_reversed_then_output_correct():
assert torch.allclose(output, input_)


def test_when_instancenorm_reversed_to_float32_then_precision_is_preserved():
inorm = InstanceNorm()
normalized = torch.tensor([[0.125]], dtype=torch.bfloat16)
loc = torch.tensor([[1_000_000.0]], dtype=torch.float32)
scale = torch.tensor([[100.0]], dtype=torch.float32)

output = inorm.inverse(
normalized,
(loc, scale),
output_dtype=torch.float32,
)

assert output.dtype == torch.float32
torch.testing.assert_close(
output,
torch.tensor([[1_000_012.5]], dtype=torch.float32),
)


@pytest.mark.parametrize("task_kwargs", [{}, {"eval_metric": "WQL", "quantile_levels": [0.1, 0.2]}])
def test_pipeline_can_evaluate_on_dummy_fev_task(task_kwargs):
pipeline = BaseChronosPipeline.from_pretrained(
Expand Down
Loading