Phân tích hiệu suất tf.data với TF Profiler

Tổng quan

Hướng dẫn này giả định bạn đã quen thuộc với TensorFlow Profilertf.data . Nó nhằm mục đích cung cấp hướng dẫn từng bước kèm theo các ví dụ để giúp người dùng chẩn đoán và khắc phục các sự cố về hiệu suất của đường dẫn đầu vào.

Để bắt đầu, hãy thu thập hồ sơ về công việc TensorFlow của bạn. Hướng dẫn về cách thực hiện hiện có sẵn cho CPU/GPUCloud TPU .

TensorFlow Trace Viewer

Quy trình phân tích chi tiết bên dưới tập trung vào công cụ xem dấu vết trong Profiler. Công cụ này hiển thị dòng thời gian hiển thị thời lượng của các hoạt động được thực hiện bởi chương trình TensorFlow của bạn và cho phép bạn xác định các hoạt động nào mất nhiều thời gian nhất để thực thi. Để biết thêm thông tin về trình xem dấu vết, hãy xem phần này của hướng dẫn TF Profiler. Nói chung, các sự kiện tf.data sẽ xuất hiện trên dòng thời gian của CPU chủ.

Quy trình phân tích

Hãy làm theo quy trình làm việc dưới đây. Nếu bạn có phản hồi để giúp chúng tôi cải thiện nó, vui lòng tạo một vấn đề trên github với nhãn “comp:data”.

1. Đường dẫn tf.data của bạn có tạo ra dữ liệu đủ nhanh không?

Bắt đầu bằng cách xác định xem đường dẫn đầu vào có phải là nút cổ chai cho chương trình TensorFlow của bạn hay không.

Để làm như vậy, hãy tìm các thao tác IteratorGetNext::DoCompute trong trình xem theo dõi. Nói chung, bạn mong đợi nhìn thấy những điều này khi bắt đầu một bước. Những lát cắt này biểu thị thời gian cần thiết để đường dẫn đầu vào của bạn tạo ra một loạt phần tử khi được yêu cầu. Nếu bạn đang sử dụng máy ảnh hoặc lặp lại tập dữ liệu của mình trong tf.function , thì những thứ này sẽ được tìm thấy trong các chuỗi tf_data_iterator_get_next .

Lưu ý rằng nếu bạn đang sử dụng chiến lược phân phối , bạn có thể thấy các sự kiện IteratorGetNextAsOptional::DoCompute thay vì IteratorGetNext::DoCompute (kể từ TF 2.3).

image

Nếu cuộc gọi quay lại nhanh chóng (<= 50 us), điều này có nghĩa là dữ liệu của bạn sẽ sẵn có khi được yêu cầu. Đường dẫn đầu vào không phải là nút cổ chai của bạn; xem hướng dẫn Profiler để biết thêm các mẹo phân tích hiệu suất chung.

image

Nếu cuộc gọi quay lại chậm, tf.data không thể đáp ứng yêu cầu của người tiêu dùng. Tiếp tục đến phần tiếp theo.

2. Bạn có đang tìm nạp trước dữ liệu không?

Cách tốt nhất để tăng hiệu suất của đường dẫn đầu vào là chèn một phép chuyển đổi tf.data.Dataset.prefetch vào cuối đường dẫn tf.data của bạn. Phép biến đổi này chồng lên quá trình tính toán tiền xử lý của đường dẫn đầu vào với bước tính toán mô hình tiếp theo và cần thiết để có hiệu suất đường dẫn đầu vào tối ưu khi đào tạo mô hình của bạn. Nếu đang tìm nạp trước dữ liệu, bạn sẽ thấy một lát cắt Iterator::Prefetch trên cùng một luồng với IteratorGetNext::DoCompute op.

image

Nếu bạn không có prefetch ở cuối quy trình , bạn nên thêm một quy trình. Để biết thêm thông tin về các đề xuất về hiệu suất tf.data , hãy xem hướng dẫn về hiệu suất của tf.data .

