OPENAI

Dịch tễ học về kết xuất cốt lõi: sửa lỗi 18 tuổi

Bùi Đăng MinhThứ ba, 30/6/2026, 09:00 (GMT+7)4 phút đọc
Dịch tễ học về kết xuất cốt lõi: sửa lỗi 18 tuổi

Sử dụng phân tích ở cấp độ dân số để gỡ lỗi các sự cố phức tạp trong cơ sở hạ tầng dữ liệu của chúng tôi.

Bởi Nathan Bronson, Thành viên Đội ngũ Kỹ thuật Các mô hình và tác nhân của OpenAI ngày càng dựa vào cơ sở hạ tầng dữ liệu có thể mở rộng để tìm kiếm dữ liệu liên quan tại thời điểm inference: khi các mô hình đang suy nghĩ về câu hỏi của bạn. Một số dịch vụ này được viết bằng C++, khả năng kiểm soát hệ thống ở mức độ thấp cho phép chúng tôi tối đa hóa hiệu suất và giảm thiểu mức sử dụng bộ nhớ. Những lợi ích hiệu quả đó rất quan trọng khi chúng tôi mở rộng quy mô, nhưng việc thiếu an toàn bộ nhớ của C++ có nghĩa là các lỗi có thể gây ra sự cố do ghi vào địa chỉ bộ nhớ không chính xác hoặc không tồn tại.

Một vài tháng trước, chúng tôi đã quan sát thấy một số sự cố xảy ra từ bên trong dịch vụ Rockset, một phần riêng biệt trong cơ sở hạ tầng dữ liệu ChatGPT của chúng tôi, vốn là chìa khóa cho nhiều plugin dữ liệu và tìm kiếm qua các cuộc hội thoại. Trong mỗi sự cố này, một hàm C++ bình thường dường như kết thúc và sau đó quay trở lại một địa chỉ giả, khiến kernel dừng chương trình vì con trỏ lệnh không còn trỏ vào mã nữa. Đôi khi khe địa chỉ trả về trong khung stack là NULL. Đôi khi bản thân thanh ghi CPU con trỏ stack dường như bị lệch 8 byte, như thể %rsp bằng cách nào đó đã bị giảm đi trong khi thực thi bình thường. Trong cả hai trường hợp, vụ tai nạn đều xảy ra khi quay trở lại.

Đây không phải là các chế độ lỗi thông thường đối với mã ứng dụng. Có thể xảy ra trường hợp viết sai địa chỉ gửi lại địa chỉ trả lại đã lưu, nhưng rất khó xảy ra. Một lỗi căn chỉnh sai %rsp đến 8 mà không liên quan đến tập hợp nội tuyến, setcontext hoặc longjmp (không có cái nào chúng tôi sử dụng) thậm chí còn lạ hơn, bởi vì mã được biên dịch chỉ điều chỉnh đăng ký trực tiếp trong phần mở đầu và phần kết của hàm. Mọi giả thuyết mà chúng tôi (hoặc ChatGPT) có thể nghĩ ra đều có bằng chứng mạnh mẽ chống lại nó, vì vậy lỗi này dường như không thể xảy ra.

Những gì chúng tôi cho là một vấn đề cuối cùng hóa ra lại là hai lỗi không liên quan, được phát hiện một cách tình cờ cùng một lúc. Đầu tiên, lỗi phần cứng thầm lặng trên một máy chủ Azure, nơi CPU không thực hiện phép toán chính xác. Thứ hai, một tình trạng chạy đua 18 năm tuổi trong GNU libunwind, một lỗi không được chú ý trong thư viện nguồn mở được sử dụng rộng rãi.

Bài đăng này là câu chuyện về cách chúng tôi xác định và khắc phục các sự cố dường như không thể giải thích được bằng cách suy nghĩ như một nhà dịch tễ học và xây dựng bộ dữ liệu chất lượng cao về toàn bộ số vụ tai nạn.

Nỗ lực gỡ lỗi đầu tiên: kiểm tra cẩn thận một vài kết xuất lõi

Đầu tiên, hãy tìm hiểu sâu hơn về Rockset. Đó là hệ thống dữ liệu dựa trên nền tảng đám mây để phân tích tìm kiếm và thời gian thực mà chúng tôi sử dụng cho nhiều trường hợp sử dụng nội bộ tại OpenAI, chẳng hạn như trình kết nối đồng bộ hóa (Rockset đã được OpenAI mua lại vào năm 2024). Các bản cập nhật phát trực tuyến được sử dụng để duy trì chỉ mục cập nhật về cơ sở kiến ​​thức của không gian làm việc để ChatGPT có thể tìm kiếm thông tin liên quan khi trả lời câu hỏi hoặc thực hiện hành động.

