How to Ensure Quantile Monotonicity with TimesFM: Non-Crossing Forecasts Explained

Enable fix_quantile_crossing=True in ForecastConfig to automatically enforce that lower quantile forecasts never exceed higher quantile forecasts, using built-in post-processing for both PyTorch and JAX backends.

TimesFM (Time Series Foundation Model) from google-research/timesfm generates probabilistic forecasts through multiple quantiles, but raw model outputs may violate monotonicity constraints where lower percentiles exceed higher ones. This guide explains how to ensure quantile monotonicity with TimesFM using the library's built-in correction mechanisms that adjust crossing quantiles after decoding.

Understanding the Quantile Crossing Problem

When TimesFM generates forecasts for percentiles (e.g., 10%, 20%, ..., 90%), statistical theory requires that lower quantiles never exceed higher quantiles. Specifically, the forecast for the 10th percentile must be less than or equal to the 50th percentile (median), which must be less than or equal to the 90th percentile.

However, the model's raw outputs can produce crossing quantiles where, for example, the 20th percentile prediction temporarily exceeds the median. This violates the non-decreasing property of quantile functions and produces invalid uncertainty bounds that break probabilistic interpretations.

Enabling Monotonicity Correction in ForecastConfig

TimesFM provides a boolean configuration flag to automatically fix crossing quantiles. According to the source in src/timesfm/configs.py (lines 47-60), the ForecastConfig class exposes fix_quantile_crossing, which triggers a post-processing step after the forward pass.

To ensure quantile monotonicity, instantiate your configuration with the flag enabled:

from timesfm import ForecastConfig

config = ForecastConfig(
    quantiles=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
    fix_quantile_crossing=True  # Enable monotonicity enforcement

)

When this flag is active, the model applies backend-specific correction logic immediately after generating raw forecasts, before returning results to the user.

Backend-Specific Implementation Details

TimesFM implements quantile monotonicity enforcement differently depending on whether you use the PyTorch or Flax/JAX backend. Both approaches ensure that lower quantiles never exceed the median and upper quantiles never fall below it.

PyTorch Implementation (_compiled_decode)

In src/timesfm/timesfm_2p5/timesfm_2p5_torch.py (lines 60-73), the PyTorch backend uses explicit loops with torch.where operations to clamp crossing values. The implementation processes the lower tail (indices 1-4, representing 10-40%) and upper tail (indices 6-9, representing 60-90%) separately:

if fc.fix_quantile_crossing:
  # Lower quantiles (10–40 %) must be ≤ median (50 %).

  for i in [4, 3, 2, 1]:
    full_forecast[:, :, i] = torch.where(
        full_forecast[:, :, i] < full_forecast[:, :, i + 1],
        full_forecast[:, :, i],               # keep the lower value

        full_forecast[:, :, i + 1],           # otherwise raise it to the next higher quantile

    )
  # Upper quantiles (60–90 %) must be ≥ median.

  for i in [6, 7, 8, 9]:
    full_forecast[:, :, i] = torch.where(
        full_forecast[:, :, i] > full_forecast[:, :, i - 1],
        full_forecast[:, :, i],
        full_forecast[:, :, i - 1],
    )

The algorithm walks inward from the extremes toward the median index (5), ensuring the lower half forms a non-decreasing sequence and the upper half forms a non-increasing sequence relative to the median.

Flax/JAX Implementation (_fix_quantile_crossing_fn)

The Flax backend in src/timesfm/timesfm_2p5/timesfm_2p5_flax.py (lines 329-353) uses JAX's scan operations for efficient vectorized processing. The _fix_quantile_crossing_fn function propagates constraints through the quantile dimension using functional programming patterns:

