Почему размещение сессии вывода на вычислительном потоке пользователя делает вывод в 1000 раз медленнее?

Вопрос или проблема

Сначала я создаю сессию и связываю выходные и входные тензоры следующим образом:

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 для достижения оптимальной производительности.

Оцените материал
Добавить комментарий

Капча загружается...