Skip to content

Commit f00ad50

Browse files
committed
feat(timesfm3): assemble model layers with fixed ReLU
1 parent c3b5614 commit f00ad50

2 files changed

Lines changed: 230 additions & 0 deletions

File tree

‎sdm/models/timesfm3/core.py‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
16+
# SPDX-License-Identifier: Apache-2.0
17+
18+
19+
from __future__ import annotations
20+
21+
from collections.abc import Sequence
22+
from typing import Any
23+
24+
import torch
25+
from torch.nn import Linear
26+
27+
from sdm.models.timesfm3.dense import ResidualBlock
28+
from sdm.models.timesfm3.transformer import StackedMixingTransformer
29+
30+
31+
class _TimesFM3Model(torch.nn.Module):
32+
"""Implement the Google TimesFM-3 full-sequence model.
33+
34+
Args:
35+
input_patch_len: Number of time steps in each input patch.
36+
output_patch_len: Number of time steps predicted from each patch.
37+
quantiles: Quantiles predicted by the output head.
38+
residual_block_config: Pre-transformer residual-block configuration.
39+
transformer_config: Transformer-stack configuration.
40+
use_variate_attention: Whether to attend across variates.
41+
value_clip: Absolute bound applied to inputs and predictions.
42+
use_stitching: Whether decoding will stitch overlapping predictions.
43+
use_linear_detrending: Whether decoding will remove linear trends.
44+
linear_detrending_threshold: Ratio controlling linear detrending.
45+
use_iterative_cpm_revin: Whether to refine RevIN statistics for
46+
Contiguous Patch Masking positions.
47+
use_frozen_running_stats: Whether decoding will freeze statistics at
48+
the context boundary.
49+
device: Device on which to create parameters and buffers.
50+
dtype: Data type of parameters.
51+
"""
52+
53+
def __init__(
54+
self,
55+
input_patch_len: int = 32,
56+
output_patch_len: int = 64,
57+
quantiles: Sequence[float] | None = None,
58+
residual_block_config: dict[str, Any] | None = None,
59+
transformer_config: dict[str, Any] | None = None,
60+
use_variate_attention: bool = True,
61+
value_clip: float = 1e20,
62+
use_stitching: bool = True,
63+
use_linear_detrending: bool = True,
64+
linear_detrending_threshold: float = 0.5,
65+
use_iterative_cpm_revin: bool = True,
66+
use_frozen_running_stats: bool = False,
67+
device: torch.device | str | None = None,
68+
dtype: torch.dtype | None = None,
69+
) -> None:
70+
super().__init__()
71+
if quantiles is None:
72+
quantiles = [index / 10 for index in range(1, 10)]
73+
if residual_block_config is None:
74+
residual_args: dict[str, Any] = {
75+
"hidden_dims": 1280,
76+
"output_dims": 1280,
77+
"use_bias": False,
78+
}
79+
else:
80+
residual_args = dict(residual_block_config)
81+
82+
if transformer_config is None:
83+
stack_args: dict[str, Any] = {
84+
"num_layers": 20,
85+
"transformer": {
86+
"model_dims": 1280,
87+
"hidden_dims": 1280,
88+
"num_heads": 16,
89+
"qk_norm": "rms",
90+
"use_rope_seq": True,
91+
"use_rope_var": False,
92+
"use_bias": False,
93+
},
94+
}
95+
else:
96+
stack_args = dict(transformer_config)
97+
transformer_args: dict[str, Any] = dict(stack_args.pop("transformer"))
98+
99+
if output_patch_len % input_patch_len != 0:
100+
raise ValueError(
101+
f"Output patch length {output_patch_len} must be a multiple "
102+
f"of input patch length {input_patch_len}."
103+
)
104+
if residual_args["output_dims"] != transformer_args["model_dims"]:
105+
raise ValueError(
106+
"Residual-block output dimensions must match transformer "
107+
"model dimensions."
108+
)
109+
if use_stitching and output_patch_len <= input_patch_len:
110+
raise ValueError(
111+
"Stitching requires output_patch_len > input_patch_len."
112+
)
113+
114+
self.input_patch_len = input_patch_len
115+
self.output_patch_len = output_patch_len
116+
self.quantiles = list(quantiles)
117+
self.num_quantiles = len(self.quantiles)
118+
self.rolls = output_patch_len // input_patch_len
119+
self.use_variate_attention = use_variate_attention
120+
self.value_clip = value_clip
121+
self.use_stitching = use_stitching
122+
self.use_linear_detrending = use_linear_detrending
123+
self.linear_detrending_threshold = linear_detrending_threshold
124+
self.use_iterative_cpm_revin = use_iterative_cpm_revin
125+
self.use_frozen_running_stats = use_frozen_running_stats
126+
127+
if use_stitching:
128+
self._stitching_extract_len = min(
129+
2 * input_patch_len,
130+
output_patch_len,
131+
)
132+
133+
self.pre_transformer_resblock = ResidualBlock(
134+
input_dims=2 * (input_patch_len + output_patch_len),
135+
device=device,
136+
dtype=dtype,
137+
**residual_args,
138+
)
139+
self.transformer_stack = StackedMixingTransformer(
140+
use_variate_attention=use_variate_attention,
141+
device=device,
142+
dtype=dtype,
143+
**stack_args,
144+
**transformer_args,
145+
)
146+
self.output_head = Linear(
147+
transformer_args["model_dims"],
148+
output_patch_len * self.num_quantiles,
149+
bias=True,
150+
device=device,
151+
dtype=dtype,
152+
)

