Tổng quan
Hướng dẫn này giả định rằng bạn đã quen thuộc với TensorFlow Profiler và tf.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 sự cố 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/GPU và Cloud TPU .
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 Profiler. Công cụ này hiển thị một dòng thời gian cho biết 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 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 theo dõi, 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 máy chủ.
Quy trình phân tích
Vui lòng 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 đề github với nhãn “comp:data”.
1. Đường dẫn tf.data
của bạn có tạo 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 ops IteratorGetNext::DoCompute
trong trình xem theo dõi. Nói chung, bạn sẽ thấy những thứ này khi bắt đầu một bước. Các lát cắt này biểu thị thời gian cần thiết để quy trình đầ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 một tf.function
, thì những thứ này 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).
Nếu các 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 có sẵn khi được yêu cầu. Đường ống đầ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.
Nếu các cuộc gọi trở lại chậm, tf.data
không thể theo kịp các 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?
Phương pháp hay nhất để đạt được 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. Quá trình chuyển đổi này chồng lấp 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 quá trình tính toán mô hình và là cần thiết để 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 lát cắt Iterator::Prefetch
trên cùng một chuỗi với tùy chọn IteratorGetNext::DoCompute
.
Nếu bạn không có prefetch
ở cuối quy trình của mình , bạn nên thêm một. Để biết thêm thông tin về đề xuất hiệu suất tf.data
, hãy xem hướng dẫn hiệu suất 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 cổ chai của bạn, hãy tiếp tục đến phần tiếp theo để phân tích thêm 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 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 việc sử dụng của mình bằng các công cụ như sar và htop 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 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 hết lợi thế của CPU máy 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à 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 thắ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 giảm tải tính toán.
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 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 song song trong nội bộ hoạt động trong tf.data
có khả năng tăng hiệu suất > 10% và có thể được thực hiện bằng cách đặt tùy chọn sau trên quy trình đầ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 vị trí nút thắt cổ chai 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 hoặc chuyển đổi 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 tên nà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 bộ 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.
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 bộ dữ liệu có thể khác một chút 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ư Batch
và Map
), 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 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
, ParallelInterleave
và MapAndBatch
) các sự kiện 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 chuyển đổi nào trong quy trình bán hàng.
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 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 luồng tf_data_iterator_resource
. Từ cái tên dài của nó, bạn có thể suy ra rằng BatchV2
ngược dòng của Prefetch
. Ngoài ra, 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 quy trình đầu vào của bạn, hãy đi bộ quy trình đầu vào từ 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 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 đó 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 chuyể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 thời gian chạy tf.data
giới thiệu tự động 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ị. Chuyển đổi ngoài cùng của đường dẫn 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 Iterator::Generator
ngược dòng. Bạn có thể tìm đường dẫn lưu trữ tương ứng bằng cách tìm kiếm các sự kiện Iterator::Model
.
ví dụ 1
Ả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 lại nhanh chóng. Điều này cho thấy rằng 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
Ả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. Chúng tôi nhận thấy ở đâ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 rằng phép biến đổi ParallelMap là nú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, thì 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 tính tất đị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ờ tất 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 đoạn phải có dung lượng 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 một phép biến đổi tf.data
trung gian là nút cổ chai, thì bạn có thể giải quyết vấn đề đó bằng cách song song hóa phép biến đổi hoặc lưu vào bộ đệm tính toán nếu dữ liệu của bạn phù hợp với bộ nhớ và phù hợp. Một số phép biến hình như Map
có phép đố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 thứ này. Các phép biến đổi khác, chẳng hạn như Filter
, Unbatch
và Batch
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ử quy trình đầ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 "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 đ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)
tài nguyên bổ sung
- hướng dẫn hiệu suất tf.data về cách viết đường ống đầu vào
tf.data
hiệu suất - Video bên trong TensorFlow: các phương pháp hay nhất
tf.data
- hướng dẫn hồ sơ
- hướng dẫn hồ sơ với colab