3 điểm bởi GN⁺ 3 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Dẫn xuất từng bước từ softmax attention đến attention tuyến tính dùng trạng thái kích thước cố định, DeltaNet chỉ ghi lại lỗi, Gated DeltaNet làm suy giảm toàn bộ trạng thái, và Kimi Delta Attention (KDA) làm suy giảm theo từng kênh
  • Attention tuyến tính cơ bản lưu tổng các tích ngoài key-value trong trạng thái (S_t), nên hoạt động tuyến tính theo độ dài chuỗi, nhưng phát sinh nhiễu ghi cộng dồn: thay vì gán giá trị mới, nó cộng vào các liên kết hiện có
  • DeltaNet ghi lại chênh lệch giữa giá trị dự đoán từ key hiện tại và value mục tiêu, nhân với (\beta_t); ba cách diễn giải — điều kiện tái tạo tức thời, gradient descent trực tuyến, và cập nhật trạng thái hạng 1 — đều dẫn đến cùng một công thức
  • Gated DeltaNet trước hết làm suy giảm toàn bộ trạng thái bằng vô hướng (\alpha_t), còn KDA mở rộng điều này thành ma trận đường chéo (D_t=\operatorname{Diag}(\alpha_t)), cho phép duy trì hoặc xóa thông tin với tỉ lệ khác nhau cho từng kênh key
  • Cùng một truy hồi KDA được chạy bằng kernel Triton truy hồi hợp nhất cho decode và bằng phương pháp chunk cho huấn luyện/prefill dài; phương pháp chunk khôi phục phụ thuộc nội bộ token bằng phép giải tam giác và tái cấu trúc thành phép nhân ma trận

Ký hiệu và trình tự triển khai

  • Trong ký hiệu bra-ket, (\lvert q\rangle) là vector cột, (\langle k\rvert) là vector hàng, (\langle k\vert q\rangle) là vô hướng, và (\lvert v\rangle\langle k\rvert) là ma trận
  • Giả định dùng một head attention nhân quả và các vector thực; key của DeltaNet được chuẩn hóa, và trạng thái ánh xạ từ không gian key sang không gian value
  • Trình tự triển khai là softmax attention → attention tuyến tính → DeltaNetGated DeltaNetKDA, và cuối cùng liên kết tới triển khai Triton dạng truy hồi và dạng chunk
  • Hai biến thể trong họ DeltaNet được dùng trong các họ mô hình Qwen và Kimi mới nhất

Từ attention độ phức tạp bậc hai đến trạng thái tuyến tính

  • Softmax attention nhân quả thông thường tính độ tương đồng giữa key và query, chuẩn hóa điểm số trên tất cả key quá khứ thành một phân phối, rồi xuất tổng có trọng số của các vector value
  • Với chuỗi độ dài (T), có (T^2) cặp key-query
    • Trong suy luận tự hồi quy, có thể cache key và value nhưng kích thước cache tăng theo chuỗi
    • Query mới cũng phải kiểm tra toàn bộ quá khứ
  • Mẫu số của softmax phụ thuộc đồng thời vào query hiện tại và tất cả key trước đó, nên khó đơn giản sắp xếp lại thứ tự tính toán
  • Nếu loại bỏ softmax, có thể gom đầu ra thành tổng các tích ngoài key-value trong quá khứ
    • (S_t=\sum_{i\le t}\lvert v_i\rangle\langle k_i\rvert)
    • (S_t=S_{t-1}+\lvert v_t\rangle\langle k_t\rvert)
    • (\lvert o_t\rangle=S_t\lvert q_t\rangle)
  • Đẳng thức cốt lõi là ((\lvert v\rangle\langle k\rvert)\lvert q\rangle=\langle k\vert q\rangle\lvert v\rangle); thay vì lưu mọi key và value quá khứ, nó lưu tích ngoài đã cộng dồn trong trạng thái kích thước cố định (d_v\times d_k)
  • Vì quét token một lần, nó hoạt động tuyến tính theo độ dài chuỗi, nhưng đánh đổi bằng việc mất chuẩn hóa và tính chọn lọc của softmax
    • Các dạng attention tuyến tính tinh vi hơn dùng feature map và hạng tử chuẩn hóa