Lớp thực thi của Rockset được viết bằng C++. Ngôn ngữ C++ cung cấp quyền truy cập cấp thấp vào CPU, điều này tốt cho hiệu suất và hiệu quả, nhưng điều đó có nghĩa là các lỗi ứng dụng có thể dẫn đến truy cập bộ nhớ không hợp lệ và lỗi phân tách. Để giúp theo dõi những điều này, chúng tôi sử dụng trình xử lý tín hiệu nghiêm trọng của Folly để ghi lại dấu vết stack khi xảy ra sự cố và tải các kết xuất lõi tương ứng (ảnh chụp nhanh trạng thái của chương trình khi chương trình gặp sự cố) lên bộ lưu trữ Azure blob để phân tích sau này. Tất cả các bước xử lý truy vấn của Rockset đều được sao chép, giúp giảm thiểu tác động lên máy khách khi xảy ra sự cố. Tuy nhiên, mỗi segfault tương ứng với một lỗi cần được sửa để đáp ứng các mục tiêu về độ tin cậy và chất lượng của chúng tôi.

Cách tiếp cận ban đầu của chúng tôi là xử lý các lõi này giống như một vấn đề gỡ lỗi thông thường: kiểm tra rất chặt chẽ một số kết xuất lõi, hình thành các giả thuyết và loại trừ chúng từng cái một.

Hầu hết các sự cố xảy ra trong một phương thức có tên DocumentTree::updateDocument. Trong những sự cố này, có vẻ như updateDocument đã gọi một hàm X không xác định nào đó, stack đã bị hỏng khi X đang hoạt động, sau đó X quay trở lại một địa chỉ không phải là mã thực thi. Trong một số trường hợp, khung vừa xuất hiện của X trông hợp lệ ngoại trừ địa chỉ trả về được lưu của nó là NULL. Trong các trường hợp khác, bản thân con trỏ stack có vẻ sai nhưng khung hợp lệ tiếp theo dường như vẫn là updateDocument.

Chúng tôi không biết khi nào stack bị hỏng, điều này để lại một không gian tìm kiếm khổng lồ. updateDocument là một phương thức lớn trải qua nhiều nội tuyến nên số lượng ứng cử viên cho X là rất lớn.

Đây có phải là lỗi trong mã C++ của chúng tôi không? Một vấn đề về trình biên dịch hoặc liên kết? Sự cố xảy ra ở một trong các thư viện thời gian chạy của chúng tôi? Lỗi nhân Linux xung quanh việc phân phối tín hiệu hoặc chuyển đổi ngữ cảnh? Một cái gì đó thậm chí còn hiếm hơn? Nếu đây là một hành động viết sai, tại sao nó không bị môi trường dàn dựng ASAN của chúng tôi phát hiện?

Chúng tôi đã cố gắng sử dụng nhật ký cấp ứng dụng của mình để xác định tất cả các lần xảy ra sự cố, nhưng khó phân loại lỗi hỏng stack chỉ dựa vào nhật ký vì bản thân dấu vết stack đã ghi đã bị hỏng hoặc bị thiếu. Chúng tôi không thể tạo một truy vấn nhật ký không có cả kết quả dương tính giả và âm tính giả. Chúng tôi đã kiểm tra nhiều lõi hơn theo cách thủ công và tìm thấy một số ví dụ bổ sung, nhưng quá trình đó tốn quá nhiều công sức để cung cấp cho chúng tôi một tập dữ liệu đáng tin cậy.

Ở giai đoạn điều tra này, chúng tôi đã loại trừ (không chính xác) lỗi phần cứng vì chúng tôi thấy sự cố trên nhiều khu vực và nhiều loại phần cứng, vì vậy chúng tôi vẫn đang tìm kiếm các nguyên nhân chỉ liên quan đến phần mềm. Trong vài ngày, chúng tôi đã tìm hiểu rất sâu về một sự cố sai lệch-%rsp, xây dựng lại lịch sử trước sự cố bằng cách sử dụng nội dung stack và đăng ký. Điều này tạo ra một số manh mối khả thi, nhưng vì chúng tôi không loại bỏ kết luận ban đầu rằng tất cả các lỗi đều có cùng một nguyên nhân nên điều này không giúp chúng tôi thoát khỏi bế tắc.