Nếu bạn đã tìm nạp trước dữ liệu và quy trình đầu vào vẫn là nút thắt cổ chai, hãy tiếp tục chuyển sang phần tiếp theo để phân tích thêm hiệu suất.

3. Bạn có đạt mức sử dụng CPU cao không?

tf.data đạt được thông lượng cao bằng cách cố gắng tận dụng tốt nhất các tài nguyên sẵn có. Nói chung, ngay cả khi chạy mô hình của bạn trên bộ tăng tốc như GPU hoặc TPU, các đường dẫn tf.data vẫn chạy trên CPU. Bạn có thể kiểm tra mức sử dụng của mình bằng các công cụ như sarhtop hoặc trong bảng điều khiển giám sát đám mây nếu bạn đang chạy trên GCP.

Nếu mức sử dụng của bạn ở mức thấp, điều này cho thấy rằng đường dẫn đầu vào của bạn có thể không tận dụng tối đa CPU chủ. Bạn nên tham khảo hướng dẫn hiệu suất của tf.data để biết các phương pháp hay nhất. Nếu bạn đã áp dụng các phương pháp hay nhất nhưng mức sử dụng cũng như thông lượng vẫn ở mức thấp, hãy tiếp tục phân tích Nút cổ chai bên dưới.

Nếu mức sử dụng của bạn sắp đạt đến giới hạn tài nguyên , để cải thiện hiệu suất hơn nữa, bạn cần cải thiện hiệu quả của quy trình đầu vào (ví dụ: tránh tính toán không cần thiết) hoặc tính toán giảm tải.

Bạn có thể cải thiện hiệu quả của đường dẫn đầu vào bằng cách tránh tính toán không cần thiết trong tf.data . Một cách để thực hiện việc này là chèn một phép biến đổi tf.data.Dataset.cache sau công việc tính toán chuyên sâu nếu dữ liệu của bạn vừa với bộ nhớ; điều này làm giảm tính toán với chi phí sử dụng bộ nhớ tăng lên. Ngoài ra, việc tắt tính năng song song nội bộ trong tf.data có khả năng tăng hiệu quả lên > 10% và có thể được thực hiện bằng cách đặt tùy chọn sau trên đường dẫn đầu vào của bạn:

dataset = ...
options = tf.data.Options()
options.experimental_threading.max_intra_op_parallelism = 1
dataset = dataset.with_options(options)

4. Phân tích nút cổ chai

Phần sau đây hướng dẫn cách đọc các sự kiện tf.data trong trình xem theo dõi để hiểu nút thắt cổ chai nằm ở đâu và các chiến lược giảm thiểu khả thi.

Hiểu các sự kiện tf.data trong Profiler

Mỗi sự kiện tf.data trong Profiler có tên Iterator::<Dataset> , trong đó <Dataset> là tên của nguồn tập dữ liệu hoặc quá trình chuyển đổi. Mỗi sự kiện cũng có tên dài Iterator::<Dataset_1>::...::<Dataset_n> , bạn có thể thấy bằng cách nhấp vào sự kiện tf.data . Trong tên dài, <Dataset_n> khớp với <Dataset> từ tên (ngắn) và các tập dữ liệu khác trong tên dài biểu thị các phép biến đổi xuôi dòng.

image

Ví dụ: ảnh chụp màn hình ở trên được tạo từ đoạn mã sau:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)

Ở đây, sự kiện Iterator::Map có tên dài Iterator::BatchV2::FiniteRepeat::Map . Lưu ý rằng tên tập dữ liệu có thể hơi khác so với API python (ví dụ: FiniteRepeat thay vì Lặp lại), nhưng phải đủ trực quan để phân tích cú pháp.

Chuyển đổi đồng bộ và không đồng bộ

Đối với các phép biến đổi tf.data đồng bộ (chẳng hạn như BatchMap ), bạn sẽ thấy các sự kiện từ các phép biến đổi ngược dòng trên cùng một luồng. Trong ví dụ trên, vì tất cả các phép biến đổi được sử dụng đều đồng bộ nên tất cả các sự kiện đều xuất hiện trên cùng một luồng.