Vấn đề ghi cộng dồn của attention tuyến tính

  • Ngay sau khi ghi (\lvert v_t\rangle\langle k_t\rvert) vào key hiện tại đã chuẩn hóa, nếu đọc bằng cùng key thì (S_t\lvert k_t\rangle=S_{t-1}\lvert k_t\rangle+\lvert v_t\rangle)
  • Lần ghi mới không gán để bộ nhớ trả về (v_t), mà cộng (v_t) vào giá trị trả về hiện có theo kiểu +=
  • Nếu trạng thái trước đó đã trả về giá trị đúng, cùng value sẽ bị nhân đôi; vì các key không trực giao với nhau, mỗi lần ghi có thể gây nhiễu với các lần ghi hiện có
  • Attention tuyến tính cung cấp bộ nhớ liên kết đã nén, nhưng thực hiện cập nhật cộng dồn thay vì cập nhật gần với phép gán = cần thiết

DeltaNet: ghi lỗi dự đoán thay vì ghi giá trị

  • DeltaNet trước hết đọc dự đoán hiện có (\widehat v_t=S_{t-1}k_t) cho key mới, rồi chỉ ghi phần chênh lệch thay vì toàn bộ value
    • (e_t=\beta_t(v_t-S_{t-1}k_t))
    • (S_t=S_{t-1}+e_tk_t^\mathsf T)
    • Cường độ ghi được học (\beta_t) nằm trong phạm vi ([0,1])
  • Nếu đọc lại ngay bằng cùng key, kết quả là ((1-\beta_t)S_{t-1}k_t+\beta_tv_t)
    • Khi (\beta_t=1), nó trả về đúng (v_t)
    • Giá trị nhỏ hơn chỉ dịch chuyển một phần dự đoán hiện có theo hướng mục tiêu
  • Cập nhật mang tính cục bộ trong không gian key
    • Với hướng query trực giao với key hiện tại, cập nhật tích ngoài bằng 0 nên đáp ứng không đổi
    • Chỉ liên kết theo hướng key hiện tại được thay thế có chọn lọc
  • Dẫn xuất từ mất mát tái tạo

    • Nếu xem trạng thái (S) là ánh xạ tuyến tính và đặt mất mát cho cặp key-value hiện tại là (\frac12\lVert Sk_t-v_t\rVert_2^2), gradient là ((Sk_t-v_t)k_t^\mathsf T)
    • Thực hiện một bước gradient descent với kích thước (\beta_t) từ (S_{t-1}) sẽ cho đúng công thức cập nhật của DeltaNet
    • Có thể diễn giải cùng một cập nhật theo ba cách
      • Trong thao tác bộ nhớ, (\beta_t) là cường độ thay thế liên kết hiện có
      • Trong học trực tuyến, (\beta_t) là learning rate
      • Trong đại số tuyến tính, đó là tích ngoài hạng 1 giữa lỗi dự đoán và key
  • Chuyển trạng thái có cấu trúc

    • Khai triển cập nhật ta có (S_t=S_{t-1}(I-\beta_tk_tk_t^\mathsf T)+\beta_tv_tk_t^\mathsf T)
    • Với key đơn vị, (I-\beta_tk_tk_t^\mathsf T) có trị riêng (1-\beta_t) theo hướng key hiện tại và trị riêng 1 ở mọi hướng trực giao
    • Nó trước hết loại bỏ liên kết theo hướng key hiện có rồi thêm liên kết mới, nhưng vẫn chưa giải quyết quản lý vòng đời của toàn bộ trạng thái

Gated DeltaNet: trước hết quên toàn bộ trạng thái

  • Khi nén toàn bộ quá khứ vào một ma trận, không thể chọn riêng các token đã được gộp vào trạng thái để bỏ qua
  • DeltaNet hiệu chỉnh quanh key hiện tại, nhưng thông tin cũ ở các hướng khác vẫn còn và có thể tiếp tục góp phần vào các lần đọc sau
  • Gated DeltaNet áp dụng cổng duy trì vô hướng được học (\alpha_t\in[0,1])
    1. Quên bằng (\widetilde S_t=\alpha_tS_{t-1})
    2. Dự đoán bằng (\widehat v_t=\widetilde S_tk_t)
    3. Hiệu chỉnh bằng (e_t=\beta_t(v_t-\widehat v_t))
    4. Ghi bằng (S_t=\widetilde S_t+e_tk_t^\mathsf T)
  • Thứ tự quên → dự đoán → hiệu chỉnh → ghi là quan trọng
    • Nếu dự đoán trước khi làm suy giảm, bộ nhớ dùng để tính lỗi sẽ khác với bộ nhớ thực sự được cập nhật
  • Quy tắc delta phụ trách thay thế theo key mục tiêu, còn cổng vô hướng phụ trách xóa toàn cục; chúng giải quyết hai vấn đề khác nhau
  • Tuy nhiên, vì một (\alpha_t) được áp dụng cho toàn bộ ma trận, mọi kênh key đều phải được duy trì hoặc quên theo cùng một tỉ lệ

