One Door to the Models, Μέρος 4: Ασύγχρονη Δουλειά Εκτός του Request Path
Το batch inference είναι δεύτερη πόρτα: αρχείο μπαίνει, job τρέχει, αρχείο βγαίνει, χωρίς πολιτική gateway στο μονοπάτι. Το Μέρος 4 κρατά τη λογιστική τίμια.

Το batch inference μονοπάτι του Azure δεν μοιάζει καθόλου με κλήση API. Ανεβάζεις ένα αρχείο JSONL, δημιουργείς ένα job, και παραλαμβάνεις ένα αρχείο εξόδου έως και 24 ώρες αργότερα, που σημαίνει ότι δεν υπάρχει συναλλαγή ανά αίτημα για να σταθεί μπροστά της μια πολιτική gateway. Κάθε εγγύηση που έχτισε το Μέρος 3, όρια tokens, μέτρηση ανά tenant, δρομολόγηση, circuit breaking, ισχύει για αιτήματα που περνούν από το API Management. Το batch δεν έχει καμία από αυτές εκ κατασκευής. Αυτό το μέρος μετακινεί τη δουλειά που δεν έπρεπε εξαρχής να βρίσκεται στο σύγχρονο μονοπάτι, και αντιμετωπίζει το γεγονός ότι κάνοντάς το ανοίγει μια δεύτερη πόρτα προς τα μοντέλα.
Τι ανήκει εκτός του request path
Οι πέντε εφαρμογές της εταιρείας παράγουν τρία είδη δουλειάς που βρίσκονται στο σύγχρονο μονοπάτι μόνο επειδή αυτό ήταν το ευκολότερο μέρος να μπουν. Το κριτήριο δεν είναι "είναι αργό", είναι ποιος περιμένει.
- Νυχτερινή σύνοψη των tickets υποστήριξης. Δεκάδες χιλιάδες αντικείμενα, κανένας άνθρωπος δεν περιμένει, τα αποτελέσματα χρειάζονται για την πρωινή αναφορά. Τέλειος υποψήφιος για batch, που σήμερα ανταγωνίζεται για το ίδιο TPM quota με έναν πελάτη στη μέση μιας συνομιλίας.
- Document ingestion για την αναζήτηση γνώσης του retail. Δουλειά με εκρήξεις, πυροδοτείται από uploads, ανεκτική σε λεπτά. Ένα queue, όχι batch job: latency σε λεπτά μετράει, latency σε ώρες δεν δουλεύει, επειδή κάποιος ανέβασε ένα έγγραφο περιμένοντας να το βρει.
- Επαναβαθμολόγηση μετά από αλλαγή prompt. Τρέχει πάνω σε ένα corpus, χωρίς κανέναν χρήστη, και ακυρώνεται στη μέση συχνότερα από όσο ολοκληρώνεται. Batch, με ρητή ιστορία ακύρωσης.
Τρία σχήματα, και αντιστοιχίζονται σε ακριβώς τρεις μηχανισμούς: ένα queue με workers για τα λεπτά, ένα batch job για τις ώρες, και το σύγχρονο gateway για ό,τι παρακολουθεί άνθρωπος. Το να τα μπερδέψεις είναι ο τρόπος με τον οποίο ένα marketing job ρίχνει έναν customer-service assistant, που είναι το περιστατικό από το Μέρος 1.
Το queue, και τι να μην βάλεις μέσα του
Το Azure Service Bus μεταφέρει τη δουλειά κλίμακας λεπτών. Οι σχεδιαστικές αποφάσεις που μετρούν αφορούν όλες το τι περιέχει το μήνυμα και όχι το ποιος broker είναι.
Μην βάζεις το prompt στο μήνυμα. Το Service Bus περιορίζει κάθε μεμονωμένη ιδιότητα μηνύματος στα 32 KB και το αθροιστικό header, user properties συν system properties, στα 64 KB, και η υπέρβαση προκαλεί serialization exception αντί να κόβει σιωπηλά. Ακόμη και μέσα στο body, ένα payload πάνω από 1 MB μετριέται διπλά στο size quota της οντότητας. Το claim-check pattern είναι η απάντηση: το έγγραφο πηγαίνει στο Blob Storage, το μήνυμα μεταφέρει μια αναφορά blob, ένα tenant ID, ένα model alias, και ένα correlation ID. Το μήνυμα μένει μικρό, το βάθος του queue παραμένει μετρική με νόημα, και το payload βρίσκεται ήδη εκεί από όπου θέλει να το διαβάσει ως stream ο worker.
Δύο ακόμη περιορισμοί διαμορφώνουν το worker pool. Ένα μεμονωμένο queue, topic, ή subscription δέχεται 5.000 ταυτόχρονα receive requests πριν αρχίσει να απορρίπτει περαιτέρω receives με σφάλμα server busy, οροφή που αφορά τον αριθμό των receivers και όχι το throughput, και φτάνεται γρηγορότερα από όσο περιμένει κανείς από έναν worker που ανοίγει έναν receiver ανά task. Και ένα namespace επιτρέπει 5.000 ταυτόχρονες συνδέσεις AMQP, οπότε το connection pooling στον worker δεν είναι βελτιστοποίηση, είναι απαίτηση σε κλίμακα.
Το dead-lettering είναι το σημείο όπου ένα LLM queue διαφέρει από ένα συνηθισμένο. Ένα μήνυμα που αποτυγχάνει επειδή το μοντέλο επέστρεψε content filter block δεν είναι το ίδιο με ένα που απέτυχε επειδή κατέρρευσε ο worker, και μόνο το δεύτερο πρέπει να ξαναδοκιμαστεί. Ο worker ολοκληρώνει το μήνυμα και καταγράφει την άρνηση ως αποτέλεσμα όταν το μοντέλο απαντά με άρνηση, και το εγκαταλείπει μόνο για αποτυχίες υποδομής. Αλλιώς το delivery count σκαρφαλώνει στο όριό του πάνω σε ένα μήνυμα που δεν θα πετύχει ποτέ, και το dead-letter queue γεμίζει με αντικείμενα που κανείς δεν μπορεί να ξεχωρίσει από πραγματικές αποτυχίες.
Κλιμάκωση των workers με KEDA
Οι workers ζουν στο AKS cluster που προμήθευσε το Μέρος 2, και δεν πρέπει να τρέχουν όταν το queue είναι άδειο. Το KEDA είναι διαθέσιμο ως AKS add-on και κλιμακώνει workloads στο μηδέν, οδηγώντας ScaledObject για deployments και ScaledJob για δουλειά σε σχήμα job, με το authentication αποσυνδεδεμένο από το workload μέσω Microsoft Entra Workload ID.
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: ingest-worker
spec:
scaleTargetRef:
name: ingest-worker
minReplicaCount: 0
maxReplicaCount: 20
pollingInterval: 15
cooldownPeriod: 120
triggers:
- type: azure-servicebus
metadata:
queueName: doc-ingest
messageCount: "20" # target backlog per replica
authenticationRef:
name: keda-workload-identity
Τρεις περιορισμοί του add-on αποφασίζουν αν αυτό δουλεύει με την πρώτη προσπάθεια ή με την τρίτη. Μην συνδυάζεις ένα ScaledObject με Horizontal Pod Autoscaler στο ίδιο workload. Το KEDA χρησιμοποιεί HPA από κάτω, οπότε τα δύο ανταγωνίζονται: αν το HPA υπάρξει πρώτο, το ScaledObject αποτυγχάνει να δημιουργηθεί, και αν το ScaledObject υπάρξει πρώτο, το HPA δημιουργείται ούτως ή άλλως και η συμπεριφορά κλιμάκωσης γίνεται παράξενη. Μόνο ένας external metric server επιτρέπεται ανά cluster, οπότε το KEDA add-on πρέπει να είναι ο μοναδικός και πολλαπλές εγκαταστάσεις KEDA δεν υποστηρίζονται, κάτι που αποκλείει το να εγκαταστήσει μια ομάδα τη δική της μέσω Helm δίπλα σε αυτήν της πλατφόρμας. Και στο AKS Standard, ενεργοποίησε το workload identity πριν ενεργοποιήσεις το KEDA add-on· με λάθος σειρά, τα pods του KEDA operator χρειάζονται restart για να πάρουν το σωστό περιβάλλον.
Μια χρήσιμη προεπιλογή: κλιμάκωσε με βάση το backlog ανά replica αντί για το απόλυτο βάθος του queue, και όρισε το maxReplicaCount από το quota του μοντέλου αντί από τη χωρητικότητα του cluster. Είκοσι workers που παίρνουν ο καθένας ένα 429 είναι χειρότεροι από πέντε που δεν παίρνουν, και το queue δεν νοιάζεται πόσο περιμένει ένα μήνυμα.
Το batch μονοπάτι, και γιατί είναι δεύτερη πόρτα
Η δουλειά κλίμακας ωρών πηγαίνει σε ένα Global-Batch model deployment, και το σχήμα αυτού του API είναι το νόημα αυτού του μέρους. Δεν υπάρχει αίτημα να αναχαιτίσεις. Ένα αρχείο ανεβαίνει, ένα job δημιουργείται, και ένα αρχείο εξόδου εμφανίζεται όταν το job ολοκληρωθεί. Οι μηχανισμοί είναι αρκετά συγκεκριμένοι ώστε να αξίζει να διατυπωθούν με ακρίβεια.
Η είσοδος είναι JSONL, ένα request object ανά γραμμή, και κάθε γραμμή μεταφέρει ένα custom_id:
{"custom_id": "ticket-88412", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "batch-summarize", "messages": [{"role": "system", "content": "Summarize the ticket in two sentences."}, {"role": "user", "content": "..."}]}}
{"custom_id": "ticket-88413", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "batch-summarize", "messages": [{"role": "system", "content": "Summarize the ticket in two sentences."}, {"role": "user", "content": "..."}]}}
Οι απαντήσεις δεν επιστρέφονται με τη σειρά που όρισε το αρχείο, και γι' αυτό το custom_id είναι υποχρεωτικό και όχι απλώς βολικό: είναι ο μόνος τρόπος να ενωθεί μια απάντηση πίσω με την είσοδό της. Το χαρακτηριστικό model πρέπει να ονομάζει το Global Batch deployment, και το ίδιο όνομα deployment πρέπει να εμφανίζεται σε κάθε γραμμή. Το να στοχεύσεις δεύτερο deployment σημαίνει δεύτερο αρχείο και δεύτερο job, κάτι που κάνει το "δρομολόγησε αυτό το batch σε όποιο μοντέλο είναι φθηνότερο σήμερα" απόφαση της στιγμής της υποβολής και όχι απόφαση δρομολόγησης. Η ίδια η καθοδήγηση της Microsoft είναι να υποβάλλονται μεγάλα αρχεία αντί για πολλά μικρά.
batch = client.batches.create(
input_file_id=file_id,
endpoint="/chat/completions",
completion_window="24h",
# 1209600 to 2592000 seconds, 14 to 30 days, before the output file expires
extra_body={"output_expires_after": {"seconds": 1209600, "anchor": "created_at"}},
)
Το job στη συνέχεια περνά από τα validating, in_progress, finalizing, και completed, και μεταφέρει ένα expires_at 24 ώρες μετά τη δημιουργία, μαζί με ένα τρέχον request_counts για τα completed, failed, και total. Το παράθυρο ολοκλήρωσης είναι 24 ώρες, και ένα job που δεν τελειώνει μέσα σε αυτό λήγει αντί να συνεχίσει. Λήγουν και τα ίδια τα αρχεία εξόδου, σε παράθυρο που ρυθμίζεται ανάμεσα σε 14 και 30 ημέρες, οπότε ένα pipeline που υποθέτει ότι τα αποτελέσματα θα είναι ακόμη εκεί το επόμενο τρίμηνο είναι μια απώλεια δεδομένων που περιμένει να συμβεί.
Και η χωρητικότητα δουλεύει διαφορετικά εδώ. Τα batch jobs καταναλώνουν quota enqueued tokens, και ένα job αρκετά μεγάλο ώστε να το ξεπεράσει απορρίπτεται αντί να μπει στη σειρά πίσω από το προηγούμενο. Ορισμένα regions υποστηρίζουν πλέον μια fail-fast συμπεριφορά που επιτρέπει να μπουν στη σειρά αρκετά batch jobs με exponential backoff, ώστε όταν τελειώνει ένα να ξεκινά αυτόματα το επόμενο. Χωρίς αυτό, ο βρόχος επανυποβολής είναι δουλειά αυτού που υποβάλλει, και ανήκει στο control plane αντί σε κάθε εφαρμογή ξεχωριστά.
Κρατώντας τη λογιστική τίμια
Εδώ είναι το άβολο κομμάτι. Το batch μονοπάτι δεν περνά από το API Management, οπότε το llm-token-limit δεν το περιορίζει, το llm-emit-token-metric δεν το μετρά, και η απόδοση κόστους ανά tenant που ετοιμάζεται να χτίσει το Μέρος 5 έχει ένα τυφλό σημείο στο μέγεθος του μεγαλύτερου workload της εταιρείας.
Δύο απαντήσεις, και η διαφορά τους αξίζει να αποφασιστεί σκόπιμα αντί να προκύψει από προεπιλογή.
- Άφησε τις εφαρμογές να υποβάλλουν batch jobs απευθείας και αποδέξου μια δεύτερη, αμέτρητη πόρτα. Το απλούστερο, και επαναφέρει αθόρυβα το ακριβές πρόβλημα για το οποίο γράφτηκε το Μέρος 1, έναν λογαριασμό που κανείς δεν μπορεί να αποδώσει.
- Κάνε το control plane τον μοναδικό υποβολέα batch. Μια εφαρμογή στέλνει ένα batch αίτημα στο control plane, το οποίο επικυρώνει τον tenant, αναλύει το model alias σε ένα Global Batch deployment, γράφει το JSONL, υποβάλλει το job, καταγράφει την υποβολή στον tenant, κάνει poll μέχρι την ολοκλήρωση, και παραδίδει πίσω την έξοδο. Το gateway παραμένει η μόνη πόρτα για το σύγχρονο traffic, και το control plane είναι η μόνη πόρτα για το ασύγχρονο.
Η δεύτερη είναι περισσότερη δουλειά και είναι αυτή που κρατά αληθινή την προκείμενη της σειράς. Προσγειώνει επίσης τη λογιστική σε πιο στέρεο έδαφος από την περίπτωση του streaming στο Μέρος 3: ένα ολοκληρωμένο batch job αναφέρει το δικό του request_counts και το αρχείο εξόδου μεταφέρει usage ανά απάντηση, οπότε η δαπάνη του batch είναι ακριβώς αποδοτέα, περισσότερο από το streamed traffic στο σύγχρονο μονοπάτι. Είναι μια ευχάριστη αντιστροφή που αξίζει να ξέρεις πριν υποθέσει κάποιος ότι το ασύγχρονο σημαίνει κατά προσέγγιση.
Τρόποι αποτυχίας που αξίζει να προσέχεις
- Ο βρόχος επανάληψης που δεν μπορεί να πετύχει. Μια άρνηση από content filter είναι αποτέλεσμα, όχι αποτυχία. Το να την ξαναδοκιμάζεις καίει quota, φουσκώνει το delivery count, και καταλήγει σε ένα dead-letter queue γεμάτο μηνύματα που είχαν απαντηθεί σωστά.
- Workers κλιμακωμένοι πέρα από το quota. Το KEDA κλιμακώνει πρόθυμα μέχρι το
maxReplicaCountμόνο με βάση το backlog. Αν αυτό ξεπερνά όσα επιτρέπει το TPM του model deployment, τα επιπλέον replicas παράγουν 429 και το queue δεν αδειάζει καθόλου γρηγορότερα. - Ένα batch job που λήγει αθόρυβα. Το παράθυρο των 24 ωρών τελειώνει σε status
expired, όχι σε exception στον κώδικα κάποιου. Χωρίς alert στο status του job, η πρωινή αναφορά απλώς λείπει, και ο πρώτος που το προσέχει είναι αυτός που τη διαβάζει. - Αρχεία εξόδου που γερνούν και χάνονται. Ένα αρχείο αποτελεσμάτων έχει λήξη ανάμεσα σε 14 και 30 ημέρες. Ό,τι πρέπει να διατηρηθεί αντιγράφεται σε storage που ανήκει στην πλατφόρμα, κατά την υποβολή, όχι αργότερα.
- Η δεύτερη πόρτα που ξανανοίγει από ευκολία. Μια ομάδα με απευθείας πρόσβαση στο Global Batch deployment ακυρώνει το μοντέλο απόδοσης κόστους. Ο κανόνας δικτύου και το alert από το Μέρος 1 ισχύουν και εδώ, και αυτό είναι το μονοπάτι που είναι πιθανότερο να τους ξεφύγει.
Τι κληρονομεί το Μέρος 5
Σύγχρονο traffic μέσω του gateway, δουλειά κλίμακας λεπτών σε ένα queue με workers που κλιμακώνονται στο μηδέν, και δουλειά κλίμακας ωρών που υποβάλλεται από το control plane σε ένα Global Batch deployment. Τρία μονοπάτια, τρία προφίλ latency, και τρεις διαφορετικές ποιότητες δεδομένων χρήσης που τροφοδοτούν το ίδιο ερώτημα: ποια ομάδα ξόδεψε τι. Αυτό το ερώτημα είναι το επόμενο, μαζί με το μοντέλο ταυτότητας που κάνει το "ποια ομάδα" καν απαντήσιμο.
Διάβασε επίσης
- Μέρος 5, Ταυτότητα, Quota, και Chargeback, όπου τα τρία σήματα χρήσης από αυτά τα μονοπάτια γίνονται ένας αριθμός ανά ομάδα, και ένα όριο του Azure Monitor αποφασίζει πώς χτίζεται.
- Μέρος 3, Το Provider Abstraction και το Streaming, το σύγχρονο μισό αυτής της εικόνας, και οι εγγυήσεις ανά transport που αυτό το μέρος παρακάμπτει.
- Batch Inference στο Bedrock: Μισή Τιμή Αν Μπορείς να Περιμένεις, η ίδια ανταλλαγή latency με κόστος από την πλευρά του AWS, με την αριθμητική του πότε αποδίδει.
Για την πλευρά υποδομής και πλατφόρμας της λειτουργίας κάτι τέτοιου σε κλίμακα, οι σημειώσεις πεδίου είναι στο ercan.cloud, και ο κόμβος είναι στο ercanermis.com.
Αναφορές
Περισσότερα από τον Ercan
Δύο ακόμη ιστότοποι, ίδιος συγγραφέας, διαφορετικό έδαφος.
Cloud, AWS, EKS, Terraform, platform engineering.
Σημειώσεις πεδίου από συστήματα παραγωγής. EKS, IAM, Terraform σε κλίμακα οργανισμού, observability, βελτιστοποίηση κόστους.
Επισκεφθείτε ercan.cloud →Ο κόμβος. Σχετικά, συμβουλευτική, επικοινωνία.
Προσωπικός κόμβος και για τις δύο διαδρομές γραφής. Ποιος είμαι, πώς λειτουργεί η συμβουλευτική, πώς να επικοινωνήσετε.
Επισκεφθείτε ercanermis.com →