Đối với các sự kiện chuyển đổi không đồng bộ (chẳng hạn như Prefetch , ParallelMap , ParallelInterleaveMapAndBatch ) từ các phép biến đổi ngược dòng sẽ nằm trên một luồng khác. Trong những trường hợp như vậy, "tên dài" có thể giúp bạn xác định sự kiện tương ứng với sự chuyển đổi nào trong quy trình.

image

Ví dụ: ảnh chụp màn hình ở trên được tạo từ đoạn mã sau:

dataset = tf.data.Dataset.range(10)
dataset = dataset.map(lambda x: x)
dataset = dataset.repeat(2)
dataset = dataset.batch(5)
dataset = dataset.prefetch(1)

Ở đây, các sự kiện Iterator::Prefetch nằm trên các chuỗi tf_data_iterator_get_next . Vì Prefetch không đồng bộ nên các sự kiện đầu vào của nó ( BatchV2 ) sẽ nằm trên một luồng khác và có thể được định vị bằng cách tìm kiếm tên dài Iterator::Prefetch::BatchV2 . Trong trường hợp này, chúng nằm trên chuỗi tf_data_iterator_resource . Từ cái tên dài của nó, bạn có thể suy ra rằng BatchV2 là thượng nguồn của Prefetch . Hơn nữa, parent_id của sự kiện BatchV2 sẽ khớp với ID của sự kiện Prefetch .

Xác định điểm nghẽn

Nói chung, để xác định nút cổ chai trong đường dẫn đầu vào của bạn, hãy đi theo đường dẫn đầu vào từ quá trình chuyển đổi ngoài cùng đến tận nguồn. Bắt đầu từ chuyển đổi cuối cùng trong quy trình của bạn, lặp lại các chuyển đổi ngược dòng cho đến khi bạn tìm thấy một chuyển đổi chậm hoặc tiếp cận tập dữ liệu nguồn, chẳng hạn như TFRecord . Trong ví dụ trên, bạn sẽ bắt đầu từ Prefetch , sau đó đi ngược dòng đến BatchV2 , FiniteRepeat , Map và cuối cùng là Range .

Nói chung, một phép biến đổi chậm tương ứng với một phép biến đổi có các sự kiện dài nhưng các sự kiện đầu vào lại ngắn. Một số ví dụ sau đây.

Lưu ý rằng phép biến đổi cuối cùng (ngoài cùng) trong hầu hết các đường dẫn đầu vào máy chủ là sự kiện Iterator::Model . Quá trình chuyển đổi Mô hình được giới thiệu tự động bởi thời gian chạy tf.data và được sử dụng để đo lường và tự động điều chỉnh hiệu suất đường ống đầu vào.

Nếu công việc của bạn đang sử dụng chiến lược phân phối thì trình xem theo dõi sẽ chứa các sự kiện bổ sung tương ứng với quy trình đầu vào của thiết bị. Biến đổi ngoài cùng của quy trình thiết bị (được lồng trong IteratorGetNextOp::DoCompute hoặc IteratorGetNextAsOptionalOp::DoCompute ) sẽ là một sự kiện Iterator::Prefetch với sự kiện Iterator::Generator ngược dòng. Bạn có thể tìm thấy đường dẫn máy chủ tương ứng bằng cách tìm kiếm các sự kiện Iterator::Model .

Ví dụ 1

image

Ảnh chụp màn hình ở trên được tạo từ đường dẫn đầu vào sau:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record)
dataset = dataset.batch(32)
dataset = dataset.repeat()

Trong ảnh chụp màn hình, hãy quan sát rằng (1) Các sự kiện Iterator::Map dài, nhưng (2) các sự kiện đầu vào của nó ( Iterator::FlatMap ) quay trở lại nhanh chóng. Điều này cho thấy rằng việc chuyển đổi Bản đồ tuần tự là nút cổ chai.