Kimi Delta Attention: suy giảm theo từng kênh

  • Kimi Delta Attention thay vô hướng (\alpha_t) bằng vector (d_k) chiều và lập (D_t=\operatorname{Diag}(\alpha_t))
  • Vì trạng thái ánh xạ từ không gian key sang không gian value, các kênh key tương ứng với các cột của (S), và phép nhân bên phải (S_{t-1}D_t) áp dụng tỉ lệ duy trì khác nhau cho từng cột
  • KDA hoạt động theo thứ tự sau
    1. Suy giảm theo từng kênh key bằng (\widetilde S_t=S_{t-1}D_t)
    2. Dự đoán bằng (\widehat v_t=\widetilde S_tk_t)
    3. Hiệu chỉnh bằng (e_t=\beta_t(v_t-\widehat v_t))
    4. Ghi bằng (S_t=\widetilde S_t+e_tk_t^\mathsf T)
    5. Đọc bằng (o_t=S_t(d_k^{-1/2}q_t))
  • Thay đổi khái niệm từ Gated DeltaNet sang KDA chỉ là nâng (\alpha_t) lên (D_t), nhưng nhờ vậy có thể xóa một kênh trong khi duy trì kênh khác
  • Chuyển tiếp đường chéo-hạng thấp

    • Khai triển KDA ta được (S_t=S_{t-1}A_t+\beta_tv_tk_t^\mathsf T), với (A_t=D_t(I-\beta_tk_tk_t^\mathsf T))
    • Có thể viết (A_t=D_t-b_ta_t^\mathsf T), (b_t=D_tk_t), (a_t^\mathsf T=\beta_tk_t^\mathsf T), nên nó trở thành chuyển tiếp đường chéo-hạng thấp (DPLR)
    • DPLR chỉ chuyển tiếp (d_k\times d_k) tác động trong không gian key; bản thân trạng thái bộ nhớ vẫn là ma trận (d_v\times d_k)
    • Mỗi họ bổ sung chức năng sau
      • Attention tuyến tính: bộ nhớ truy hồi kích thước cố định
      • DeltaNet: thay thế có chọn lọc theo hướng mục tiêu
      • Gated DeltaNet: suy giảm toàn bộ trạng thái
      • KDA: suy giảm theo từng kênh key
    • Triển khai thường lưu (g_t=\log\alpha_t\le0), rồi tính tỉ lệ duy trì bằng (\exp(g_t))
    • Có thể xem triển khai tham chiếu 5 bước với layout chuyển vị (d_k\times d_v) tại naive_recurrent_kda

Kernel Triton truy hồi hợp nhất cho decode

  • KDA có hai cách thực thi chính
    • Phương pháp truy hồi hợp nhất: phù hợp với decode, chuỗi ngắn và serving có duy trì trạng thái
    • Phương pháp chunk: phù hợp với huấn luyện và prefill dài
  • fused_recurrent_kda_fwd chạy một chương trình Triton cho mỗi chuỗi, value head và ô value độ rộng 32
    • BK bao phủ chiều key trong cấu hình hỗ trợ thông thường
    • Mỗi chương trình sở hữu một ô [BK, BV] của trạng thái đã chuyển vị và quét token theo thứ tự
    • Các ô value, head và chuỗi khác nhau chạy độc lập
  • Kernel thực hiện đúng truy hồi: suy giảm trạng thái, rút gọn dự đoán theo key, tính residual, ghi tích ngoài, và rút gọn đọc query
  • Cách này phù hợp với decode khi mỗi lần chỉ có một token mới đi vào, nhưng không chuyển được các phép toán vector thành các phép nhân ma trận lớn hiệu quả cho Tensor Core, nên bất lợi cho huấn luyện và prefill dài

