11import torch
2- from test_tpu import matmul
2+ from test_tpu import matmul , reset_dut
33from torch ._inductor .compile_fx import compile_fx
44from torch ._dynamo import register_backend
55from torch .library import custom_op , register_fake
1717def tpu_matmul (a_q : Tensor , b_q : Tensor , bias : Optional [Tensor ] = None ,
1818 a_scale : Optional [float ] = None , b_scale : Optional [float ] = None ,
1919 a_zero : Optional [int ] = None , b_zero : Optional [int ] = None ) -> Tensor :
20- # Prepare centered int8 inputs for the TPU:
21- # 1) convert to int32 so subtraction is safe
22- # 2) subtract zero_point to center around 0
23- # 3) clamp to int8 range and cast back to int8 for the hardware
20+
21+ if a_zero is None :
22+ a_zero = 0
23+ if b_zero is None :
24+ b_zero = 0
25+
26+ # Convert to int32 for safe arithmetic
2427 a_i32 = a_q .to (torch .int32 )
2528 b_i32 = b_q .to (torch .int32 )
26-
27- if a_zero is not None :
28- a_center = a_i32 - int (a_zero )
29- else :
30- a_center = a_i32
31- if b_zero is not None :
32- b_center = b_i32 - int (b_zero )
29+
30+ # Subtract zero points
31+ a_centered = a_i32 - int (a_zero )
32+ b_centered = b_i32 - int (b_zero )
33+
34+ # For uint8-style quantization (zero_point=-128), shift to signed range
35+ shift_amount = 0
36+ if a_zero == - 128 :
37+ shift_amount = 128
38+ a_hw = (a_centered - shift_amount ).clamp (- 128 , 127 ).to (torch .int8 )
3339 else :
34- b_center = b_i32
35-
36- a_hw = a_center .clamp (- 128 , 127 ).to (torch .int8 )
37- b_hw = b_center .clamp (- 128 , 127 ).to (torch .int8 )
38-
39- # Call TPU: provide int8 inputs; TPU returns int32 accumulators
40+ a_hw = a_centered .clamp (- 128 , 127 ).to (torch .int8 )
41+
42+ b_hw = b_centered .clamp (- 128 , 127 ).to (torch .int8 )
43+
44+ # Call TPU
4045 future = concurrent .futures .Future ()
4146 async def wrapper ():
4247 try :
48+ await reset_dut (dut )
4349 result_int32 = await matmul (dut , a_hw , b_hw , transpose = True , is_torch = True )
4450 future .set_result (result_int32 )
4551 except Exception as e :
4652 future .set_exception (e )
4753 cocotb .start_soon (wrapper ())
4854 result_int32 = future .result ()
49-
50- # Dequantize to float using scale product (accumulators are int32)
51- if a_scale is None or b_scale is None :
52- out = result_int32 .to (torch .float32 )
53- else :
55+
56+ # Apply correction for the shift
57+ if shift_amount != 0 :
58+ correction = shift_amount * b_centered .sum (dim = 1 , dtype = torch .int32 )
59+ correction = correction .view (1 , - 1 ).expand_as (result_int32 )
60+ result_int32 = result_int32 + correction
61+
62+ # Convert to float and scale
63+ result_float = result_int32 .to (torch .float32 )
64+
65+ if a_scale is not None and b_scale is not None :
5466 scale = float (a_scale ) * float (b_scale )
55- out = result_int32 .to (torch .float32 ) * scale
56-
67+ out = result_float * scale
68+ else :
69+ out = result_float
70+
71+ # Add bias
5772 if bias is not None :
5873 out = out + bias
59-
6074 return out
6175
6276@register_fake ("tpu::matmul" )
6377def tpu_matmul_abstract (a : Tensor , b : Tensor , bias : Optional [Tensor ] = None , * args , ** kwargs ) -> Tensor :
64- M , N = a .shape [- 1 ], b .shape [- 2 ]
65- out = a .new_zeros (a .shape [:- 1 ] + (N ,), dtype = torch .float32 )
78+ # a: (..., K) where K is input features
79+ # b: (N, K) where N is output features (weight matrix, will be transposed)
80+ # output: (..., N) after computing a @ b.T
81+
82+ # b.shape[0] is N (number of output features)
83+ N = b .shape [0 ]
84+
85+ # Output shape: same batch dims as a, but last dim is N
86+ out_shape = list (a .shape [:- 1 ]) + [N ]
87+ out = a .new_zeros (out_shape , dtype = torch .float32 )
88+
6689 if bias is not None :
6790 out = out + bias
6891 return out
@@ -76,14 +99,15 @@ def _backend(gm: torch.fx.GraphModule, example_inputs):
7699 # print("\n=== FX graph received ===")
77100 # gm.graph.print_tabular()
78101
79- # ---- replace every linear but try to preserve quantized buffers + scale info ----
102+ # Replace linear operations with TPU matmul
80103 dequant_op = torch .ops .quantized_decomposed .dequantize_per_tensor .default
104+
81105 for node in list (gm .graph .nodes ):
82106 if node .target == torch .ops .aten .linear .default :
83107 x_node , w_node , bias = node .args
84108
85109 def unwrap_dequant (n ):
86- # If node is a dequantize_per_tensor node, return (q_tensor, scale, zero_point, node)
110+ # If node is a dequantize_per_tensor node, return quantization params
87111 if isinstance (n , torch .fx .Node ) and n .target == dequant_op :
88112 # args: (quantized_tensor, scale, zero_point, min, max, dtype)
89113 q_tensor = n .args [0 ]
@@ -99,23 +123,25 @@ def unwrap_dequant(n):
99123 if x_un and w_un :
100124 q_x , x_scale , x_zp , x_deq_node = x_un
101125 q_w , w_scale , w_zp , w_deq_node = w_un
126+
127+ # Verify zero points are 0 for symmetric quantization
128+ print (f"Replacing linear: x_zp={ x_zp } , w_zp={ w_zp } " )
129+
102130 new_node = gm .graph .call_function (
103131 torch .ops .tpu .matmul ,
104132 args = (q_x , q_w , bias , x_scale , w_scale , x_zp , w_zp ),
105133 )
106134 node .replace_all_uses_with (new_node )
107135 gm .graph .erase_node (node )
108- # try to erase the now-unused dequantize nodes
109- try :
136+
137+ # Clean up unused dequantize nodes
138+ if len (x_deq_node .users ) == 0 :
110139 gm .graph .erase_node (x_deq_node )
111- except Exception :
112- pass
113- try :
140+ if len (w_deq_node .users ) == 0 :
114141 gm .graph .erase_node (w_deq_node )
115- except Exception :
116- pass
117142 else :
118- # Fallback: if inputs are already raw tensors/float, call tpu.matmul with them
143+ # Fallback for non-quantized tensors
144+ print (f"Warning: Linear layer not quantized, using fallback" )
119145 new_node = gm .graph .call_function (
120146 torch .ops .tpu .matmul ,
121147 args = (x_node , w_node , bias ),
0 commit comments