test_embedding_batching.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. import asyncio
  2. import numpy as np
  3. import pytest
  4. import torch
  5. from fastapi import HTTPException
  6. from prometheus_client import CollectorRegistry
  7. from sentence_transformers import SentenceTransformer
  8. from zhifu.modules.knowledge.batching import Batcher
  9. from zhifu.modules.knowledge.config import KnowledgeSettings
  10. pytestmark = pytest.mark.integration
  11. @pytest.fixture(scope="module")
  12. def model():
  13. settings = KnowledgeSettings()
  14. torch.set_num_threads(4)
  15. encoder = SentenceTransformer(
  16. settings.embedding_model,
  17. revision=settings.embedding_revision,
  18. cache_folder=settings.model_cache,
  19. device=settings.inference_device,
  20. )
  21. encoder.max_seq_length = 512
  22. return encoder
  23. async def test_batch_preserves_request_and_text_order(model):
  24. registry = CollectorRegistry()
  25. batcher = Batcher(model, KnowledgeSettings(), registry)
  26. groups = [["键盘如何清洁"], ["订单发票申请", "包裹配送查询"], ["退换货申请"]]
  27. requests = [asyncio.create_task(batcher.submit(texts)) for texts in groups]
  28. await asyncio.sleep(0)
  29. task = asyncio.create_task(batcher.run())
  30. try:
  31. results = await asyncio.gather(*requests)
  32. expected = await asyncio.to_thread(
  33. model.encode,
  34. [text for group in groups for text in group],
  35. normalize_embeddings=True,
  36. show_progress_bar=False,
  37. )
  38. np.testing.assert_allclose(np.concatenate(results), expected, atol=1e-5)
  39. assert [len(result) for result in results] == [1, 2, 1]
  40. assert registry.get_sample_value("zhifu_embedding_batch_texts_count") == 1
  41. assert registry.get_sample_value("zhifu_embedding_batch_texts_sum") == 4
  42. finally:
  43. batcher.close()
  44. await task
  45. async def test_queue_capacity_cancellation_and_shutdown(model):
  46. batcher = Batcher(model, KnowledgeSettings(queue_texts=32), CollectorRegistry())
  47. request = asyncio.create_task(batcher.submit(["物流查询"] * 32))
  48. await asyncio.sleep(0)
  49. with pytest.raises(HTTPException) as error:
  50. await batcher.submit(["发票查询"])
  51. assert error.value.status_code == 429
  52. request.cancel()
  53. with pytest.raises(asyncio.CancelledError):
  54. await request
  55. assert batcher.waiting == 0 and not batcher.pending
  56. waiting = asyncio.create_task(batcher.submit(["退换货查询"]))
  57. await asyncio.sleep(0)
  58. batcher.close()
  59. with pytest.raises(HTTPException) as error:
  60. await waiting
  61. assert error.value.status_code == 503
  62. assert batcher.waiting == 0
  63. with pytest.raises(HTTPException) as error:
  64. await batcher.submit(["发票查询"])
  65. assert error.value.status_code == 503
  66. async def test_expired_queue_request_is_not_computed(model):
  67. registry = CollectorRegistry()
  68. settings = KnowledgeSettings(queue_wait_seconds=0.001, batch_wait_seconds=0.01)
  69. batcher = Batcher(model, settings, registry)
  70. request = asyncio.create_task(batcher.submit(["查询物流"]))
  71. await asyncio.sleep(0)
  72. task = asyncio.create_task(batcher.run())
  73. try:
  74. with pytest.raises(HTTPException) as error:
  75. await request
  76. assert error.value.status_code == 429
  77. assert registry.get_sample_value("zhifu_embedding_batch_texts_count") == 0
  78. assert batcher.waiting == 0
  79. finally:
  80. batcher.close()
  81. await task
  82. async def test_cancelled_active_request_does_not_cancel_batch(model):
  83. batcher = Batcher(model, KnowledgeSettings(), CollectorRegistry())
  84. first = asyncio.create_task(batcher.submit(["查询包裹物流"]))
  85. second = asyncio.create_task(batcher.submit(["申请电子发票"]))
  86. task = asyncio.create_task(batcher.run())
  87. try:
  88. async with asyncio.timeout(10):
  89. await batcher.computing.wait()
  90. first.cancel()
  91. with pytest.raises(asyncio.CancelledError):
  92. await first
  93. result = await second
  94. assert len(result) == 1 and len(result[0]) == 1024
  95. assert not batcher.active and batcher.waiting == 0
  96. finally:
  97. batcher.close()
  98. await task
  99. async def test_shutdown_finishes_active_computation(model):
  100. batcher = Batcher(model, KnowledgeSettings(), CollectorRegistry())
  101. request = asyncio.create_task(batcher.submit(["键盘清洁说明"] * 4))
  102. task = asyncio.create_task(batcher.run())
  103. try:
  104. async with asyncio.timeout(10):
  105. await batcher.computing.wait()
  106. batcher.close()
  107. with pytest.raises(HTTPException) as error:
  108. await request
  109. assert error.value.status_code == 503
  110. finally:
  111. batcher.close()
  112. await task
  113. assert not batcher.active and not batcher.accepting