Chunkwise KDA: sắp xếp lại truy hồi thành phép nhân ma trận

  • Chunkwise KDA phải xử lý cùng lúc (C) token nhưng vẫn tạo đúng cùng trạng thái và đầu ra như cách truy hồi từng token
  • Mỗi chunk tính hai kết quả
    • (S_{c+1}) sau khi xử lý toàn bộ chunk từ trạng thái đi vào (S_c)
    • Đầu ra nhân quả của mọi token trong chunk
  • Khó khăn cốt lõi là lỗi delta của mỗi token phụ thuộc vào các lần ghi trước đó trong cùng chunk
  • Suy giảm tích lũy và lỗi tạm thời

    • Gọi suy giảm đường chéo của token (i) là (D_i), và suy giảm tích lũy từ biên chunk đến token (i) là (D_{0:i}=D_0D_1\cdots D_i)
    • Khi lần ghi của token (j) được truyền đến token (i), (D_{j+1:i}) được áp dụng; vì là ma trận đường chéo, các ma trận suy giảm giao hoán với nhau
    • Trước tiên tính song song lỗi tạm thời, bỏ qua các lần ghi khác bên trong chunk
      • (\bar e_i=\beta_i(v_i-S_cD_{0:i}k_i))
    • Ngoại trừ token đầu tiên, lỗi tạm thời đã bỏ sót ảnh hưởng của các lần ghi nội bộ chunk trước đó nên không thể dùng nguyên trạng
  • Khôi phục phụ thuộc nhân quả

    • Định nghĩa hệ số ảnh hưởng của token trước (j) lên lỗi của token hiện tại (i) là (\rho_{ij}=\beta_i k_j^\mathsf TD_{j+1:i}k_i)
    • Lỗi thực có phụ thuộc tuần tự dạng (e_i=\bar e_i-\sum_{j<i}\rho_{ij}e_j)
    • Nếu đặt (\rho_{ij}) vào ma trận tam giác dưới nghiêm ngặt (R_c), ma trận lỗi đã xếp chồng được tính bằng (E_c=\bar E_c(A_c^{kk})^\mathsf T), (A_c^{kk}=(I+R_c)^{-1})
    • Không cần nghịch đảo ma trận đặc thông thường
      • (I+R_c) là ma trận tam giác với phần tử đường chéo bằng 1
      • Chỉ cần thực hiện phép giải tam giác nhân quả cho từng kênh value
  • Tính trạng thái cuối chunk

    • Trạng thái đi vào đi qua mọi suy giảm của chunk, còn mỗi lần ghi nội bộ chunk chỉ đi qua các suy giảm phía sau nó
    • Nếu xếp các key đã suy giảm đến cuối chunk thành các hàng trong (K_c^{\mathrm{end}}), trạng thái có thể được viết gọn bằng phép nhân ma trận sau
      • (S_{c+1}=S_cD_{0:C-1}+E_cK_c^{\mathrm{end}})
    • Nhiều lần ghi tích ngoài hạng 1 được gộp thành một phép nhân ma trận để đẩy trạng thái của cả chunk tiến lên một lần
  • Tính mọi đầu ra trong chunk

    • KDA đọc sau khi đã ghi token hiện tại, nên đầu ra của token (i) cũng bao gồm lần ghi của chính nó
    • Định nghĩa hệ số ảnh hưởng của lần ghi trước (j) lên query (i) là (\chi_{ij}=s,k_j^\mathsf TD_{j+1:i}q_i), với (j\le i)
    • Đặt các hệ số vào ma trận đọc tam giác dưới (A_c^{qk})
      • Các số 0 ở tam giác trên chặn đóng góp từ token tương lai
      • Phần tử đường chéo phản ánh việc token hiện tại đọc sau lần ghi của chính nó
    • Nếu xếp các vector đã suy giảm từ biên đến từng query trong (Q_c^{\mathrm{boundary}}), toàn bộ đầu ra là
      • (O_c=sS_cQ_c^{\mathrm{boundary}}+E_c(A_c^{qk})^\mathsf T)
    • Phép nhân ma trận đầu tiên đọc trạng thái đi vào chunk đã suy giảm, còn phép nhân thứ hai cộng đóng góp từ các lần ghi nhân quả bên trong chunk

