Skip to content
academia.sh

Lesson 21 / 26

Self-Hosted Models

Running a model on your own hardware makes the unit memory instead of tokens: the weight memory of three fictional scales is 70.5 million, 367.5 million and 2.14 billion values, the intermediate state per request in a 960-token context is 11.8 million, 47.2 million and 157.3 million values, the concurrent requests a three-billion-value budget affords are 248, 55 and 5, and the label accuracy on the same 120 fictional tickets is 0.6333, 0.8167 and 0.8667. The product of concurrent requests and accuracy falls from 157.1 to 4.3, so the largest scale yields one thirty-sixth of the correct-response capacity. With the distribution temperature at zero both runs give the same 0.8667, and at 0.02 28 of the 120 tickets change from run to run.

Published Updated

In this lesson

  • Computes weight memory and the intermediate state per request separately as counts of values.
  • Finds how many concurrent requests a memory budget affords given the parameter count and the context length.
  • Writes the choice the budget imposes by placing the accuracy and the concurrent request count of the three scales side by side.
  • Shows with two runs the determinism that self-hosting brings.
Contents

In the previous twenty lessons the model was always an endpoint standing far away. Everything measured was read from the surface of that endpoint: how many calls went out, how many tokens were spent, how many requests were rejected, how many responses were cut short. All of those numbers shared one assumption: the model itself sits on someone else’s machine, and how many requests are processed there at the same time is not your problem.

When the model moves to your own hardware, that assumption falls and the unit changes. What is spent is no longer tokens but memory, and memory is not a growing bill but a fixed ceiling. This lesson counts that ceiling: how many values the weights of a scale hold in memory, how many values the intermediate state of one request adds, and how many concurrent requests a given budget affords. The memory accounting on the training side was measured in the Training Practice topic of the Deep Learning course; that procedure is not repeated here, only its counterpart on the inference side is built.

  • LA1. The code is a simulator; no real model, runtime or hardware is called. The tickets and the scales are fictional, the seed is 20260218.
  • LA2. Memory is counted as a count of values. How many digits a value is stored with is the subject of the next lesson and is held fixed here.
  • LA3. A scale is given by three structural quantities: the number of layers, the hidden width and the vocabulary size. The parameter count is computed from them.
  • LA4. The simulator’s own weight table is small and exists only to produce accuracy. The memory accounting is done from the structural description of the scales, not from the simulator’s table.
  • LA5. Weight memory is independent of the requests and is a single copy. The intermediate state is separate for each request and is directly proportional to the context length.
  • LA6. The intermediate state keeps two arrays of width size in every layer for every token in the context; that is, per request two times layers times width times context values.
  • LA7. As the scale grows, the number of distinguishing rows of the simulator grows (12, 26, 121). This models capability growing with scale and carries no other claim.
  • LA8. With the distribution temperature at zero the most likely choice is made and the output is the same from run to run; above zero, seed-dependent sampling is performed.
  • LA9. The budget is three billion values; weights and intermediate state are paid for from the same budget.
  • LA10. The units measured are the count of values and the number of concurrent requests; raw time is not written.

Three Scales, Three Capabilities

The simulator splits a text into pieces of five characters and keeps a weight row for each piece. The row has a base shared by all five topics; what distinguishes is the small difference added on top of the base for each topic. While reading a ticket, the model takes the highest value it sees for each topic and picks the topic with the highest score. Scale changes only one thing here: how many pieces have a distinguishing row.

# SIMULATOR -- FICTIONAL. There is no real model, runtime or hardware;
# memory is counted as a count of values.
import math

SEED, M32 = 20260218, 0xFFFFFFFF


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


def pick(u, w):
    t = 0.0
    for i, x in enumerate(w):
        t += x
        if u < t:
            return i
    return len(w) - 1


def tokenize(m):                        # FICTIONAL tokenizer, a piece is at most five characters
    b = []
    for s in m.lower().split():
        while len(s) > 5:
            b.append(s[:5])
            s = s[5:]
        b.append(s)
    return b


def text_hash(m):
    h = 2166136261
    for c in m:
        h = ((h ^ ord(c)) * 16777619) & M32
    return h


