Skip to content

[AINode] Add TiRex-2 as a built-in forecasting model - #19273

Open
kotselidis wants to merge 9 commits into
apache:masterfrom
kotselidis:ainode-tirex2
Open

kotselidis wants to merge 9 commits into
apache:masterfrom
kotselidis:ainode-tirex2

Conversation

@kotselidis

@kotselidis kotselidis commented Nov 13, 2026 •

Copy link
Copy Markdown

Description

This PR adds TiRex-2 (NXAI) to AINode's built-in forecasting models, next to timer_xl, sundial, chronos2, moirai2, toto and moment. TiRex-2 is an xLSTM time series foundation model with about 83M parameters. It forecasts nine quantiles (0.1 to 0.9), up to 320 steps ahead, for univariate and multivariate series, and it can use past-only and future-known covariates. The model is registered as tirex2. AINode downloads its weights from the Hugging Face repository NX-AI/TiRex-2 at startup, as it does for the other built-in models.

Usage

SELECT * FROM FORECAST(
  model_id => 'tirex2',
  targets => (SELECT time, s FROM db.t WHERE device = 'd1' ORDER BY time DESC LIMIT 1024) ORDER BY time,
  output_length => 24)

Once the weights are downloaded, SHOW MODELS lists tirex2 with model type tirex2, category builtin and state active.

Example: the input is 1024 points of 50 + 10·sin(2πt/24) + 0.01·t. The request is a TForecastReq with outputLength = 24, sent to AINode's inference manager on CPU with the published checkpoint. The response is 24 values (the median forecast). The first six:

step forecast true value
1 51.636 51.580
2 50.628 50.591
3 50.294 50.260
4 50.641 50.611
5 51.603 51.620
6 53.277 53.219

The mean absolute error is 0.112 over 24 steps and 0.126 over 96 steps. If output_length is above the model's horizon, the request fails with a clear message: 1599: TiRex-2 forecasts at most 320 steps, but output_length is 400.

Model code

iotdb/ainode/core/model/tirex2/ contains the inference model from NX-AI/tirex-2 0.5.0 (Apache License 2.0), in the same way as chronos2/ contains the code from chronos-forecasting. The model code is unchanged except for these points:

  • Imports use the iotdb.ainode.core.model.tirex2 package, and files carry the ASF header followed by NXAI's copyright notice.
  • The recurrent kernels follow the device of their inputs. Upstream picks the kernels when the model is built. AINode builds the model on CPU and then moves it to the device it is loaded on, so the choice now happens at run time. CUDA inputs use the Triton sLSTM and mLSTM kernels when Triton is available and the GPU has compute capability 8.0 (Ampere) or newer, since the kernels multiply bfloat16 operands. Other inputs, and older GPUs such as Volta and Turing, use the PyTorch kernels.
  • Triton counts as available only if its kernel modules import. Triton compiles a kernel from its Python source. In a packaged AINode binary, Triton may be bundled without that source; the model then falls back to the PyTorch kernels instead of failing.
  • The activation named in config.json must be a torch.nn module, such as torch.nn.SiLU. Upstream imports any dotted path from the config.
  • A checkpoint in the per-gate layout must contain the mLSTM input and forget gate biases. Upstream fills a missing bias with zeros.
  • The PyTorch sLSTM kernel converts the recurrent weight once per sequence instead of at every step.
  • Code that AINode does not need is left out: the high-level ForecastModel API, dataframe/GluonTS/fev adapters, plotting and the demo. The narwhals dependency goes with them.

TiRex-2's own loader replays the forward pass from CUDA graphs. This PR leaves CUDA graphs off (use_cuda_graphs = False), so every forecast runs eagerly.

Upstream compiles its residual blocks with torch.compile. This PR runs them eagerly. Inductor needs a C++ compiler and the Python headers at run time, and the packaged AINode executable does not ship the headers. Eager and compiled forecasts are bit-identical on CPU. FlexAttention, which upstream also compiles on CUDA, is off in the published configuration.