Lưu ý rằng trong ảnh chụp màn hình, sự kiện InstantiatedCapturedFunction::Run tương ứng với thời gian cần thiết để thực thi chức năng bản đồ.

Ví dụ 2

image

Ảnh chụp màn hình ở trên được tạo từ đường dẫn đầu vào sau:

dataset = tf.data.TFRecordDataset(filename)
dataset = dataset.map(parse_record, num_parallel_calls=2)
dataset = dataset.batch(32)
dataset = dataset.repeat()

Ví dụ này tương tự như trên nhưng sử dụng ParallelMap thay vì Map. Ở đây, chúng tôi nhận thấy rằng (1) các sự kiện Iterator::ParallelMap dài, nhưng (2) các sự kiện đầu vào của nó Iterator::FlatMap (nằm trên một luồng khác, vì ParallelMap không đồng bộ) lại ngắn. Điều này cho thấy việc chuyển đổi ParallelMap là nút cổ chai.

Giải quyết điểm nghẽn

Bộ dữ liệu nguồn

Nếu bạn đã xác định nguồn tập dữ liệu là nút thắt cổ chai, chẳng hạn như đọc từ tệp TFRecord, bạn có thể cải thiện hiệu suất bằng cách trích xuất dữ liệu song song. Để làm như vậy, hãy đảm bảo rằng dữ liệu của bạn được phân chia thành nhiều tệp và sử dụng tf.data.Dataset.interleave với tham số num_parallel_calls được đặt thành tf.data.AUTOTUNE . Nếu tính xác định không quan trọng đối với chương trình của bạn, bạn có thể cải thiện hiệu suất hơn nữa bằng cách đặt cờ deterministic=False trên tf.data.Dataset.interleave kể từ TF 2.2. Ví dụ: nếu bạn đang đọc từ TFRecords, bạn có thể thực hiện các thao tác sau:

dataset = tf.data.Dataset.from_tensor_slices(filenames)
dataset = dataset.interleave(tf.data.TFRecordDataset,
  num_parallel_calls=tf.data.AUTOTUNE,
  deterministic=False)

Lưu ý rằng các tệp được phân chia phải có kích thước vừa phải để phân bổ chi phí mở tệp. Để biết thêm chi tiết về trích xuất dữ liệu song song, hãy xem phần này của hướng dẫn hiệu suất tf.data .

Bộ dữ liệu chuyển đổi

Nếu bạn đã xác định một chuyển đổi tf.data trung gian là nút thắt cổ chai, bạn có thể giải quyết nó bằng cách song song hóa chuyển đổi hoặc lưu vào bộ nhớ đệm tính toán nếu dữ liệu của bạn vừa với bộ nhớ và phù hợp. Một số phép biến đổi như Map có các phép biến đổi song song; hướng dẫn hiệu suất tf.data trình bày cách song song hóa những điều này. Các phép biến đổi khác, chẳng hạn như Filter , UnbatchBatch vốn là tuần tự; bạn có thể song song hóa chúng bằng cách giới thiệu “song song bên ngoài”. Ví dụ: giả sử đường dẫn đầu vào của bạn ban đầu trông giống như sau, với Batch là nút thắt cổ chai:

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)
dataset = filenames_to_dataset(filenames)
dataset = dataset.batch(batch_size)

Bạn có thể giới thiệu "song song bên ngoài" bằng cách chạy nhiều bản sao của đường dẫn đầu vào trên các đầu vào được phân chia và kết hợp các kết quả:

filenames = tf.data.Dataset.list_files(file_path, shuffle=is_training)

def make_dataset(shard_index):
  filenames = filenames.shard(NUM_SHARDS, shard_index)
  dataset = filenames_to_dataset(filenames)
  Return dataset.batch(batch_size)

indices = tf.data.Dataset.range(NUM_SHARDS)
dataset = indices.interleave(make_dataset,
                             num_parallel_calls=tf.data.AUTOTUNE)
dataset = dataset.prefetch(tf.data.AUTOTUNE)

Tài nguyên bổ sung