TOPICS = ["invoice", "breakdown", "reading", "tariff", "outage"]
TEMPLATES = {"invoice": ["bu ayki tutar gecen aya gore cok yuksek",
                         "odedim ama sistemde borc gorunuyor",
                         "ayrintili dokumu nereden gorebilirim"],
             "breakdown": ["evde su akmiyor bir sorun var",
                           "baglanti bozuk oldu ve hic calismiyor",
                           "ariza kaydi actim ama ekip gelmedi"],
             "reading": ["okuma yanlis girilmis endeks tutmuyor",
                         "cihaz degisimi icin randevu istiyorum",
                         "endeksi ile fatura ayni degil"],
             "tariff": ["hangi basamaktayim ogrenmek istiyorum",
                        "tarife degisikligi icin ne yapmaliyim",
                        "birim ucret abone tipine gore degisir mi"],
             "outage": ["mahallede elektrik yok ne zaman gelecek",
                        "planli duyuru nereden takip edilir",
                        "aksamdan beri kesinti yasiyoruz"]}
EXTRAS = ["ayrica gecen hafta da aradim", "kayit numarasi verildi",
          "ekip bilgi verecekti", "degisiklik talebim de var"]
TICKETS = []                            # 120 FICTIONAL support tickets
for i in range(120):
    r = rng(SEED + i)
    k = TOPICS[pick(r(), [0.24, 0.22, 0.18, 0.20, 0.16])]
    m = TEMPLATES[k][int(r() * 3)]
    if r() < 0.34:
        m += " " + EXTRAS[int(r() * 4)]
    TICKETS.append({"id": 4001 + i, "topic": k, "text": m})

PIECE_TOPICS = {}                       # which topic's template each piece occurs in
for k, items in TEMPLATES.items():
    for m in items:
        for t in set(tokenize(m)):
            PIECE_TOPICS.setdefault(t, {})[k] = 1
for e in EXTRAS:
    for t in set(tokenize(e)):
        PIECE_TOPICS.setdefault(t, {})

BASE = 0.50                             # weight of a piece shared by all topics
WEIGHTS = {}
for i, t in enumerate(sorted(PIECE_TOPICS)):
    r = rng(SEED + 7919 * (i + 1))
    row = {k: BASE for k in TOPICS}
    if PIECE_TOPICS[t]:
        for k in PIECE_TOPICS[t]:
            row[k] = BASE + 0.05 + 0.40 * r()    # small distinguishing difference
    else:
        row[TOPICS[int(r() * 5)]] = BASE + 0.02 + 0.10 * r()   # misleading link
    WEIGHTS[t] = row
ORDER = sorted(WEIGHTS, key=lambda t: (BASE - max(WEIGHTS[t].values()), t))

SCALES = {"small": (12, 512, 12), "medium": (24, 1024, 26),
          "large": (40, 2048, len(WEIGHTS))}        # layers, width, distinguishing rows


def score(text, rows):
    learned = set(ORDER[:rows])
    p = {k: BASE for k in TOPICS}
    for t in tokenize(text):
        if t in learned:
            for k in TOPICS:
                p[k] = max(p[k], WEIGHTS[t][k])
    return p


def respond(text, scale, temperature=0.0, seed=SEED):
    p = score(text, SCALES[scale][2])
    if temperature <= 0.0:
        return max(sorted(p), key=lambda a: p[a])   # most likely choice
    u = [math.exp((p[k] - max(p.values())) / temperature) for k in TOPICS]
    return TOPICS[pick(rng(seed ^ text_hash(text))(), [x / sum(u) for x in u])]


print("vocabulary", len(WEIGHTS), "pieces ; sample ticket:", TICKETS[0]["text"])
print("scale   accuracy")
for name in SCALES:
    d = sum(respond(x["text"], name) == x["topic"] for x in TICKETS)
    print(f"{name:<7} {d / 120:.4f}")
print("\ntemperature  seed 20260218  seed 20260219")
for s in (0.0, 0.02):
    a, b = (sum(respond(x["text"], "large", s, t) == x["topic"]
                for x in TICKETS) / 120 for t in (SEED, SEED + 1))
    print(f"{s:>11.2f}  {a:>13.4f}  {b:>13.4f}")
vocabulary 121 pieces ; sample ticket: planli duyuru nereden takip edilir
scale   accuracy
small   0.6333
medium  0.8167
large   0.8667

temperature  seed 20260218  seed 20260219
       0.00         0.8667         0.8667
       0.02         0.8167         0.8583

The first table says what the scale buys. The small scale assigns 0.6333 of the hundred and twenty tickets to the right topic, the medium 0.8167, the large 0.8667. The difference between them is not capability itself but how many distinctions have been learned: the small scale knows which topic only twelve pieces point to, and the remaining one hundred and nine pieces are indistinguishable from one another for it.

The second table shows a side of self-hosting that an endpoint far away does not have. With the distribution temperature at zero, two different seeds give the same 0.8667; the response to no ticket changes. When the temperature rises to 0.02, the same two runs give 0.8167 and 0.8583. The sampling setting is not in the provider’s hands but in yours; determinism becomes not a request but a configuration. This is also the reason the numbers in the following sections stay fixed from run to run.

Memory Budget and Concurrent Requests

Memory consists of two separate items, and the two behave very differently. Weight memory holds the parameters of the model; it is a single copy no matter how many requests arrive, and it does not change. The intermediate state is kept for the context of each request; it grows with the number of requests and with the context length. The budget has to cover both, so the number of concurrent requests is determined by what remains of the budget after the weights are subtracted.

BUDGET, VOCAB = 3_000_000_000, 32000    # memory budget and FICTIONAL structural vocabulary
CONTEXTS = (240, 960, 3840)


def parameter_count(L, H):
    return 12 * L * H * H + 2 * VOCAB * H


def intermediate_state(L, H, context):
    return 2 * L * H * context


print("scale   weights(count)  state/request  concurrent  accuracy  capacity")
for name, (L, H, rows) in SCALES.items():
    a, d = parameter_count(L, H), intermediate_state(L, H, 960)
    i = sum(respond(x["text"], name) == x["topic"] for x in TICKETS) / 120
    e = max(0, (BUDGET - a) // d)
    print(f"{name:<7} {a:>14} {d:>14} {e:>11} {i:>9.4f} {e * i:>9.1f}")
print("\nscale   " + "  ".join(f"context {b:>4}" for b in CONTEXTS))
for name, (L, H, rows) in SCALES.items():
    a = parameter_count(L, H)
    e = [max(0, (BUDGET - a) // intermediate_state(L, H, b)) for b in CONTEXTS]
    print(f"{name:<7} " + "  ".join(f"{x:>12}" for x in e))
print("\nrounds for 120 requests (context 960):")
for name, (L, H, rows) in SCALES.items():
    e = max(0, (BUDGET - parameter_count(L, H)) // intermediate_state(L, H, 960))
    print(f"  {name:<7} concurrent {e:>4}  round {-(-120 // e):>3}")
print("if the budget drops to 2000000000 values, the large scale cannot be loaded:",
      parameter_count(40, 2048) > 2_000_000_000)
scale   weights(count)  state/request  concurrent  accuracy  capacity
small         70516736       11796480         248    0.6333     157.1
medium       367525888       47185920          55    0.8167      44.9
large       2144337920      157286400           5    0.8667       4.3

scale   context  240  context  960  context 3840
small            993           248            62
medium           223            55            13
large             21             5             1

rounds for 120 requests (context 960):
  small   concurrent  248  round   1
  medium  concurrent   55  round   3
  large   concurrent    5  round  24
if the budget drops to 2000000000 values, the large scale cannot be loaded: True

The first column gives the distance between the scales: weight memory is 70.5 million, 367.5 million and 2.14 billion values. When the width doubles, the layer parameters quadruple, because the parameter count of each layer grows with the square of the width. The effect of the number of layers, on the other hand, is linear. The difference between these two growth rates explains why the choice of scale is not gradual but jumps.

The second column is less intuitive. The intermediate state grows with the width itself, not its square, but it is directly proportional to the context length. In the large scale, a context of 960 tokens asks for 157.3 million values per request; that is one thirteenth of the weights of the same scale. Thirteen concurrent requests consume the same memory as loading a second model.

The Choice the Budget Imposes

The concurrent request column turns the table into a choice. The three-billion-value budget can carry 248 requests at the same time in the small scale, 55 in the medium scale and 5 in the large scale, with a context of 960 tokens. Moving to the large scale raises accuracy from 0.8167 to 0.8667; the gain is 0.0500. The cost of the same step is that concurrent requests drop to one tenth.

The last column reduces these two numbers to a single measure. The product of concurrent requests and accuracy is the correct-response capacity the budget can produce at the same time: 157.1 in the small scale, 44.9 in the medium, 4.3 in the large. The largest scale gives the highest accuracy and produces one thirty-sixth of the correct-response capacity. Which column wins depends on the workload: if the number of incoming tickets is below what the budget can carry, the accuracy column decides; if it is above, the capacity column decides. This is a question never asked at a distant endpoint, because there concurrency is paid for from the provider’s budget and the bill shows up only as tokens.

The second table shows the same tradeoff on the context axis. When the context goes from 240 to 3840, concurrent requests fall to one sixteenth in every scale; in the large scale to 1, that is, to queueing. Context length looks like a quality setting but in a local setup it is directly a capacity setting.

The last line writes the hard side of the ceiling. If the budget drops to two billion values, the large scale cannot be loaded; the number of concurrent requests does not fall, it becomes zero. At a distant endpoint, overload produces a rate limit response and is met with a retry; in a local setup the counterpart is never starting. The two failures do not have the same form, and the second has no retry.

Concurrency Is a Latency Class

The number of concurrent requests alone is a capacity measure; applied to a workload it turns into a latency class. When all one hundred and twenty fictional tickets are queued, the small scale serves them all in one round, the medium scale needs three, the large scale twenty-four rounds. The round count is not raw time and does not vary with hardware; it is a number, fixed from run to run, that comes out of the ratio of the budget to the intermediate state. This is exactly what is needed to measure how long the last ticket in line waits: a queue of twenty-four rounds is not in the same latency class as a queue of one round.

The question of which column a runtime choice touches in this table and which it cannot touch is answered from here as well. The weight column comes from the structural description of the scale and does not depend on which runtime is chosen. The only item a runtime can change is how the intermediate state is held: whether the part of the context shared between requests is shared, whether the place of a finished request is freed immediately, how many requests are processed together in a round. All of these move the second column, and therefore the number of concurrent requests and the round count. The measurable way to compare one runtime with another is also to put these two numbers side by side at the same budget and the same context length; the column is compared, not the name.

Summary

  • In self-hosting the unit is the count of values, not tokens; memory is not a growing bill but a fixed ceiling.
  • Weight memory is a single copy and independent of the requests; the intermediate state is separate for each request and grows in direct proportion to the context length.
  • The weights of the three fictional scales are 70.5 million, 367.5 million and 2.14 billion values; when the width doubles, the layer parameters quadruple.
  • A three-billion-value budget affords 248, 55 and 5 concurrent requests in a 960-token context; accuracy is 0.6333, 0.8167 and 0.8667.
  • The product of concurrent requests and accuracy falls from 157.1 to 4.3: the scale with the highest accuracy yields one thirty-sixth of the correct-response capacity.
  • With the distribution temperature at zero both runs give the same 0.8667; in a local setup determinism is a configuration decision.

Next Step

All of this lesson’s accounting rested on a single assumption: memory was counted as a count of values, and how much room a value takes in memory was assumed fixed. That assumption can be lifted. If a value is stored with fewer digits, the same parameter count takes less room, and a larger scale or more concurrent requests fit into the same budget. In return, the small differences the model rests on become coarser, and what distinguished the weight rows of this lesson was exactly those small differences. The next lesson lowers the number of digits at three levels and prints two numbers side by side: how much the memory falls, at what point accuracy falls, and on which task it falls first.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close