Вопрос или проблема
Сначала я создаю сессию и связываю выходные и входные тензоры следующим образом:
input_names: List[str] = []
x_dict = OrderedDict()
y_dict = OrderedDict()
output_names: List[str] = []
# по умолчанию уже используется максимальная оптимизация
session = ort.InferenceSession(model_onnx1.SerializeToString(), providers=['CUDAExecutionProvider'])
model_metadata = session.get_modelmeta()
binding = session.io_binding()
print("Метаданные модели:")
print(f" Производитель: {model_metadata.producer_name}")
print(f" Версия модели: {model_metadata.version}")
print(f" Название графа: {model_metadata.graph_name}")
print("\nВходные данные:")
for x_ in session.get_inputs():
print(f" Имя: {x_.name}")
print(f" Размер: {x_.shape}")
print(f" Тип: {x_.type}")
input_names.append(x_.name)
val_ = torch.zeros(
size=x_.shape,
dtype=TypeHelper.ort_type_to_torch_type(x_.type),
device=device
).contiguous()
binding.bind_input(
name=x_.name,
device_type=device.type,
device_id=device.index,
element_type=TypeHelper.ort_type_to_numpy_type(x_.type),
shape=x_.shape,
buffer_ptr=val_.data_ptr()
)
x_dict[x_.name] = val_
# Получаем детали выходных данных (имя, размер, тип)
print("\nВыходные данные:")
for y_ in session.get_outputs():
print(f" Имя: {y_.name}")
print(f" Размер: {y_.shape}")
print(f" Тип: {y_.type}")
output_names.append(y_.name)
val_ = torch.zeros(
size=y_.shape,
dtype=TypeHelper.ort_type_to_torch_type(y_.type),
device=device
).contiguous()
binding.bind_output(
name=output_names[0],
device_type=device.type,
device_id=device.index,
element_type=TypeHelper.ort_type_to_numpy_type(y_.type),
shape=y_.shape,
buffer_ptr=val_.data_ptr()
)
y_dict[y_.name] = val_
Затем я пытаюсь использовать сессию для вывода, следующий dt обычно составляет 1 мс
# все тензоры уже находятся на cuda:0
for name, val in zip(x_names, xs):
x_dict[name].copy_(val)
torch.cuda.synchronize(device=device)
# почему здесь необходима синхронизация? разве предыдущая операция не должна быть в потоке по умолчанию
# и автоматически синхронизироваться в этом потоке для всех операций?
# без этой синхронизации сессия использует неполные данные.
start_time = timeit.default_timer()
session.run_with_iobinding(
binding
)
ret = tuple(y_dict[name].clone() for name in y_names)
torch.cuda.synchronize(device=device)
dt = timeit.default_timer() - start_time
print(dt*1000)
Первая странная часть – почему copy_ требует синхронизации с CPU? разве все уже не находится на GPU и не работает в потоке по умолчанию?
Затем я решаю использовать пользовательский вычислительный поток, но следующий код дает dt = 800 мс
provider="CUDAExecutionProvider"
stream_ = torch.cuda.Stream(device=device)
session.set_providers(
[provider],
[
{"user_compute_stream": str(stream_.cuda_stream)}
]
)
option = session.get_provider_options()[provider]
print(option)
assert int(option["has_user_compute_stream"]) > 0
for name, val in zip(x_names, xs):
print(val.device)
x_dict[name].copy_(val, non_blocking=True)
stream_.wait_stream(torch.cuda.default_stream(device))
start_event.record(self.stream)
session.run_with_iobinding(
binding
)
torch.cuda.default_stream(device).wait_stream(stream_)
ret = tuple(y_dict[name].clone() for name in y_names)
end_event.record(torch.cuda.default_stream(device))
torch.cuda.synchronize()
dt_ms = start_event.elapsed_time(end_event)
Почему использование пользовательского потока делает это значительно медленнее?
Ответ или решение
Изучая проблему замедления обработки при использовании пользовательского вычислительного потока в сессии вывода модели, мы сталкиваемся с несколькими ключевыми аспектами работы с CUDA и библиотекой ONNX Runtime. Вопрос о том, почему использование пользовательского потока делает вывод модели в 1000 раз медленнее, требует всестороннего анализа.
1. Основы работы с потоками CUDA
CUDA поддерживает использование нескольких потоков для параллельного выполнения операций на GPU. Однако важно понимать, как эти потоки взаимодействуют между собой и с основной (дефолтной) очередью выполнения. В вашем случае вы создали пользовательский поток, однако вы не полностью синхронизировали его с основным потоком.
Почему синхронизация важна?
При использовании copy_
для передачи данных между массивами необходимо убедиться, что операция завершилась до передачи данных в модель нейросети. Без явной синхронизации с помощью torch.cuda.synchronize()
, данные могут быть не полностью загружены в переданные тензоры, что может привести к ошибкам или непредсказуемому поведению модели.
2. Повышенные накладные расходы на синхронизацию
Изменение потока на пользовательский приводит к дополнительным накладным расходам на управление этими потоками. Каждый раз, когда вы передаете контроль между потоками, например, когда вы вызываете wait_stream
, происходит необходимая синхронизация и ожидание завершения операций. Из-за этого вы теряете преимущества параллелизма, поскольку операции в пользовательском потоке могут долго ждать завершения работы в основном потоке.
3. Передача контроля между потоками
Когда вы используете пользовательский поток, в случае конфликта с основным потоком для доступа к ресурсам GPU (например, память), происходит дополнительное время ожидания. Реакция системы на такие ситуации может вызвать замедление работы. Вы заметили, что время выполнения увеличилось, что может быть связано с тем, что операции, выполняемые в пользовательском потоке, не могут эффективно получать доступ к ресурсам, так как они конкурируют с задачами в основном потоке.
4. Оптимизация производительности
Чтобы улучшить производительность и восстановить ожидаемую скорость вывода, вы можете рассмотреть следующие подходы:
- Синхронизация: Убедитесь, что все операции завершаются успешно до начала работы с моделью.
- Оптимизация потока: Возможно, стоит продолжить использовать основной поток, если ваши данные и модель уже оптимизированы для работы в данном контексте.
- Параллельная обработка: Используйте возможности CUDA для реализации нескольких операций на GPU без необходимости переключения между потоками.
Заключение
Использование пользовательского вычислительного потока может значительно увеличить время обработки, если не учтены особенности управления потоками и синхронизации. При работе с моделями, выделяющими ресурсы GPU, важно иметь четкое представление о текущем состоянии потока, чтобы снизить накладные расходы и избежать задержек. Рекомендую пересмотреть архитектуру вашего рабочего процесса и хорошо ознакомиться с нюансами взаимодействия потоков в CUDA для достижения оптимальной производительности.