Manh mối từ stack

Trước khi đi đến bước ngoặt của cuộc điều tra, điều quan trọng là phải giải thích loại thông tin nào chúng tôi đang trích xuất từ ​​các tệp cốt lõi.

Rockset được biên dịch bằng -fno-omit-frame-pointer, do đó, khung stack đang hoạt động luôn có thể truy cập được thông qua %rbp và người gọi tạo thành một danh sách liên kết các con trỏ khung.

Trên Linux x86_64, AMD64 System V ABI cũng dự trữ 128 byte dưới %rsp làm vùng màu đỏ. Vùng đó có sẵn cho mã không gian người dùng và quan trọng là kernel hứa sẽ không chặn nó khi nó gửi tín hiệu, như một phần của hợp đồng ABI.

Vùng màu đỏ là trọng tâm trong việc gỡ lỗi của chúng tôi về sự cố sau khi quay lại vì nó lưu giữ một số thông tin từ trước khi quay lại. Khi SIGSEGV được kích hoạt, trình xử lý tín hiệu nghiêm trọng của Folly sẽ chạy trên stack của luồng bị lỗi. Các khung stack không còn hoạt động (vì chức năng của chúng đã quay trở lại) sẽ bị bộ xử lý tín hiệu ghi đè, ngoại trừ 128 byte cuối cùng. Đó là lý do tại sao chúng ta có thể nói những điều như “Khung stack vừa xuất hiện của X trông hợp lệ, ngoại trừ địa chỉ trả về NULL”. Vùng màu đỏ giữ lại một số khung không hoạt động hoặc đôi khi chỉ là phần đuôi của một khung không hoạt động.

Sơ đồ stack hiển thị các khung stack bị hỏng có thể ghi đè địa chỉ trả về và gây ra sự cố.

Chúng tôi đã tìm thấy một sự cố stack không thẳng hàng trong đó tất cả các chức năng liên quan đều rất nhỏ. Điều đó cho chúng ta thấy rằng %rsp đã bị sai lệch trong quá trình thực thi một hàm tương đối đơn giản và sau đó có nhiều lệnh gọi thành công hơn. Chương trình chỉ gặp sự cố khi chức năng hoạt động cuối cùng đã cố gắng quay trở lại. Không có đường dẫn mã nào trong số đó sử dụng ngoại lệ, tập hợp nội tuyến, setcontext hoặc longjmp, vì vậy nếu con trỏ stack thực sự thay đổi theo cách cốt lõi đề xuất thì không có lỗi chính đáng nào trong mã không gian người dùng giải thích được vấn đề.

Điều đó đã đẩy chúng tôi về phía hạt nhân.

Rockset sử dụng tín hiệu mạnh mẽ hơn hầu hết các chương trình. Việc thực thi truy vấn được chia thành nhiều tác vụ nhẹ để trao đổi dữ liệu. Điều này rất quan trọng để xử lý khối lượng công việc QPS cao một cách hiệu quả, nhưng nó khiến việc tính toán CPU theo mỗi truy vấn trở nên khó khăn vì công việc cho nhiều truy vấn được ghép kênh trên cùng một nhóm luồng.

Giải pháp của chúng tôi là thứ mà chúng tôi gọi là rough_thread_cputime_clock, gần đúng với clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...) đủ rẻ để lấy mẫu ở mọi ranh giới nhiệm vụ. API hẹn giờ_create có thể được sử dụng để lên lịch phân phối tín hiệu định kỳ dựa trên một số khái niệm về thời gian trôi qua, bao gồm cả việc tích lũy thời gian CPU. Chúng tôi lên lịch gửi tín hiệu (SIGUSR2) cứ sau vài mili giây thời gian CPU, tại thời điểm đó, trình xử lý tín hiệu sẽ cập nhật giá trị luồng cục bộ. Mặc dù nhiều tác vụ không thấy tốc độ tăng xung nhịp thô trong khi chúng đang thực thi, nhưng việc tính tổng tất cả các delta sẽ tạo ra ước tính không thiên vị về thời gian CPU thực tế cho một truy vấn.

Nguồn / Original source: OpenAI (@sama & @OpenAIDevs)