‎test/models/timesfm3/test_core.py‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from typing import Any
5+
6+
import torch
7+
8+
from sdm.models.timesfm3.core import _TimesFM3Model
9+
from sdm.testing import withCUDA
10+
11+
12+
def _residual_config(output_dims: int = 8) -> dict[str, Any]:
13+
return {
14+
"hidden_dims": 8,
15+
"output_dims": output_dims,
16+
"use_bias": False,
17+
}
18+
19+
20+
def _transformer_config(model_dims: int = 8) -> dict[str, Any]:
21+
return {
22+
"num_layers": 2,
23+
"transformer": {
24+
"model_dims": model_dims,
25+
"hidden_dims": 12,
26+
"num_heads": 2,
27+
"qk_norm": "rms",
28+
"use_rope_seq": True,
29+
"use_rope_var": False,
30+
"use_bias": False,
31+
},
32+
}
33+
34+
35+
def _internal_model(
36+
device: torch.device | str,
37+
dtype: torch.dtype | None = None,
38+
*,
39+
use_iterative_cpm_revin: bool = True,
40+
use_linear_detrending: bool = True,
41+
) -> _TimesFM3Model:
42+
return _TimesFM3Model(
43+
input_patch_len=2,
44+
output_patch_len=4,
45+
quantiles=[0.1, 0.5, 0.9],
46+
residual_block_config=_residual_config(),
47+
transformer_config=_transformer_config(),
48+
use_iterative_cpm_revin=use_iterative_cpm_revin,
49+
use_linear_detrending=use_linear_detrending,
50+
device=device,
51+
dtype=dtype,
52+
)
53+
54+
55+
@withCUDA
56+
def test_internal_model_assembles_checkpoint_parameters(
57+
device: torch.device,
58+
) -> None:
59+
source = _internal_model(device)
60+
state = source.state_dict()
61+
assert state["pre_transformer_resblock.hidden_layer.weight"].shape == (
62+
8,
63+
12,
64+
)
65+
assert state[
66+
"transformer_stack.layers.0.seq_attn.qkv_lin.weight"
67+
].shape == (
68+
24,
69+
8,
70+
)
71+
assert state["output_head.weight"].shape == (12, 8)
72+
73+
loaded = _internal_model("meta")
74+
loaded.load_state_dict(state, strict=True, assign=True)
75+
assert all(parameter.device == device for parameter in loaded.parameters())
76+
torch.testing.assert_close(
77+
loaded.output_head.weight, source.output_head.weight
78+
)

0 commit comments

Comments
 (0)