def _fix_quantile_crossing_fn(full_forecast):
  # Propagate the minimum value from median outward for lower quantiles.

  lower_quantiles = _scan_along_axis(
      lambda carry, x: (w := jnp.minimum(carry, x), w),
      init=full_forecast[..., 5],
      xs=full_forecast[..., 1:5],
      axis=-1,
      reverse=True,
  )[1]

  # Propagate the maximum value from median outward for upper quantiles.

  upper_quantiles = _scan_along_axis(
      lambda carry, x: (w := jnp.maximum(carry, x), w),
      init=full_forecast[..., 5],
      xs=full_forecast[..., 6:10],
      axis=-1,
      reverse=False,
  )[1]

  # Re‑assemble the corrected forecast tensor.

  return jnp.concatenate(
      [
          full_forecast[..., :1],   # backcast placeholder

          lower_quantiles,
          full_forecast[..., 5:6],  # median

          upper_quantiles,
      ],
      axis=-1,
  )

This approach uses _scan_along_axis to sweep from the median outward, applying jnp.minimum to lower quantiles (moving backward through indices 4,3,2,1) and jnp.maximum to upper quantiles (moving forward through indices 6,7,8,9), ensuring monotonicity without explicit Python loops.

Complete Usage Example

The timesfm-forecasting/scripts/forecast_csv.py script demonstrates the practical workflow. After loading your TimesFM checkpoint, enable the correction before forecasting:

from timesfm import TimesFm, ForecastConfig

# Initialize model (PyTorch or Flax)

model = TimesFm.load_from_checkpoint(checkpoint_path)

# Configure with quantile monotonicity enforcement

config = ForecastConfig(
    context_len=512,
    horizon_len=96,
    quantiles=[0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
    fix_quantile_crossing=True
)

# Generate forecasts - quantiles are now guaranteed non-crossing

forecasts = model.forecast(
    inputs=time_series_data,
    config=config
)

Summary

  • Quantile crossing occurs when TimesFM's raw predictions violate the requirement that lower percentiles must not exceed higher percentiles, producing invalid prediction intervals.
  • Enable fix_quantile_crossing=True in ForecastConfig (defined in src/timesfm/configs.py) to activate automatic correction during the decoding phase.
  • PyTorch backend applies iterative torch.where operations in src/timesfm/timesfm_2p5/timesfm_2p5_torch.py to clamp lower quantiles upward and upper quantiles downward toward the median.
  • Flax/JAX backend uses functional scans in src/timesfm/timesfm_2p5/timesfm_2p5_flax.py to propagate minimum and maximum constraints from the median outward through the quantile dimension.
  • The correction requires no manual intervention beyond the configuration flag and runs automatically as part of the _compiled_decode or _fix_quantile_crossing_fn execution.

Frequently Asked Questions

What is quantile crossing and why does it matter?

Quantile crossing occurs when a forecast for a lower percentile (e.g., 10%) exceeds a forecast for a higher percentile (e.g., 90%), violating the mathematical definition of quantiles. This produces logically inconsistent uncertainty bounds where prediction intervals have negative width, making the forecasts statistically invalid and unreliable for risk assessment or decision-making under uncertainty.

Does enabling fix_quantile_crossing affect model calibration?

The monotonicity correction adjusts only the ordering of quantile forecasts to ensure $Q_{\tau_1} \leq Q_{\tau_2}$ for $\tau_1 < \tau_2$, without retraining model parameters or altering the central tendency. While this guarantees valid prediction intervals, it may slightly shift empirical coverage if raw outputs were severely crossed. However, crossed quantiles are inherently uncalibrated, so the correction improves interpretability and statistical validity.

Which TimesFM backends support quantile monotonicity correction?

Both the PyTorch implementation (timesfm_2p5_torch.py) and the Flax/JAX implementation (timesfm_2p5_flax.py) in the google-research/timesfm repository support fix_quantile_crossing. The flag works identically in both frameworks, applying framework-specific post-processing automatically based on which backend your model instance uses.

How can I verify that quantile monotonicity has been enforced?

After obtaining forecasts, verify monotonicity by checking that each quantile slice is less than or equal to the next along the quantile dimension. For PyTorch tensors, use torch.all(forecasts[..., :-1] <= forecasts[..., 1:]). For JAX arrays, use jnp.all(jnp.diff(forecasts, axis=-1) >= 0). When fix_quantile_crossing=True, these assertions will return True, confirming the post-processing step successfully reordered any crossed quantiles.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →