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 với TensorFlow Profilertf.data . Nó nhằm mục đích cung cấp hướng dẫn từng bước với các ví dụ để giúp người dùng chẩn đoán và khắc phục các vấn đề về hiệu suất đường ống đầ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 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 theo dõi trong Hồ sơ. Công cụ này hiển thị dòng thời gian cho biết thời lượng của các hoạt động được thực thi 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 mất nhiều thời gian nhất để thực hiện. Để biết thêm thông tin về trình xem theo dõi, hãy xem phần này của hướng dẫn về Hồ sơ TF. 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ủ.

Phân tích quy trình làm việc

Vui lòng theo dõi quy trình làm việc bên dưới. 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 sự cố trên github với nhãn “comp: data”.

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

Bắt đầu bằng cách xác định xem liệu đườ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 hoạt động của IteratorGetNext::DoCompute trong trình xem theo dõi. Nói chung, bạn sẽ thấy những điều này khi bắt đầu một bước. Các lát cắt này thể hiện thời gian cần thiết để đường dẫn đầu vào của bạn mang lại một loạt các phần tử khi nó được yêu cầu. Nếu bạn đang sử dụng keras hoặc lặp qua tập dữ liệu của mình trong một tf.function . thì chúng sẽ được tìm thấy trong 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 các cuộc gọi trở lại nhanh chóng (<= 50 us), điều này có nghĩa là dữ liệu của bạn sẽ khả dụng khi được yêu cầu. Đường ống đầu vào không phải là nút thắt cổ chai của bạn; xem hướng dẫn về Hồ sơ để biết thêm các mẹo phân tích hiệu suất chung.

image

Nếu các cuộc gọi trả về chậm, tf.data không thể theo kịp 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?

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

image

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

Nếu bạn đã tìm nạp trước dữ liệu và đường dẫn đầu vào vẫn là điểm nghẽn của bạn, hãy tiếp tục đến phần tiếp theo để phân tích sâu hơn về hiệu suất.

3. Bạn có đang đạ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 sử dụng tốt nhất có thể các tài nguyên có sẵn. Nói chung, ngay cả khi chạy mô hình của bạn trên một bộ tăng tốc như GPU hoặc TPU, các đường ống tf.data vẫn được chạy trên CPU. Bạn có thể kiểm tra việ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 hiệu suất sử dụng của bạn thấp, điều này cho thấy rằng đường ống đầu vào của bạn có thể không tận dụng được hết sức mạnh của CPU chủ. Bạn nên tham khảo hướng dẫn hiệu suất 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 và việc sử dụng mà thông lượng vẫn ở mức thấp, hãy tiếp tục đến phần Phân tích điểm nghẽn bên dưới.

Nếu việc sử dụng của bạn sắp đế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 phải cải thiện hiệu quả của đường dẫn đầu vào của mình (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 của mình 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 chuyển đổi tf.data.Dataset.cache sau công việc đòi hỏi tính toán nhiề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 chế độ 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 sẽ hướng dẫn cách đọc các sự kiện tf.data trong trình xem theo dõi để hiểu vị trí tắc nghẽn và các chiến lược giảm thiểu có thể có.

Hiểu các sự kiện tf.data trong Hồ sơ

Mỗi sự kiện tf.data trong Profiler có tên là Iterator::<Dataset> , trong đó <Dataset> là tên của nguồn hoặc biến đổi của tập dữ liệu. 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> với tên (ngắn) và các tập dữ liệu khác trong tên dài đại diện cho 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ừ 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 là Iterator::BatchV2::FiniteRepeat::Map . Lưu ý rằng tên tập dữ liệu có thể hơi khác 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.

Biế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 là đồng bộ, tất cả các sự kiện xuất hiện trên cùng một luồng.

Đối với các phép biến đổi không đồng bộ (chẳng hạn như Prefetch , ParallelMap , ParallelInterleaveMapAndBatch ), các sự kiện từ 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 biến đổi nào trong một sự kiện tương ứng với một sự kiện.

image

Ví dụ: ảnh chụp màn hình ở trên được tạo từ 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 tf_data_iterator_get_next . Vì Prefetch là không đồng bộ, 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à ngược dòng 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 điểm nghẽn trong đường ống đầu vào của bạn, hãy đi bộ đường ống đầu vào từ biến đổi ngoài cùng đến nguồn. Bắt đầu từ chuyển đổi cuối cùng trong đường dẫn của bạn, lặp lại thành các biến đổi ngược dòng cho đến khi bạn tìm thấy 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. Dưới đây là một số ví dụ.

Lưu ý rằng biến đổi cuối cùng (ngoài cùng) trong hầu hết các đường ống dẫn đầu vào máy chủ là sự kiện Iterator::Model . Việc chuyển đổi Mô hình được đưa vào tự động bởi thời gian chạy tf.data và được sử dụng để đánh giá 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 là sử dụng chiến lược phân phối , trình xem theo dõi sẽ chứa các sự kiện bổ sung tương ứng với đường dẫn đầu vào của thiết bị. Biến đổi ngoài cùng của đường ống thiết bị (được lồng trong IteratorGetNextOp::DoCompute hoặc IteratorGetNextAsOptionalOp::DoCompute ) sẽ là một sự kiện Iterator::Prefetch với một sự kiện lặp ngược dòng của Iterator::Generator . 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) Iterator::Map dài, nhưng (2) các sự kiện đầu vào của nó ( Iterator::FlatMap ) trả về nhanh chóng. Điều này cho thấy rằng sự biến đổi Bản đồ tuần tự là nút thắ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 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ì Bản đồ. Ở đây chúng ta 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 là không đồng bộ) lại ngắn. Điều này cho thấy rằng chuyển đổi ParallelMap là nút thắt cổ chai.

Giải quyết nút thắt cổ chai

Bộ dữ liệu nguồn

Nếu bạn đã xác định nguồn tập dữ liệu là nú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 chia nhỏ trên 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 thuyết 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 hơn nữa hiệu suất 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ể làm như 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 đoạn phải lớn hợp lý để 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 chuyển đổi tf.data trung gian là nút cổ chai, bạn có thể giải quyết nó bằng cách song song chuyển đổi hoặc tính toán vào bộ nhớ đệm nếu dữ liệu của bạn phù hợp với bộ nhớ và nó phù hợp. Một số phép biến hình như Map có các phép đối song song; hướng dẫn hiệu suất tf.data thích cách song song hóa chúng. Các phép biến đổi khác, chẳng hạn như Filter , UnbatchBatch vốn có tuần tự; bạn có thể song song hóa chúng bằng cách giới thiệu "tính 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 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 "tính 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 qua các đầu vào được phân đoạn 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)

Các nguồn bổ sung