AINode integration

  • modeling_tirex2.TiRex2ForPrediction is the model, loaded through PyTorchModelHubMixin (hub_mixin_cls in ModelInfo). AINode already supports this loading path. The transformers PreTrainedModel path is not used, because the published model.safetensors stores one weight per gate. TiRex-2's load_state_dict hooks fuse those weights into the layout of its recurrent layers, and transformers' loader bypasses the hooks. The weights load with strict=True, so a checkpoint with a missing or extra weight is rejected.
  • pipeline_tirex2.TiRex2Pipeline turns each input into one TiRex-2 series. The targets of an input, shape (target_count, input_length), are forecast jointly. A covariate that appears only in past_covariates becomes a past covariate. A covariate that also appears in future_covariates becomes a known covariate, built from its past and future values. All inputs of a request run in one batch. The pipeline returns the 0.5 quantile.
  • output_length must be between 1 and 320. TiRex-2 has no autoregressive mode, so longer horizons are rejected instead of truncated. ModelInfo gets an optional max_output_length, set to 320 for tirex2. InferenceManager checks output_length against it, together with AINode's own limit, before a request is dispatched to an inference pool. A rejected request therefore never reaches the pool, and the pool keeps serving. Models without the field keep AINode's limit.

Dependencies

There are no new dependencies. The model needs PyTorch, NumPy, safetensors and huggingface_hub, which AINode already pins. On Linux, the CUDA wheels of PyTorch include Triton. TiRex-2 0.5.0 replaced its earlier flashrnn and mlstm_kernels dependencies, and their CUDA toolkit requirement, with these Triton kernels. pyproject.toml, ainode.spec and ainode.xml are unchanged. ainode.spec already collects every module under iotdb.ainode.core.

Licences

TiRex-2's code and its published weights are both under the Apache License 2.0 (code licence, weights licence). The weights are downloaded at run time and are not part of the distribution. This PR adds TiRex-2 to LICENSE, NOTICE and AINode's NOTICE-binary, with the copyright line from TiRex-2's NOTICE file.

NXAI's intended-use notice says the release is not intended for high-risk uses under the EU AI Act. The notice states that it does not change the licence.

Tests

  • tests/test_tirex2.py (unittest, CPU only, no downloads) uses a two-block TiRex-2 with random weights. The tests:
    • save it in the published checkpoint's per-gate layout and check that it reloads with identical weights;
    • check that a checkpoint with a missing weight, or a missing input or forget gate bias, is rejected;
    • load it through AINode's built-in loader and the TiRex2Pipeline, and check the output shapes for 1-D and 2-D targets and that the result is the model's median;
    • check how covariates are assembled, that output_length above the horizon is rejected, and that the activation lookup is restricted;
    • check that CPU inputs and GPUs below compute capability 8.0 use the PyTorch kernels, and that a forecast runs without torch.compile.
  • tests/test_builtin_model_limits.py (unittest) checks that the model limit lowers AINode's limit. With tirex2 loaded in a pool, a 400-step forecast is rejected without being dispatched to the pool, and the next 24-step forecast is served.
  • The tirex2 entry is added to AINodeTestUtils.BUILTIN_LTSM_MAP, so AINodeSharedClusterIT lists, loads and forecasts with it on CPU and GPU, like the other built-in foundation models. forecastTableFunctionErrorTest checks each model's own output_length limit: 320 for tirex2 and 2880 for the others.
  • With the published checkpoint on CPU, the forecasts are bit-identical to the unmodified TiRex-2 0.5.0 code for a univariate series, a multivariate series with past and known covariates, and a batch of series of different lengths.

This PR has:

  • been self-reviewed.
  • added or updated version, license, or notice information
  • added comments explaining the "why" and the intent of the code wherever would not be obvious
    for an unfamiliar reader.
  • added unit tests or modified existing tests to cover new code paths, ensuring the threshold
    for code coverage.
  • added integration tests.

Key changed/added classes (or packages if there are too many classes) in this PR
  • iotdb/ainode/core/model/tirex2/ (new): the TiRex-2 model, TiRex2ForPrediction, TiRex2Pipeline
  • iotdb/ainode/core/model/model_info.py: ModelInfo.max_output_length; tirex2 entry in BUILTIN_HF_TRANSFORMERS_MODEL_MAP
  • iotdb/ainode/core/manager/inference_manager.py: output_length checked against the model's limit before pool dispatch
  • iotdb-core/ainode/tests/test_tirex2.py, tests/test_builtin_model_limits.py (new)
  • integration-test/.../ainode/utils/AINodeTestUtils.java: tirex2 in BUILTIN_LTSM_MAP, getMaxOutputLength
  • integration-test/.../ainode/it/AINodeSharedClusterIT.java: per-model limit in forecastTableFunctionErrorTest
  • LICENSE, NOTICE, iotdb-core/ainode/src/assembly/resources/NOTICE-binary

