Skip to content

Commit c89f6a4

Browse files
Fix integer true-divide silently truncating instead of promoting to float (#32)
Summary Integer true-divide (x / y, torch.true_divide, torch.div(x, y, rounding_mode=None)) was dividing operands as integers first and only casting the truncated result to float afterward, silently dropping the fractional part on every backend. Per PyTorch's promotion rules, integer operands must be promoted to float before dividing. div.Tensor/div.Scalar/true_divide.Tensor were wired to the generic replace_binary_ops handler, which keeps same-kind integers as integers (correct for add/sub/mul, wrong for true divide). Re-pointed them to replace_truediv, which already promotes to the node's real float output type before dividing. Fixed the same latent bug in replace_div_tensor_mode's rounding_mode=None branch. Grouped div-family tests into a TestDiv class (matches existing TestCopy-style convention) and added regression tests for the integer cases above. floordiv/mod/fmod/rounding_mode="floor"/"trunc" were already correct and untouched. Test plan python unit test
1 parent 4529671 commit c89f6a4

8 files changed

Lines changed: 221 additions & 143 deletions

File tree

coreai_torch/_aten_to_core.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -810,8 +810,6 @@ def replace_binary_ops(
810810
"add.Tensor": coreai.broadcasting_add,
811811
"add.Scalar": coreai.broadcasting_add,
812812
"add": coreai.broadcasting_add,
813-
"div.Tensor": coreai.broadcasting_divide,
814-
"div.Scalar": coreai.broadcasting_divide,
815813
"maximum.default": coreai.broadcasting_maximum,
816814
"minimum.default": coreai.broadcasting_minimum,
817815
"fmod.Tensor": coreai.broadcasting_modulo,
@@ -876,13 +874,20 @@ def replace_div_tensor_mode(
876874
else (node.args[2] if len(node.args) > 2 else None)
877875
)
878876

877+
if rounding_mode is None:
878+
# True divide: integer operands must promote to the node's float
879+
# output type before dividing, not the generic same-kind-stays-integer
880+
# promotion rule used by "floor"/"trunc" below.
881+
result_type = get_output_element_type_from_node(node)
882+
return coreai.broadcasting_divide(
883+
coreai.cast(x, result_type), coreai.cast(y, result_type)
884+
)
885+
879886
promoted_type = get_promoted_type(x.type, y.type)
880887
casted_x = coreai.cast(x, promoted_type)
881888
casted_y = coreai.cast(y, promoted_type)
882889

883-
if rounding_mode is None:
884-
return coreai.broadcasting_divide(casted_x, casted_y)
885-
elif rounding_mode == "floor":
890+
if rounding_mode == "floor":
886891
return coreai.broadcasting_floor_divide(casted_x, casted_y)
887892
elif rounding_mode == "trunc":
888893
# Integer division already truncates toward zero, so a plain divide
@@ -3588,8 +3593,8 @@ def sdpa_maskless(q: Value, k: Value, v: Value) -> Value:
35883593
"cos.default": replace_unary_ops,
35893594
"cosh.default": replace_unary_ops,
35903595
"cumsum.default": replace_cumsum,
3591-
"div.Scalar": replace_binary_ops,
3592-
"div.Tensor": replace_binary_ops,
3596+
"div.Scalar": replace_truediv,
3597+
"div.Tensor": replace_truediv,
35933598
"div.Tensor_mode": replace_div_tensor_mode,
35943599
"embedding.default": replace_embedding,
35953600
"empty.default": replace_empty,
@@ -3719,7 +3724,7 @@ def sdpa_maskless(q: Value, k: Value, v: Value) -> Value:
37193724
"truediv": replace_truediv,
37203725
"to.dtype": replace_to_dtype,
37213726
"topk.default": replace_topk,
3722-
"true_divide.Tensor": replace_binary_ops,
3727+
"true_divide.Tensor": replace_truediv,
37233728
"trunc.default": replace_trunc,
37243729
"trunc": replace_trunc,
37253730
"unsqueeze.default": replace_unsqueeze,

docs/api/TorchConverter.md

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -241,16 +241,20 @@ import torch
241241
from coreai._compiler.dialects import coreai
242242
from coreai_torch._utils import get_operands
243243

244+
244245
@torch.library.custom_op("my_lib::scaled_add", mutates_args=())
245246
def scaled_add(x: torch.Tensor, y: torch.Tensor, scale: float) -> torch.Tensor:
246247
return x + scale * y
247248

249+
248250
@scaled_add.register_fake
249251
def _(x, y, scale):
250252
return torch.empty_like(x)
251253

254+
252255
converter = TorchConverter()
253256

257+
254258
@converter.register_torch_lowering("my_lib::scaled_add.default")
255259
def lower_scaled_add(values_map, node, loc):
256260
x, y = get_operands(values_map, node, [0, 1], loc)
@@ -259,6 +263,7 @@ def lower_scaled_add(values_map, node, loc):
259263
scaled_y = coreai.broadcasting_mul(y, scale_val, loc=loc)
260264
return coreai.broadcasting_add(x, scaled_y, loc=loc)
261265

266+
262267
coreai_program = converter.add_exported_program(exported).to_coreai()
263268
coreai_program.optimize()
264269
```
@@ -272,7 +277,10 @@ from coreai_torch._utils import get_operand
272277

273278
converter = TorchConverter()
274279

275-
@converter.register_torch_lowering("aten::_adaptive_avg_pool2d.default", allow_override=True)
280+
281+
@converter.register_torch_lowering(
282+
"aten::_adaptive_avg_pool2d.default", allow_override=True
283+
)
276284
def lower_adaptive_avg_pool2d_static(values_map, node, loc):
277285
x = get_operand(values_map, node, 0, loc)
278286
output_h, output_w = node.args[1]
@@ -290,6 +298,7 @@ def lower_adaptive_avg_pool2d_static(values_map, node, loc):
290298
coreai.cast(float(kernel_h * kernel_w), x.type.element_type),
291299
)
292300

301+
293302
coreai_program = converter.add_exported_program(exported).to_coreai()
294303
coreai_program.optimize()
295304
```
@@ -317,7 +326,12 @@ Registers one or more `TorchMetalKernel` objects so the converter can convert th
317326

318327
```python
319328
import torch
320-
from coreai_torch import TorchConverter, TorchMetalKernel, MetalParameter, get_decomp_table
329+
from coreai_torch import (
330+
TorchConverter,
331+
TorchMetalKernel,
332+
MetalParameter,
333+
get_decomp_table,
334+
)
321335

322336

323337
def torch_add(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
@@ -383,9 +397,9 @@ coreai_program = (
383397
TorchConverter()
384398
.add_pytorch_module(
385399
model,
386-
export_fn=lambda m: torch.export.export(m, args=example_inputs).run_decompositions(
387-
coreai_torch.get_decomp_table()
388-
),
400+
export_fn=lambda m: torch.export.export(
401+
m, args=example_inputs
402+
).run_decompositions(coreai_torch.get_decomp_table()),
389403
)
390404
.to_coreai()
391405
)
@@ -457,6 +471,7 @@ class Linear(nn.Module):
457471
def forward(self, x):
458472
return self.fc(x)
459473

474+
460475
ep = torch.export.export(Linear().eval(), args=(torch.randn(1, 8),))
461476
ep = ep.run_decompositions(get_decomp_table())
462477

@@ -473,16 +488,17 @@ TorchConverter().add_exported_program(
473488
class KVCache(nn.Module):
474489
def __init__(self):
475490
super().__init__()
476-
self.register_buffer("kv_cache", torch.zeros(1, 4)) # state[0]
477-
self.register_buffer("pos_idx", torch.zeros(1)) # state[1]
491+
self.register_buffer("kv_cache", torch.zeros(1, 4)) # state[0]
492+
self.register_buffer("pos_idx", torch.zeros(1)) # state[1]
478493

479494
def forward(self, x, y, z):
480-
self.kv_cache.add_(x) # buffer mutation
481-
self.pos_idx.add_(1) # buffer mutation
482-
y.mul_(2) # state[2]: mutated user input
495+
self.kv_cache.add_(x) # buffer mutation
496+
self.pos_idx.add_(1) # buffer mutation
497+
y.mul_(2) # state[2]: mutated user input
483498
# non-mutated: x -> input[0], z -> input[1]
484499
return self.kv_cache + y, z * 3
485500

501+
486502
ep = torch.export.export(
487503
KVCache().eval(),
488504
args=(torch.randn(1, 4), torch.randn(1, 4), torch.randn(1, 4)),

docs/api/composite-ops.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ Use these module subclasses (and ATen-derived ops) to preserve an operation's bo
77
**Public import:**
88

99
```python
10-
from coreai_torch.composite_ops import GatherMM, GatedDeltaUpdate, RMSNormImpl, RoPE, SDPA
10+
from coreai_torch.composite_ops import (
11+
GatherMM,
12+
GatedDeltaUpdate,
13+
RMSNormImpl,
14+
RoPE,
15+
SDPA,
16+
)
1117
```
1218

1319
coreai-torch provides composite ops in two categories:

docs/api/composite-ops/gather-mm.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,10 @@ class MoELayer(nn.Module):
8282
8383
def forward(
8484
self,
85-
x: torch.Tensor, # [B, T, 1, 1, D]
86-
experts: torch.Tensor, # [E, D, H]
87-
indices: torch.Tensor, # [B, T, K]
88-
) -> torch.Tensor: # [B, T, K, 1, H]
85+
x: torch.Tensor, # [B, T, 1, 1, D]
86+
experts: torch.Tensor, # [E, D, H]
87+
indices: torch.Tensor, # [B, T, K]
88+
) -> torch.Tensor: # [B, T, K, 1, H]
8989
return self.gather_mm(x, experts, rhs_indices=indices)
9090
9191
@@ -129,10 +129,10 @@ class FusedMoELayer(nn.Module):
129129
130130
def forward(
131131
self,
132-
x: torch.Tensor, # [B, T, 1, 1, D]
133-
fused_experts: torch.Tensor, # [2, E, D, H] (gate + up stacked)
134-
indices: torch.Tensor, # [B, T, K]
135-
) -> torch.Tensor: # [2, B, T, K, 1, H]
132+
x: torch.Tensor, # [B, T, 1, 1, D]
133+
fused_experts: torch.Tensor, # [2, E, D, H] (gate + up stacked)
134+
indices: torch.Tensor, # [B, T, K]
135+
) -> torch.Tensor: # [2, B, T, K, 1, H]
136136
return self.gather_mm(x, fused_experts, rhs_indices=indices)
137137
```
138138
@@ -169,10 +169,11 @@ def _gather(x, indices, num_batch_axes=0):
169169
flat_indices = indices.to(torch.int32).flatten()
170170
flat_gather = torch.index_select(x, dim=num_batch_axes, index=flat_indices)
171171
result_shape = (
172-
x.shape[:num_batch_axes] + indices.shape + x.shape[num_batch_axes + 1:]
172+
x.shape[:num_batch_axes] + indices.shape + x.shape[num_batch_axes + 1 :]
173173
)
174174
return flat_gather.view(result_shape)
175175
176+
176177
def gather_mm(lhs, rhs, lhs_indices=None, rhs_indices=None, num_batch_axes=0):
177178
if lhs_indices is not None:
178179
lhs = _gather(lhs, lhs_indices, num_batch_axes=num_batch_axes)

docs/api/composite-ops/instance-norm.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,15 @@ gamma = torch.randn(C)
4040
beta = torch.randn(C)
4141

4242
output = torch.ops.aten.instance_norm.default(
43-
input, gamma, beta,
44-
None, None, # running_mean / running_var unused in inference
45-
True, # use_input_stats
46-
0.1, # momentum (ignored in inference)
47-
1e-5, # eps
48-
True, # cudnn_enabled (ignored)
43+
input,
44+
gamma,
45+
beta,
46+
None,
47+
None, # running_mean / running_var unused in inference
48+
True, # use_input_stats
49+
0.1, # momentum (ignored in inference)
50+
1e-5, # eps
51+
True, # cudnn_enabled (ignored)
4952
)
5053
```
5154

docs/api/debugging.md

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,12 @@ from coreai_torch.debugging.comparator import create_comparator_for_programs
8484
comparator = await create_comparator_for_programs(
8585
source_program=exported_program,
8686
target_program=coreai_program,
87-
target_entry_point="main"
87+
target_entry_point="main",
8888
)
8989

9090
# Compare outputs with tolerance
9191
result = await comparator.compare_with_tolerance(
92-
inputs={"x": example_input},
93-
rtol=1e-5,
94-
atol=1e-8
92+
inputs={"x": example_input}, rtol=1e-5, atol=1e-8
9593
)
9694

9795
# Check for differences
@@ -123,8 +121,7 @@ coreai_op_ids = [1, 5, 10, 15]
123121

124122
# Capture intermediate values
125123
results = await inspector.get_intermediates_for_ops(
126-
coreai_op_ids,
127-
inputs={"x": np.random.randn(2, 4).astype(np.float32)}
124+
coreai_op_ids, inputs={"x": np.random.randn(2, 4).astype(np.float32)}
128125
)
129126

130127
# Check results
@@ -144,7 +141,7 @@ Analyze structural differences between model implementations using graph isomorp
144141
from coreai_torch.debugging.graph_diff import (
145142
compute_exported_program_diff,
146143
compute_coreai_program_diff,
147-
write_diff
144+
write_diff,
148145
)
149146

150147
# Compare two PyTorch programs
@@ -160,12 +157,7 @@ else:
160157
print(f"✗ Found {diff.summary.unmapped_source_node_count} structural differences")
161158

162159
# Write detailed diff report to stdout
163-
write_diff(
164-
diff,
165-
diff.source_graph,
166-
diff.target_graph,
167-
max_items=20
168-
)
160+
write_diff(diff, diff.source_graph, diff.target_graph, max_items=20)
169161
```
170162

171163

@@ -180,9 +172,7 @@ from coreai_torch.debugging.benchmarker import benchmark_coreai_program
180172

181173
# Run benchmark
182174
result = await benchmark_coreai_program(
183-
coreai_program=coreai_program,
184-
inputs={"x": torch.randn(2, 4)},
185-
num_runs=50
175+
coreai_program=coreai_program, inputs={"x": torch.randn(2, 4)}, num_runs=50
186176
)
187177

188178
# Show timing summary
@@ -203,10 +193,8 @@ Create custom checks beyond NaN/infinity:
203193
```python
204194
def check_large_values(outputs):
205195
"""Check if any output has values > threshold"""
206-
return any(
207-
abs(arr).max() > 1000.0 if arr is not None else False
208-
for arr in outputs
209-
)
196+
return any(abs(arr).max() > 1000.0 if arr is not None else False for arr in outputs)
197+
210198

211199
# Use custom check
212200
result = await validator.check(check_large_values, inputs=example_input)
@@ -256,9 +244,7 @@ exported_program = torch.export.export(model, args=example_input)
256244

257245
# Save intermediate values to disk
258246
metadata_path = save_intermediates(
259-
program=exported_program,
260-
inputs=example_input,
261-
output_dir=Path("./debug_output")
247+
program=exported_program, inputs=example_input, output_dir=Path("./debug_output")
262248
)
263249

264250
print(f"Intermediates saved to: {metadata_path}")
@@ -291,12 +277,13 @@ def custom_filter(node, result):
291277
"""Only save convolution and linear layer outputs"""
292278
return any(op in str(node.target).lower() for op in ["conv", "linear", "matmul"])
293279

280+
294281
# Save only filtered operations
295282
metadata_path = save_intermediates(
296283
program=exported_program,
297284
inputs=example_input,
298285
output_dir=Path("./debug_output"),
299-
node_filter=custom_filter
286+
node_filter=custom_filter,
300287
)
301288
```
302289

docs/getting-started/installation.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ Run the following to confirm coreai-torch is installed correctly — a version s
4646

4747
```python
4848
import coreai_torch
49+
4950
print(coreai_torch.__version__)
5051
```
5152

0 commit comments

Comments
 (0)