Pipeline Triton dạng chunk

  • Triển khai chunk không phải một kernel khổng lồ, mà là pipeline gồm nhiều lần gọi kernel
  • Trước tiên tính suy giảm log tích lũy bên trong chunk
    • Biểu diễn (D_{j+1:i}) bằng hiệu của hai prefix sum, thay vì nhân dài các vector duy trì
  • Tiếp đó tạo các ma trận tương tác nhân quả (A^{qk}) và (A^{kk}), rồi dùng (A^{kk}) để xây dựng dạng WY cho các lần ghi đã hiệu chỉnh của chunk
  • Kernel trạng thái thực hiện vòng quét liên chunk duy nhất
    • Tạo trạng thái đi vào từng chunk
    • Giải quyết lỗi delta của chunk
  • Sau khi trạng thái đi vào đã được tính, kernel đầu ra có thể xử lý song song token của các chunk và ô khác nhau
  • Triển khai thực tế trước tiên tính khối tương tác đường chéo 16 token, rồi chạy các kernel hợp nhất cho phần ngoài đường chéo và phép giải tam giác
  • chunk_kda_fwd điều phối các bước; các entry point chính là chunk_kda_fwd_intra, chunk_gated_delta_rule_fwd_h, chunk_gla_fwd_o_gk
    • v_new trong mã là lỗi đã được giải quyết
    • h là trạng thái đi vào chunk
    • kg là key đã suy giảm đến cuối chunk
  • Phương pháp truy hồi và phương pháp chunk không phải hai attention khác nhau, mà là hai lịch thực thi của cùng một truy hồi KDA
    • Phương pháp truy hồi là các phép toán vector nối tiếp cho decode độ trễ thấp
    • Phương pháp chunk là các phép toán ma trận cho huấn luyện và prefill xoay quanh Tensor Core