Copilot AI left a comment

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.

🟡 Changes recommended

An unsupported forecast horizon can terminate the inference worker and leave callers waiting indefinitely, while Volta and Turing GPUs are incorrectly routed to incompatible Triton kernels.

4 open issues
What changed in this PR

Adds TiRex-2 to AINode’s built-in forecasting models, supporting joint multivariate forecasts and optional covariates with median predictions up to 320 steps ahead.

Changes:

  • Registers tirex2 and integrates checkpoint loading and forecasting pipelines.
  • Vendors the model components and device-aware PyTorch/Triton kernels.
  • Adds unit and cluster-test coverage, plus source and binary licensing notices.
File Description
NOTICE Adds TiRex-2 attribution.
LICENSE Records licensing for vendored code.
iotdb-core/​ainode/​tests/​test_tirex2.py Tests checkpoint loading, forecasts, covariates, and validation.
iotdb-core/​ainode/​src/​assembly/​resources/​NOTICE-binary Adds binary-distribution attribution.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​types.py Defines series and covariate inputs.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​pipeline_tirex2.py Adapts AINode inputs and returns median forecasts.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​modeling_tirex2.py Loads checkpoints through state-dictionary conversion hooks.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​model.py Implements the forecasting model and prediction flow.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​cuda_graph.py Provides optional CUDA graph replay support.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​variate_mixing_block.py Combines temporal and cross-variable processing.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​slstm_block.py Implements sLSTM layers and kernel dispatch.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​scaler.py Normalizes inputs and restores forecast scale.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​residual_block.py Adds compiled residual feed-forward blocks.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​postprocessor.py Handles batching, trend differencing, and output reconstruction.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​patch_tokenizer.py Converts series into patches and reconstructs outputs.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​norm.py Provides normalization layers.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​mlstm_block.py Implements mLSTM layers and checkpoint conversion.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​kernel/​slstm_triton.py Adds the CUDA sLSTM kernel.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​kernel/​slstm_torch.py Adds the PyTorch sLSTM fallback.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​kernel/​mlstm_triton.py Adds CUDA chunkwise mLSTM kernels.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​kernel/​mlstm_native.py Adds the native PyTorch mLSTM implementation.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​kernel/​__init__.py Detects Triton availability for kernel selection.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​feedforward.py Provides feed-forward layers.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​bi_xlstm.py Combines forward and reverse recurrent processing.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​attention_block.py Implements grouped attention across variables.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​__init__.py Initializes the component package.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​__init__.py Initializes the TiRex-2 package.
iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​model_info.py Registers the built-in model and loading classes.
integration-test/​src/​test/​java/​org/​apache/​iotdb/​ainode/​utils/​AINodeTestUtils.java Includes TiRex-2 in shared built-in model tests.

Review effort: Balanced

