2323from paddle .base import Executor
2424
2525
26+ def _np_cosine_similarity (x1 , x2 , axis = 1 , eps = 1e-8 ):
27+ """Reference following torch2.12.0: broadcast both inputs first, then divide
28+ each of them by its own norm along ``axis`` clamped to ``eps``.
29+ """
30+ x1 , x2 = np .broadcast_arrays (x1 , x2 )
31+ n1 = np .maximum (np .sqrt (np .sum (x1 * x1 , axis = axis , keepdims = True )), eps )
32+ n2 = np .maximum (np .sqrt (np .sum (x2 * x2 , axis = axis , keepdims = True )), eps )
33+ return np .sum ((x1 / n1 ) * (x2 / n2 ), axis = axis )
34+
35+
2636class TestCosineSimilarityAPI (unittest .TestCase ):
2737 def setUp (self ):
2838 self .places = get_places ()
2939
3040 def _get_numpy_out (self , x1 , x2 , axis = 1 , eps = 1e-8 ):
31- bs = np .broadcast_shapes ([x1 .shape [axis ]], [x2 .shape [axis ]])
32- w12 = np .sum (x1 * x2 , axis = axis )
33- w1 = np .sum (x1 * x1 , axis = axis )
34- w2 = np .sum (x2 * x2 , axis = axis )
35- m1 , m2 = bs [0 ] / x1 .shape [axis ], bs [0 ] / x2 .shape [axis ]
36- if m1 != 1 :
37- w1 = w1 * m1
38- if m2 != 1 :
39- w2 = w2 * m2
40- n12 = np .sqrt (np .clip (w1 * w2 , eps * eps , None ))
41- cos_sim = w12 / n12
42- return cos_sim
41+ return _np_cosine_similarity (x1 , x2 , axis = axis , eps = eps )
4342
4443 def check_static_result (self , place ):
4544 paddle .enable_static ()
@@ -177,18 +176,7 @@ def setUp(self):
177176 self .places = get_places ()
178177
179178 def _get_numpy_out (self , x1 , x2 , axis = 1 , eps = 1e-8 ):
180- bs = np .broadcast_shapes ([x1 .shape [axis ]], [x2 .shape [axis ]])
181- w12 = np .sum (x1 * x2 , axis = axis )
182- w1 = np .sum (x1 * x1 , axis = axis )
183- w2 = np .sum (x2 * x2 , axis = axis )
184- m1 , m2 = bs [0 ] / x1 .shape [axis ], bs [0 ] / x2 .shape [axis ]
185- if m1 != 1 :
186- w1 = w1 * m1
187- if m2 != 1 :
188- w2 = w2 * m2
189- n12 = np .sqrt (np .clip (w1 * w2 , eps * eps , None ))
190- cos_sim = w12 / n12
191- return cos_sim
179+ return _np_cosine_similarity (x1 , x2 , axis = axis , eps = eps )
192180
193181 def test_dygraph_1 (self ):
194182 paddle .disable_static ()
@@ -211,5 +199,224 @@ def test_dygraph_1(self):
211199 np .testing .assert_allclose (tensor_x1 .grad .shape , tensor_x1 .shape )
212200
213201
202+ class TestCosineSimilarityAPI_RankMismatch (unittest .TestCase ):
203+ """x1 and x2 may have different ranks: they are broadcast first, so
204+ ``axis`` indexes the common shape rather than each input's own shape.
205+ """
206+
207+ def test_dygraph (self ):
208+ paddle .disable_static ()
209+ np .random .seed (1 )
210+ for shape1 , shape2 , axis in [
211+ ([2 , 3 ], [1 , 2 , 3 ], 1 ),
212+ ([2 , 3 ], [1 , 2 , 3 ], 2 ),
213+ ([2 , 3 ], [1 , 2 , 3 ], - 1 ),
214+ ([2 , 3 ], [1 , 2 , 3 ], - 2 ),
215+ ([4 , 5 ], [3 , 4 , 5 ], 0 ),
216+ ([5 ], [3 , 4 , 5 ], 2 ),
217+ ([1 , 7 ], [2 , 3 , 7 ], 1 ),
218+ ([3 , 4 , 5 ], [4 , 5 ], 1 ),
219+ ]:
220+ np_x1 = np .random .rand (* shape1 ).astype (np .float32 )
221+ np_x2 = np .random .rand (* shape2 ).astype (np .float32 )
222+ np_out = _np_cosine_similarity (np_x1 , np_x2 , axis = axis )
223+
224+ y = F .cosine_similarity (
225+ paddle .to_tensor (np_x1 ), paddle .to_tensor (np_x2 ), axis = axis
226+ )
227+ msg = f"x1={ shape1 } x2={ shape2 } axis={ axis } "
228+ self .assertEqual (list (y .shape ), list (np_out .shape ), msg )
229+ np .testing .assert_allclose (
230+ y .numpy (), np_out , rtol = 1e-05 , err_msg = msg
231+ )
232+
233+ def test_static (self ):
234+ paddle .enable_static ()
235+ np .random .seed (1 )
236+ try :
237+ for shape1 , shape2 , axis in [
238+ ([2 , 3 ], [1 , 2 , 3 ], 1 ),
239+ ([4 , 5 ], [3 , 4 , 5 ], 0 ),
240+ ([5 ], [3 , 4 , 5 ], 2 ),
241+ ([3 , 4 , 5 ], [4 , 5 ], - 1 ),
242+ ]:
243+ np_x1 = np .random .rand (* shape1 ).astype (np .float32 )
244+ np_x2 = np .random .rand (* shape2 ).astype (np .float32 )
245+ np_out = _np_cosine_similarity (np_x1 , np_x2 , axis = axis )
246+ msg = f"x1={ shape1 } x2={ shape2 } axis={ axis } "
247+
248+ main_program = static .Program ()
249+ startup_program = static .Program ()
250+ with static .program_guard (main_program , startup_program ):
251+ x1 = static .data (name = "x1" , shape = shape1 )
252+ x2 = static .data (name = "x2" , shape = shape2 )
253+ result = F .cosine_similarity (x1 , x2 , axis = axis )
254+ for place in get_places ():
255+ fetches = Executor (place ).run (
256+ main_program ,
257+ feed = {"x1" : np_x1 , "x2" : np_x2 },
258+ fetch_list = [result ],
259+ )
260+ np .testing .assert_allclose (
261+ fetches [0 ], np_out , rtol = 1e-05 , err_msg = msg
262+ )
263+ finally :
264+ paddle .disable_static ()
265+
266+
267+ class TestCosineSimilarityAPI_LargeBroadcast (unittest .TestCase ):
268+ """Large inputs that broadcast along ``axis``: the reduction length is the
269+ broadcast one, which is what the ||repeat(x, m)|| == sqrt(m) * ||x||
270+ correction has to reproduce.
271+ """
272+
273+ def test_dygraph (self ):
274+ paddle .disable_static ()
275+ np .random .seed (1 )
276+ b , n , d = 4 , 512 , 256
277+ for shape1 , shape2 , axis in [
278+ # broadcast on the reduced axis, so both norms need the correction
279+ ([b , 1 , d ], [b , n , d ], 1 ),
280+ # broadcast on the reduced axis with mismatched ranks
281+ ([1 , d ], [b , n , d ], 1 ),
282+ # broadcast off the reduced axis, no correction
283+ ([b , 1 , d ], [b , n , d ], 2 ),
284+ ([d ], [b , n , d ], 2 ),
285+ ]:
286+ np_x1 = np .random .rand (* shape1 ).astype (np .float32 )
287+ np_x2 = np .random .rand (* shape2 ).astype (np .float32 )
288+ np_out = _np_cosine_similarity (np_x1 , np_x2 , axis = axis )
289+
290+ y = F .cosine_similarity (
291+ paddle .to_tensor (np_x1 ), paddle .to_tensor (np_x2 ), axis = axis
292+ )
293+ msg = f"x1={ shape1 } x2={ shape2 } axis={ axis } "
294+ self .assertEqual (list (y .shape ), list (np_out .shape ), msg )
295+ np .testing .assert_allclose (
296+ y .numpy (), np_out , rtol = 1e-05 , err_msg = msg
297+ )
298+
299+
300+ class TestCosineSimilarityAPI_Numerics (unittest .TestCase ):
301+ def test_small_but_valid_vectors (self ):
302+ # |x1| * |x2| is below eps while both norms are far above it, so
303+ # clamping the product instead of each norm would attenuate the
304+ # result to about zero
305+ paddle .disable_static ()
306+ np_x1 = np .array ([[1e-6 , 0.0 , 0.0 ]], dtype = np .float32 )
307+ np_x2 = np .array ([[2e-6 , 0.0 , 0.0 ]], dtype = np .float32 )
308+ y = F .cosine_similarity (
309+ paddle .to_tensor (np_x1 ), paddle .to_tensor (np_x2 ), axis = 1
310+ )
311+ np .testing .assert_allclose (
312+ y .numpy (), np .ones ([1 ], dtype = np .float32 ), rtol = 1e-06
313+ )
314+
315+ def test_large_vectors_do_not_overflow (self ):
316+ # |x1|^2 * |x2|^2 overflows float32 here while each norm stays finite
317+ paddle .disable_static ()
318+ np_x1 = np .full ([1 , 4 ], 1e10 , dtype = np .float32 )
319+ np_x2 = np .full ([1 , 4 ], - 1e10 , dtype = np .float32 )
320+ y = F .cosine_similarity (
321+ paddle .to_tensor (np_x1 ), paddle .to_tensor (np_x2 ), axis = 1
322+ )
323+ np .testing .assert_allclose (
324+ y .numpy (), - np .ones ([1 ], dtype = np .float32 ), rtol = 1e-06
325+ )
326+
327+ def test_zero_vector (self ):
328+ paddle .disable_static ()
329+ np_x1 = np .zeros ([2 , 4 ], dtype = np .float32 )
330+ np_x2 = np .random .rand (2 , 4 ).astype (np .float32 )
331+ x1 = paddle .to_tensor (np_x1 )
332+ x1 .stop_gradient = False
333+ y = F .cosine_similarity (x1 , paddle .to_tensor (np_x2 ), axis = 1 )
334+ np .testing .assert_allclose (
335+ y .numpy (), np .zeros ([2 ], dtype = np .float32 ), rtol = 1e-06
336+ )
337+ y .sum ().backward ()
338+ self .assertFalse (np .isnan (x1 .grad .numpy ()).any ())
339+
340+
341+ class TestCosineSimilarityAPI_BroadcastMemory (unittest .TestCase ):
342+ """Taking the norm of a broadcast input must not materialize the expanded
343+ copy: for x1=[B, 1, D] against x2=[B, N, D] that would be an extra
344+ O(B * N * D) allocation. FLAGS_use_accuracy_compatible_kernel opts into that
345+ allocation on purpose, so this only holds for the default path.
346+ """
347+
348+ @unittest .skipIf (
349+ not paddle .is_compiled_with_cuda (), "peak memory is queried from CUDA"
350+ )
351+ @unittest .skipIf (
352+ paddle .get_flags (["FLAGS_use_accuracy_compatible_kernel" ]).get (
353+ "FLAGS_use_accuracy_compatible_kernel" , False
354+ ),
355+ "the accuracy compatible path expands the inputs on purpose" ,
356+ )
357+ def test_no_expanded_copy (self ):
358+ paddle .disable_static ()
359+ origin_device = paddle .get_device ()
360+ paddle .set_device ('gpu' )
361+ try :
362+ B , N , D = 32 , 256 , 1024
363+ x1 = paddle .randn ([B , 1 , D ])
364+ x2 = paddle .randn ([B , N , D ])
365+ full_size = B * N * D * 4
366+
367+ paddle .device .cuda .empty_cache ()
368+ paddle .device .cuda .reset_max_memory_allocated ()
369+ base = paddle .device .cuda .max_memory_allocated ()
370+ F .cosine_similarity (x1 , x2 , axis = 1 ).numpy ()
371+ extra = paddle .device .cuda .max_memory_allocated () - base
372+
373+ # x2 / n2 and their product are the only full size temporaries
374+ self .assertLess (
375+ extra ,
376+ 2.5 * full_size ,
377+ f"{ extra } bytes of temporaries for a { full_size } byte input, "
378+ "the broadcast input is likely being expanded" ,
379+ )
380+ finally :
381+ paddle .set_device (origin_device )
382+
383+
384+ class TestCosineSimilarityAPI_DtypePromotion (unittest .TestCase ):
385+ def test_integral_input_is_promoted (self ):
386+ paddle .disable_static ()
387+ np_x1 = np .array ([[1 , 2 , 3 ]], dtype = np .int32 )
388+ np_x2 = np .array ([[3.0 , 2.0 , 1.0 ]], dtype = np .float32 )
389+ y = F .cosine_similarity (
390+ paddle .to_tensor (np_x1 ), paddle .to_tensor (np_x2 ), axis = 1
391+ )
392+ self .assertEqual (y .dtype , paddle .float32 )
393+ np_out = _np_cosine_similarity (np_x1 .astype (np .float32 ), np_x2 )
394+ np .testing .assert_allclose (y .numpy (), np_out , rtol = 1e-06 )
395+
396+ def test_mixed_float_dtypes (self ):
397+ paddle .disable_static ()
398+ np_x1 = np .random .rand (2 , 5 ).astype (np .float32 )
399+ np_x2 = np .random .rand (2 , 5 ).astype (np .float64 )
400+ y = F .cosine_similarity (
401+ paddle .to_tensor (np_x1 ), paddle .to_tensor (np_x2 ), axis = 1
402+ )
403+ self .assertEqual (y .dtype , paddle .float64 )
404+ np_out = _np_cosine_similarity (np_x1 .astype (np .float64 ), np_x2 )
405+ np .testing .assert_allclose (y .numpy (), np_out , rtol = 1e-06 )
406+
407+ def test_non_floating_common_dtype (self ):
408+ paddle .disable_static ()
409+ for dtype in ('int32' , 'int64' , 'bool' , 'complex64' ):
410+ x = paddle .ones ([1 , 3 ], dtype = dtype )
411+ with self .assertRaises (TypeError ):
412+ F .cosine_similarity (x , x , axis = 1 )
413+
414+ def test_negative_eps (self ):
415+ paddle .disable_static ()
416+ x = paddle .ones ([1 , 3 ])
417+ with self .assertRaises (ValueError ):
418+ F .cosine_similarity (x , x , axis = 1 , eps = - 1e-8 )
419+
420+
214421if __name__ == '__main__' :
215422 unittest .main ()
0 commit comments