1 bình luận

 
Ý kiến trên Hacker News
  • Trong 15 năm qua, machine learning đã cần một ký hiệu toán học thống nhất, và có lẽ sẽ còn tiếp tục cần. Trước đây còn tệ hơn, vì trong các bài báo của các nhà nghiên cứu khắp thế giới xuất hiện đủ kiểu ký hiệu kỳ lạ
    Khi mỗi bài báo dùng ký hiệu khác nhau, việc hiểu sẽ bị cản trở. Ít nhất bài viết này giải thích rõ ký hiệu ngay từ đầu, mà những bài báo làm vậy khá hiếm. Ban đầu tôi còn không nhận ra có chức năng chuyển đổi ký hiệu, nhưng nó rất hữu ích

    • Tôi không hiểu vì sao lại chuộng ký hiệu toán học truyền thống dùng các ký tự như ∣q⟩ thay vì ký hiệu một chữ cái hay kiểu dữ liệu rõ ràng. Có thể nó có ưu điểm là ngắn gọn, nhưng nếu viết công thức dưới dạng giả mã hoặc một ngôn ngữ lập trình thực tế như Python thì có lẽ sẽ dễ hiểu hơn nhiều
    • Bài viết này chỉ giải thích một khía cạnh của ký hiệu, chứ không cung cấp định nghĩa các biến được dùng. Nếu đã học machine learning thì bạn có thể biết hoặc đoán k, q, S là gì, nhưng nếu không có kiến thức nền liên quan thì phần lớn bài viết sẽ trở nên mù mờ
    • Trước đây tôi cũng nghĩ vậy, nhưng vì thời gian nhìn vào công thức dài hơn nhiều so với nhìn vào code, nên một khi đã biết ý nghĩa của ký hiệu thì ký hiệu ngắn gọn dễ đọc hơn nhiều. Viết bằng chữ còn tránh được cả chuyện đặt tên vốn nổi tiếng là khó
  • Dù nói rằng “có thể đã tự nghĩ ra được...”, việc tạo ra hoặc kết hợp một thứ chưa từng tồn tại là cực kỳ khó
    Khi ai đó cuối cùng công bố xong một việc khó, ngay lập tức sẽ có phản ứng kiểu “cũng không khó lắm”, “tôi cũng làm được”, và mọi thứ bắt đầu trông đơn giản. Khi phát triển, cũng thường có chuyện tưởng mình đã phát minh ra thứ mới, rồi sau đó phát hiện nó đã được tạo ra từ thập niên 1970 và được dùng rộng rãi. Chỉ là nó chưa từng cắt ngang con đường của tôi nên tôi không biết nó tồn tại

  • Với tôi, ký hiệu bra-ket làm mọi thứ trở nên đơn giản và trực quan. Trong ký hiệu vector, tôi hay lẫn bên nào là hàng, bên nào là cột, rồi chỉ lần theo từng khối và mất tập trung; còn với bra-ket thì toàn bộ rất trực quan
    Tôi nghĩ có nhiều bài hay mình đã bỏ lỡ, nên định thử chuyển các bài khác sang ký hiệu này. Nói thêm, tôi có bằng tiến sĩ vật lý và bị chứng khó đọc nhẹ

  • Nhìn văn phong kiểu “tích ngoài là ma trận, còn tích trong là số. Thay vì lưu tất cả key và value trong quá khứ, ta lưu tổng các tích ngoài trong trạng thái kích thước cố định S_t”, tôi tin chắc đây là bài do LLM viết

    • Có lẽ bắt đầu từ việc yêu cầu một tiêu đề có từ khóa thịnh hành
    • Nếu prompt Claude đừng dùng dấu gạch ngang () thì sẽ ra kết quả như thế này
  • Cũng có một hướng dẫn có minh họa trực quan: https://snowchord.com/blog/linear-attention-visualized/

  • Mỗi khi thấy những bài và tiêu đề như thế này, tôi cảm thấy biết ơn sâu sắc và khiêm nhường trước vô số người thông minh hơn mình rất nhiều. Ở trung học và đại học tôi từng được xem là người rất thông minh, và tôi lanh lợi hơn mức trung bình, nhưng chắc chắn vẫn có hàng triệu người khiến tôi trông như tay mơ
    Ở đây, thông minh nghĩa là khả năng giữ trong đầu và suy luận về các khái niệm, hệ thống khổng lồ và phức tạp; đây có vẻ là tài năng đặc biệt quan trọng với các nhà toán học

    • Dù các công cụ AI ngày càng tăng tốc công việc, tôi vẫn nghĩ nguồn gốc của phần lớn ý tưởng mới sẽ tiếp tục là con người
      Một thí nghiệm tư duy tôi từng làm khi uống rượu với bạn bè là cách ly trẻ em khỏi màn hình và nội dung đại chúng do thuật toán cung cấp, rồi nuôi dạy chúng trong một môi trường thuận lợi cho học tập, nơi chất lượng phương tiện và tài liệu được kiểm soát nghiêm ngặt như khi huấn luyện các mô hình tiên tiến. Nó giống như một tu viện dành cho trẻ em, dạy chúng tri thức mới nhất về thực tại thông qua toán học, kỹ thuật, khoa học máy tính, deep learning, v.v.
      Rốt cuộc, để tận dụng các công cụ AI tiên tiến nhằm mở rộng ranh giới tri thức, ta vẫn cần những người rất thông minh và có tư duy chưa bị ô nhiễm quá nhiều. Ý tưởng rằng AI sẽ thay thế hoàn toàn con người là một hướng nghĩ sai
  • Nhân tiện, tên gọi ký hiệu bra-ket thực sự bắt nguồn từ dấu ngoặc (bracket)
    https://en.wikipedia.org/wiki/Bra-ket_notation

  • Ban đầu tôi hơi do dự, nhưng nhờ ký hiệu ket, các phép toán rõ ràng hơn nhiều nên tôi thích nó. Tuy vậy, giá mà cũng có một phần ôn nhanh về một số biến, như d_k trong attention bậc hai, thì tốt hơn

  • Lúc đầu tôi nản vì không nghĩ ra được lời giải này, nhưng rồi nhận ra mình còn gặp khó khi tự viết tìm kiếm nhị phân bằng JavaScript, nên lập tức thấy nhẹ nhõm. Không đời nào tôi có khả năng nghĩ ra Kimi Delta Attention

    • Code đại số tuyến tính có một mặt là dễ viết hơn bất ngờ. Nó không có đệ quy rối rắm như code khoa học máy tính thông thường, mọi biến đều có quan hệ toán học với nhau, và các khái niệm toán học phổ biến đã có thư viện triển khai tốt
      Vòng lặp cũng hiếm khi sâu quá hai, ba tầng; nếu phức tạp hơn thế thì dù sao giao cho thư viện vẫn tốt hơn