Comment on lines +67 to +68
if output_length > self.max_output_length:
raise ValueError(

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.

Confirmed with a bounded reproduction using the actual pool execution loop, batcher, and TiRex-2 pipeline validation (only the model was stubbed with future_len=320): a 400-step request terminates the execution thread with ValueError, leaves the finished queue empty, and leaves a subsequent valid request queued. Please add coverage through the loaded-model pool path, including a valid request after the rejection; the current test only verifies that the direct pipeline call raises.

@CRZbulabula CRZbulabula left a comment

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.

Reviewed 87af3d4. There is a blocking packaged-runtime failure below, confirmed by the current AINode CI logs. I also independently reproduced the invalid-horizon worker failure and acceptance of missing gate biases reported in the existing threads; I will add the reproduction details there.

The seven new TiRex-2 unit tests pass in a local Python 3.11 / PyTorch 2.11.0 environment. This is a source-level sanity check, not validation of the pinned PyTorch 2.14.1, GPU kernels, or packaged executable. The current AINode integration job fails, and the TODO check also flags the new TODO in slstm_torch.py.

from iotdb.ainode.core.model.tirex2.component.norm import RMSNorm


@torch.compile

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.

[P1] Make compilation optional for the packaged AINode runtime

This unconditional torch.compile makes ordinary CPU inference fail in the shipped PyInstaller executable. In the current AINode job, forecastTableFunctionTest fails with CppCompileError: fatal error: Python.h: No such file or directory: Inductor looks under the frozen executable's _internal/include/python3.11, which is not populated by the current packaging. The uploaded AINode log also shows the same error escaping _requests_execute_loop during the loaded-model test, killing the execution thread and leaving the query to time out. This is an actual runtime failure, beyond first-request compilation overhead. Please make compilation opt-in or provide a supported packaged compilation path with an eager fallback, and verify both direct and pooled forecasts using the built executable.

Copilot AI balanced review requested due to automatic review settings November 14, 2026 15:08

Copilot AI left a comment

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.

🔵 Needs a closer look

The Triton sLSTM and mLSTM kernels lack numerical parity tests against their reference implementations, leaving recurrence, state propagation, and padded-tail correctness unverified by CPU forecasts and shape-only GPU checks.

3 open issues
2 previously missed issues

In code that hasn't changed since last review

Medium severity Add numerical parity tests for the Triton mLSTM implementation

iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​kernel/​mlstm_triton.py:278

The new Triton mLSTM implementation is not checked numerically against mlstm_chunkwise_native. All model unit forecasts use CPU, and the GPU cluster smoke test verifies only shape/count, so errors in chunk-state propagation or padded-tail handling can pass the existing assertions. Please add CUDA-gated, download-free parity cases spanning a chunk boundary and a non-multiple-of-64 sequence length, with tolerances appropriate to the kernel's dtype casts.

Medium severity Add numerical parity tests for the sLSTM Triton recurrence

iotdb-core/​ainode/​iotdb/​ainode/​core/​model/​tirex2/​component/​kernel/​slstm_triton.py:279

The sLSTM Triton recurrence has no numerical regression coverage: test_tirex2.py runs forecasts only on CPU and mocks CUDA dispatch, while the cluster smoke test checks result shape/count rather than values. In particular, the recurrent-weight transpose and gate/state layout are not checked against the PyTorch implementation. Please add a CUDA-gated, download-free comparison of outputs and final states using nonzero recurrent weights, nonzero initial states, and multiple time steps.

1 resolved since last review

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 14, 2026 15:13

Copilot AI left a comment

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.

🟡 Changes recommended

TiRex-2 causes a forecast-limit test assertion to fail, while zero-length requests can terminate the inference loop and leave callers waiting indefinitely.

4 open issues
1 previously missed issue

In code that hasn't changed since last review

High severity Reject zero-length forecasts before dispatch

iotdb-core/​ainode/​iotdb/​ainode/​core/​manager/​inference_manager.py:251

The new pre-dispatch validation still allows TForecastReq(outputLength=0). With tirex2 loaded, this reaches TiRex2._prepare_input(), which raises ValueError; the uncaught exception exits _requests_execute_loop(), leaving this caller waiting indefinitely and subsequent requests unprocessed. Reject values below 1 here as well, before dispatch, and cover a zero-length rejection followed by a valid pooled request.

Review effort: Balanced

"moment", new FakeModelInfo("moment", "moment", "builtin", "active")))
"moment", new FakeModelInfo("moment", "moment", "builtin", "active")),
new AbstractMap.SimpleEntry<>(
"tirex2", new FakeModelInfo("tirex2", "tirex2", "builtin", "active")))
Copilot AI balanced review requested due to automatic review settings November 14, 2026 15:19

Copilot AI left a comment

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.

🔵 Needs a closer look

Adding TiRex-2 to shared integration coverage causes the horizon-rejection test to fail because it expects an upper bound of 2880 while the model-specific validation returns 320.

3 open issues
1 resolved since last review

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 14, 2026 15:25

Copilot AI left a comment

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.

🔵 Needs a closer look

Adding TiRex-2 breaks the shared integration test because it expects a 2880-step limit while the model correctly reports 320; the range-error assertions need to be model-aware.

2 open issues
1 resolved since last review

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 14, 2026 15:30

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Copilot AI balanced review requested due to automatic review settings November 14, 2026 15:31

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Copilot AI balanced review requested due to automatic review settings November 14, 2026 15:33

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Copilot AI balanced review requested due to automatic review settings November 14, 2026 17:19

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants