Finite FP16 MX blocks become NaNs on the default PyTorch path
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 48/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Domain
- machine-learning
Research direction
Start with mx/mx_ops.py at the reported scale calculation around line 285, then inspect the zero guard around line 78. Run the provided reproduction on CPU and compare the six formats and the linear call with the expected finite outputs. Done means zero blocks stay zero and the small E5M2 block and public linear call return finite expected results; the report says no correction was tested.
Written by the indexing model from the issue text.
Description
Finite FP16 blocks become NaNs during MX quantization with custom_cuda=False. This includes zero blocks and a public linear call that should return zero.
Tested at microsoft/microxcaling commit 7bc41952de394f5cc5e782baf132e7c7542eb4e4 (mx==1.1.0). Both fresh environments used Python 3.12.3 and NumPy 1.26.4 on x86_64 Linux. The failure reproduced with CPU PyTorch 2.2.0+cpu and 2.4.0+cpu.
Reproduction
Run these commands in Bash with Python 3.12.3. The example is CC0.
git clone https://github.com/microsoft/microxcaling.git microxcaling
cd microxcaling
git checkout --detach 7bc41952de394f5cc5e782baf132e7c7542eb4e4
python3.12 -m venv .venv
.venv/bin/python -m pip install torch==2.4.0 --index-url https://download.pytorch.org/whl/cpu
.venv/bin/python -m pip install numpy==1.26.4 packaging ninja==1.13.2 hatchling==1.32.4 hatch-vcs==0.5.0 editables
MAX_JOBS=1 .venv/bin/python -m pip install --no-build-isolation --no-deps -e .
OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 .venv/bin/python - <<'PY'
import torch
from mx import linear
from mx.mx_ops import quantize_mx_op
from mx.specs import MxSpecs
torch.set_num_threads(1)
specs = MxSpecs()
specs.update(scale_bits=8, block_size=32, custom_cuda=False)
formats = (
"fp8_e5m2", "fp8_e4m3", "fp6_e3m2",
"fp6_e2m3", "fp4_e2m1", "int8",
)
for fmt in formats:
x = torch.zeros(32, dtype=torch.float16)
y = quantize_mx_op(x, specs, fmt, axes=-1, round="even")
print(fmt, "zero NaNs:", y.isnan().sum().item())
x = torch.zeros(32, dtype=torch.float16)
x[0] = 2**-10
y = quantize_mx_op(x, specs, "fp8_e5m2", axes=-1, round="even")
print("E5M2 small:", y[:2].tolist())
specs.update(
a_elem_format="fp8_e5m2", w_elem_format="fp8_e5m2",
quantize_backprop=False,
)
output = linear(
torch.zeros((1, 32), dtype=torch.float16),
torch.ones((1, 32), dtype=torch.float16),
mx_specs=specs,
)
print("linear:", output.item())
PY
For the other tested Torch version, use a fresh venv and substitute torch==2.2.0 in the install command. The no-dependency editable install omits unused torchvision and torchaudio, whose dependency mismatch is already reported in issue 31.
Expected and observed
The README documents native FP16 computation and CPU as the numeric reference. PR 28 added native FP16 support.
All-zero blocks should remain zero. For the small E5M2 block, the implemented scale algorithm selects X=2^-25 (E8M0 byte 0x66). The nonzero element is 2^15 (E5M2 code 0x78), so the decoded result is exactly 2^-10. Zero elements remain zero. OCP MX v1.0 §5.1 and Tables 2 and 7 define these finite encodings and products. The scale need not fit the FP16 container. An all-zero block does not require a unique scale.
These encodings are derived expectations for this library's maximum-based scale algorithm. The API returns dequantized tensors, and MX §6.3 permits other scale-selection algorithms.
| Case | Expected | Observed on both Torch versions |
|---|---|---|
| Zero block, each of the six formats | 32 zeros, no NaNs | 32 NaNs |
E5M2 block with 2^-10 and 31 zeros |
Identical finite block | 32 NaNs, beginning [nan, nan] |
FP16 zero input and unit weights in linear |
0.0 |
nan |
Source and scope
mx_ops.py:285 computes 2**shared_exp in FP16. The exact 2^-25 scale rounds to zero before division. Zero blocks also lose the FP32 zero guard at mx_ops.py:78 and underflow the clamped scale. Scale and normalization intermediates need a type that represents these values. No correction was tested.
Zero blocks were checked in all six formats with even, nearest, floor and both FP32-flush settings. The small input is an exact power of two. Wider FP32/BF16 inputs and the FP16 2^-9 threshold control passed. The original C++ CPU numeric code returned the expected values through a CPU-only build adapter. The adapter preserved the numeric code and changed only build plumbing. The stock CUDA extension and GPU execution were not tested. These cases use finite inputs and forward computations.
Found by ChipLab's AI verification agents and independently reproduced.
ChipLab ref: CLAB-2026-623
- Dominant language
- Python
- Stars
- 365
- Forks
- 54
- PR merge metrics
- No merged PRs in 30d
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/microxcaling
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
microsoft/microxcaling#49 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
microsoft/microxcaling#48 ·
-
Difficulty 5/5 Over a week Newbie friendliness 20/100
microsoft/microxcaling#43 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 35/100
microsoft/microxcaling#42 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 45/100
microsoft/microxcaling#36 ·
All issues in microsoft/microxcaling
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
AstrBotDevs/AstrBot#10720 ·
Maintainers usually reply within 1 day
-
fix(xiaomi): curated picker list still offers six ids that return 400 (plan-limited vs retired)Opencause/upstream comp/cli P2 provider/other tool/file type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
NousResearch/hermes-agent#143900 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 1-3 hours Newbie friendliness 85/100
WLAN-Pi/wlanpi-profiler#341 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
Maintainers usually reply within 1 day