An Empty resources: {} Makes Kubernetes 1.34 to 1.36 Pods BestEffort
---
title: An Empty resources: {} Makes Kubernetes 1.34 to 1.36 Pods BestEffort
published: true
date: 2026-11-04 15:00:00 UTC
tags: Kubernetes,QoS,GitLabRunner,k3s
canonical_url: devops-daily.com
---
On Kubernetes 1.34, 1.35 and 1.36, one empty block in a pod spec changes how the node treats the pod. If the pod sets spec.resources: {} at pod level, the kubelet gives it QoS class BestEffort. This happens even when every container sets requests and limits, whenever the PodLevelResources feature gate is on, which is the default from 1.34. GitLab Runner's Kubernetes executor builds its job pods with that empty block. Its source does this from 18.10.0 to 19.4.3, unless the runner config sets pod-level values.
We ran the same pods on k3s v1.34.12, v1.35.9, v1.36.5 and v1.37.1. On the three older versions, the empty block moved the pod into the BestEffort cgroup slice. The kubelet set its oom_score_adj to 1000, the highest value the kernel accepts. Next to a busy neighbour, a pod that requested half a CPU got 0.06 to 0.08 cores instead of 0.57 to 0.58. A namespace quota that blocks BestEffort pods rejected it. On 1.37.1 the same pods kept their Burstable and Guaranteed classes.
One thing did not change, and the research we started from got it wrong. It said these pods are first in line for kubelet eviction under memory pressure. They are not. The kubelet's eviction ranking compares memory usage with the memory request. For an empty block, that request still comes from the containers. The QoS class is not part of the ranking, and the kubelet evicted our pods in the same order on every version.
TL;DR
- On k3s v1.34.12, v1.35.9 and v1.36.5, a pod with container requests and limits plus spec.resources: {} reported qosClass: BestEffort. The Guaranteed variant did too. On v1.37.1 they reported Burstable and Guaranteed. With the new PodLevelResourcesFixKubeletQOSClass feature gate turned off, 1.37.1 went back to BestEffort.
- The node settings followed the class. oom_score_adj went from 935 (Burstable) and -997 (Guaranteed) to 1000, and the pod cgroup moved to kubepods-besteffort.slice with cpu.weight 1 and no pod-level memory.max. The container-level limits stayed in place.
- A neighbour pod kept both vCPUs busy. Next to it, the BestEffort-classified pod got a median of 0.061 to 0.083 cores on the three affected versions. The same pod without the empty block got 0.573 to 0.580 (so 10.6% to 14.3%). On 1.37.1 the two were equal.
- A ResourceQuota with scope BestEffort and pods: "0" rejected the pod on 1.34 to 1.36 and admitted it on 1.37.
- The kubelet's memory-pressure eviction order was the same on all four versions, in all 12 repetitions: the true BestEffort pod first, then by memory usage relative to the container request. The empty block did not move a pod forward.
- If you run GitLab Runner on Kubernetes, upgrade to 19.5.0 or later. As of 2 November 2026 the latest Kubernetes patch releases for 1.34, 1.35 and 1.36 still have the bug.
Prerequisites
- A cluster on Kubernetes 1.34, 1.35 or 1.36 (kubectl version), or a machine where you can run the lab
- kubectl and jq
- If you run GitLab Runner with the Kubernetes executor: its version (gitlab-runner --version)
- To run the lab: a DigitalOcean token (referral link); one s-2vcpu-4gb droplet for about an hour costs a few cents
Where the empty block comes from
Pod-level resources (KEP-2837) let you set CPU and memory for the pod as a whole, not only per container. The feature gate PodLevelResources went alpha in 1.32 and beta, on by default, in 1.34. Our Kubernetes 1.36 post covers the in-place resize of those values.
The QoS calculation has a shortcut for them. This is ComputePodQOS at v1.34.0, v1.35.0 and v1.36.0, and still at the latest patches, v1.34.13, v1.35.10 and v1.36.6:
// pkg/apis/core/v1/helper/qos/qos.go, v1.36.6
// When pod-level resources are specified, we use them to determine QoS class.
if utilfeature.DefaultFeatureGate.Enabled(features.PodLevelResources) &&
pod.Spec.Resources != nil {
An empty block is not nil. It has no requests and no limits, so the function returns BestEffort. The loop over the containers never runs. The issue that reported this, #135082 from November 2025, says that almost everywhere else Kubernetes does not differentiate between nil and empty pod-level resources. The helper for that, IsPodLevelResourcesSet, treats {} as not set.
The fix is PR #137150, merged on 15 July 2026 for 1.37. It adds the condition IsPodLevelResourcesSet(pod) behind a new gate, PodLevelResourcesFixKubeletQOSClass, beta and on by default from 1.37. It was not backported. The patch releases of 27 October 2026, v1.34.13, v1.35.10 and v1.36.6, still have the old condition. The rest of that release is in our Kubernetes 1.37 review; PodLevelResources itself stays beta in 1.37.
GitLab Runner added pod-level resources to its job pods in 18.10.0 (MR !5922). At every tag we read (18.10.0, 19.3.5 and 19.4.3), the helper that builds the block returns a non-nil object:
// executors/kubernetes/kubernetes.go, GitLab Runner v19.4.3
func (s *executor) podResourcesReference() *api.ResourceRequirements {
resources := api.ResourceRequirements{}
if s.configurationOverwrites != nil {
resources.Limits = s.configurationOverwrites.podLimits
resources.Requests = s.configurationOverwrites.podRequests
}
return &resources
}
If the runner config sets none of pod_cpu_request, pod_memory_request, pod_cpu_limit and pod_memory_limit, both maps are empty. The pod then goes to the API server with resources: {}. The MR that fixed it, !7346, says the same: job pods "were showing qosClass: BestEffort instead of the expected Burstable", and after the fix .spec.resources "should now be empty/absent (was {})". It merged on 1 October 2026 and shipped in 19.5.0. The 19.4.3 and 19.3.5 tags of 28 October 2026 still have the old function. The class matters only when the runner config gives the job containers requests or limits. Without them, the pods were BestEffort anyway.
The lab
The lab runs on one DigitalOcean droplet: s-2vcpu-4gb in fra1, Ubuntu 24.04, cgroup v2. A cloud firewall allows SSH from one address and nothing else. The Kubernetes API port is never exposed, and kubectl runs on the node over SSH. For each version, a script removes k3s, installs the pinned release, and runs five stages. The kubelet gets an explicit hard eviction threshold, memory.available<2Gi. It also gets a 30 second pressure transition period instead of the default 5 minutes, so repetitions do not wait for the node condition to clear.
1. pod spec resources: {} + container requests
2. ComputePodQOS sees a block: BestEffort
3. IsPodLevelResourcesSet sees {}: not set
4. oom_score_adj 1000
5. cgroup slice besteffort, cpu.weight 1
6. ResourceQuota scope BestEffort
7. eviction ranking container requests
Connections:
- pod spec -> ComputePodQOS
- pod spec -> IsPodLevelResourcesSet
- ComputePodQOS -> oom_score_adj
- ComputePodQOS -> cgroup slice
- ComputePodQOS -> ResourceQuota scope
- IsPodLevelResourcesSet -> eviction ranking
The pods:
| Pod | Pod-level block | Container requests | Container limits |
| --- | --- | --- | --- |
| be | none | none | none |
| ctl-burstable | none | 100m CPU, 256Mi | 500m CPU, 512Mi |
| emp-burstable | {} | 100m CPU, 256Mi | 500m CPU, 512Mi |
| ctl-guaranteed | none | 200m CPU, 256Mi | 200m CPU, 256Mi |
| emp-guaranteed | {} | 200m CPU, 256Mi | 200m CPU, 256Mi |
| plr-burstable | requests 100m/256Mi, limits 500m/512Mi | none | none |
plr-burstable is the control for the feature itself: a real pod-level block works as documented on every version. The empty block is the only problem.
We wrote down the claim and what would disprove it before the droplet existed, and committed it first (CLAIM.md). Every recorded file carries a hash of the manifests and node scripts, and the summary script refuses to mix files with different hashes.
The-DevOps-Daily/k8s-empty-pod-resources on GitHub
Finding 1: the class flips on 1.34 to 1.36
The API server stored the empty block exactly as written. On v1.34.12, v1.35.9 and v1.36.5 the kubelet then reported BestEffort for both emp-* pods, while their controls kept Burstable and Guaranteed. On v1.37.1 the emp-* pods got the same class as their controls:
results/demo-k3s-v1.36.5.txt and results/demo-k3s-v1.37.1.txt
# k3s v1.36.5+k3s1
# ... kubectl apply and wait omitted; the whole session is in the file
$ kubectl get pods -n demo -o custom-columns='NAME:.metadata.name,POD-LEVEL:.spec.resources,QOS:.status.qosClass'
NAME POD-LEVEL QOS
ctl-burstable <none> Burstable
ctl-guaranteed <none> Guaranteed
emp-burstable map[] BestEffort
emp-guaranteed map[] BestEffort
# k3s v1.37.1+k3s1, same manifests
# ... kubectl apply and wait omitted
$ kubectl get pods -n demo -o custom-columns='NAME:.metadata.name,POD-LEVEL:.spec.resources,QOS:.status.qosClass'
NAME POD-LEVEL QOS
ctl-burstable <none> Burstable
ctl-guaranteed <none> Guaranteed
emp-burstable map[] Burstable
emp-guaranteed map[] Guaranteed
The same result on every version, from results/summary.md:
| Pod | v1.34.12 | v1.35.9 | v1.36.5 | v1.37.1 | v1.37.1, gate off |
| --- | --- | --- | --- | --- | --- |
| ctl-burstable | Burstable | Burstable | Burstable | Burstable | Burstable |
| emp-burstable | BestEffort | BestEffort | BestEffort | Burstable | BestEffort |
| ctl-guaranteed | Guaranteed | Guaranteed | Guaranteed | Guaranteed | Guaranteed |
| emp-guaranteed | BestEffort | BestEffort | BestEffort | Guaranteed | BestEffort |
| plr-burstable | Burstable | Burstable | Burstable | Burstable | Burstable |
| be | BestEffort | BestEffort | BestEffort | BestEffort | BestEffort |
plr-burstable, with real pod-level requests and limits, was Burstable everywhere. Pod-level resources work. Only the empty block breaks the class.
To check that the 1.37 gate is the fix and not something else in the release, we reinstalled v1.37.1 with PodLevelResourcesFixKubeletQOSClass=false on both the API server and the kubelet. Both emp-* pods went back to BestEffort, with oom_score_adj 1000. The API server and kubelet metrics reported the gate as BETA, on by default and off in that run.
Finding 2: the kernel sees a BestEffort pod
QoS is not only a label. The kubelet turns it into numbers that the kernel uses. We read them from /proc and /sys/fs/cgroup on the node, for the main process of each container:
oom_score_adj of the main container process
| | Value | Series |
| --- | --- | --- |
| ctl-burstable, every version | 935 | no pod-level block |
| emp-burstable, 1.34 to 1.36 | 1000 | resources: {}, 1.34 to 1.36 |
| emp-burstable, 1.37.1 | 935 | resources: {}, 1.37.1 |
| ctl-guaranteed, every version | -997 | no pod-level block |
| emp-guaranteed, 1.34 to 1.36 | 1000 | resources: {}, 1.34 to 1.36 |
| emp-guaranteed, 1.37.1 | -997 | resources: {}, 1.37.1 |
_Read from /proc//oom_score_adj on the node. The 1.34.12, 1.35.9 and 1.36.5 values are identical. The kernel adds this value to a process's badness score when it picks one to kill. Source: results/summary.md._
The kernel adds oom_score_adj to each process's badness score before it picks a process to kill, and it accepts values from -1000 to +1000 (proc.rst). A Burstable container gets 1000 - 1000 * request / node memory, which is 935 for a 256Mi request on this node. A Guaranteed container gets -997. The empty block gave both 1000, the same as a pod with no resources at all. oom_score, the current OOM killer score the kernel shows for the process, went from 1290 (Burstable) and 4 (Guaranteed) to 1334.
The cgroup changed too. On v1.36.5:
| | ctl-burstable | emp-burstable | ctl-guaranteed | emp-guaranteed |
| --- | --- | --- | --- | --- |
| QoS slice | kubepods-burstable.slice | kubepods-besteffort.slice | kubepods.slice | kubepods-besteffort.slice |
| pod cpu.weight | 4 | 1 | 8 | 1 |
| pod cpu.max | 0.5 CPU | max (none) | 0.2 CPU | max (none) |
| pod memory.max | 512 MiB | max (none) | 256 MiB | max (none) |
| container cpu.max | 0.5 CPU | 0.5 CPU | 0.2 CPU | 0.2 CPU |
| container memory.max | 512 MiB | 512 MiB | 256 MiB | 256 MiB |
| oom_score | 1290 | 1334 | 4 | 1334 |
The container cgroup still has its own limits, so emp-burstable cannot use more than 512 MiB or half a CPU. What it loses is everything the kubelet sets at pod and QoS level. The pod cgroup has no memory limit and no CPU quota of its own. It sits in kubepods-besteffort.slice, where the kubelet sets cpu.weight to 1 for the whole slice.
Finding 3: half a CPU requested, 0.06 to 0.08 cores received
cpu.weight 1 matters only when the CPU is busy. To see how much, we ran cpu-neighbour, a Burstable pod that requests 1 CPU and runs two busy processes, so both vCPUs are always taken. Next to it, cpu-ctl and cpu-emp took turns, three times each, alternating which went first. Both request 500m CPU with a 1 CPU limit and run one busy process; cpu-emp adds the empty block. Each pod reads its own cgroup's usage_usec over 20 seconds after a 5 second warm-up.
CPU used next to a busy neighbour, median of 3 runs
| | Value | Series |
| --- | --- | --- |
| v1.34.12 cpu-ctl | 0.579 cores | cpu-ctl (no block) |
| v1.34.12 cpu-emp | 0.083 cores | cpu-emp (resources: {}) |
| v1.35.9 cpu-ctl | 0.58 cores | cpu-ctl (no block) |
| v1.35.9 cpu-emp | 0.07 cores | cpu-emp (resources: {}) |
| v1.36.5 cpu-ctl | 0.573 cores | cpu-ctl (no block) |
| v1.36.5 cpu-emp | 0.061 cores | cpu-emp (resources: {}) |
| v1.37.1 cpu-ctl | 0.576 cores | cpu-ctl (no block) |
| v1.37.1 cpu-emp | 0.577 cores | cpu-emp (resources: {}) |
_2 vCPU droplet. Both pods request 500m with a 1 CPU limit and run one busy process; the neighbour requests 1 CPU and runs two. Each pod measured its own cgroup usage_usec over 20 s. Source: results/summary.md._
On the three affected versions, cpu-emp got a median of 0.083, 0.070 and 0.061 cores, against 0.579, 0.580 and 0.573 for cpu-ctl. That is 14.3%, 12.1% and 10.6% of the control's CPU, or 7.0 to 9.4 times less. On v1.37.1 the two were the same: 0.576 and 0.577 cores.
The weights explain the direction. cpu-ctl's pod cgroup has cpu.weight 20 for its 500m request and sits in the burstable slice next to the neighbour. The kubelet sets the burstable slice's weight from the sum of the CPU requests of the Burstable pods in it, which gave 63 here. cpu-emp's pod cgroup has cpu.weight 1 and sits in the besteffort slice. The kubelet always sets that slice to the minimum, cpu.weight 1. The 500m request of cpu-emp still sets a weight on its container cgroup, but that only counts against other containers in the same pod. On a busy node like this one, a CI build in the BestEffort slice competes with cpu.weight 1 at that level and can get far less CPU than Burstable or Guaranteed pods. We did not run a build.
The runs were short and sequential. Each pod ran three times in a fixed alternating order, and we did not record the neighbour's own usage or other load on the host. On the affected versions, each of the nine cpu-emp runs got less CPU than every cpu-ctl run, including the runs where cpu-emp went first. The exact percentages still belong to this setup.
Finding 4: a "no BestEffort" quota rejects the pod
Some clusters forbid BestEffort pods with a quota like this one, so every workload must declare resources:
apiVersion: v1
kind: ResourceQuota
metadata:
name: no-besteffort
spec:
hard:
pods: '0'
scopes: ['BestEffort']
The API server applies the scope at create time, before any kubelet sees the pod, and it calls the same GetPodQOS function. With no status yet, that function computes the class from the spec. On 1.34 to 1.36 it rejected emp-burstable, a pod whose container sets requests and limits:
quota.py on k3s v1.36.5
$ grep -A3 ' quota$' results/k3s-v1.36.5/run.log
2026-11-02T10:33:20Z quota
be admitted=False Error from server (Forbidden): error when creating "/root/k8s-empty-pod-resources/manifests/quota/be.yaml": pods "be" is forbidden: exceeded quota: no-besteffort, requested: pods=1, used: pods=0, limited: pods=0
ctl-burstable admitted=True pod/ctl-burstable created
emp-burstable admitted=False Error from server (Forbidden): error when creating "/root/k8s-empty-pod-resources/manifests/quota/emp-burstable.yaml": pods "emp-burstable" is forbidden: exceeded quota: no-besteffort, requested: pods=1, used: pods=0, limited: pods=0
On v1.37.1 the API server admitted emp-burstable, and still rejected be. We did not run a runner. But a job pod from 18.10.0 to 19.4.x carries the same empty block, so in a namespace with this quota it should fail the same way on 1.34 to 1.36. The fix discussion names the reverse risk: a pod "could theoretically bypass restricted BestEffort quotas by being classified as Guaranteed during admission, while still behaving as BestEffort at the node level". That can happen when the API server has the fix and the kubelet does not.
Finding 5: the eviction order did not change
The five eviction pods each allocate a fixed amount of memory and then sleep. They are sized so that the two possible rules predict different orders:
| Pod | Memory request | Allocates | Working set before the hog | Usage against request |
| --- | --- | --- | --- | --- |
| be | none | 50 MiB | 54 MiB | above (request is 0) |
| ctl | 256Mi | 200 MiB | 204 MiB | about 52 MiB below |
| gua-ctl | 256Mi (= limit) | 150 MiB | 154 MiB | about 102 MiB below |
| emp + {} | 256Mi | 100 MiB | 104 MiB | about 152 MiB below |
| gua-emp + {} | 256Mi (= limit) | 50 MiB | 54 MiB | about 202 MiB below |
If the kubelet ranked by QoS class, emp and gua-emp would go out together with be, before ctl and gua-ctl. If it ranks by container requests, be goes first because any usage exceeds a request of zero, and the rest follow in order of usage minus request: ctl, gua-ctl, emp, gua-emp.
A host process outside every pod, hog.py, then holds MemAvailable at 1000 MiB. That is below the 2Gi threshold even after all five pods are gone, so the kubelet keeps evicting until nothing evictable is left. The kubelet cannot evict the hog. The pods differ in size on purpose, to separate the two models, so this is not a matched pair. CoreDNS also ran on the node. The kubelet ranked it last in every ranking it logged. In 5 of the 12 logs the window ran long enough to show the next step: the kubelet logged cannot evict a critical pod once only CoreDNS was left.
Every repetition on every version gave the request order. This is the kubelet's own ranking on v1.36.5, while it reported emp and gua-emp as BestEffort:
k3s v1.36.5, repetition 1
# the class the kubelet reported for each pod
$ jq -c 'select(.rep == 1) | .qos' results/k3s-v1.36.5/40-pressure.jsonl
{"be":"BestEffort","ctl":"Burstable","emp":"BestEffort","gua-ctl":"Guaranteed","gua-emp":"BestEffort"}
# the kubelet's ranking before each eviction; sed keeps the journald timestamp and the message, and drops the host name, the service PID, the severity letter, the kubelet's own timestamp, the second PID and the source file
$ grep -E 'ranked for eviction|evicted successfully' results/k3s-v1.36.5/40-kubelet-rep1.log | sed -E 's/^([^]+) .*eviction_manager.go:[0-9]+\] /\1 /'
2026-11-02T10:38:17.816980+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/be","evict-check/ctl","evict-check/gua-ctl","evict-check/emp","evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"]
2026-11-02T10:38:20.108268+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/be"
2026-11-02T10:38:21.127785+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/ctl","evict-check/gua-ctl","evict-check/emp","evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"]
2026-11-02T10:38:23.555889+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/ctl"
2026-11-02T10:38:24.579473+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/gua-ctl","evict-check/emp","evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"]
2026-11-02T10:38:27.242735+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/gua-ctl"
2026-11-02T10:38:28.259762+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/emp","evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"]
2026-11-02T10:38:30.555840+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/emp"
2026-11-02T10:38:31.572048+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"]
2026-11-02T10:38:34.078890+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/gua-emp"
The source explains it. rankMemoryPressure sorts by three keys. First, whether usage exceeds the memory request. Then pod priority. Then usage minus the memory request. The first and third keys get the request from GetResourceRequestQuantity. For an empty block, that helper asks IsPodLevelResourcesSet, gets false, and uses the container requests. Priority does not read requests, and the QoS class is not an input at all. If the kubelet had used a request of zero for emp, emp would have been in the first group with be. It came fourth.
When each pod was evicted, 12 repetitions
| Series | Samples | Min | Median | p95 | Max |
| --- | --- | --- | --- | --- | --- |
| be | 12 | 5.6s | 8.350000000000001s | 11.5s | 11.5s |
| ctl | 12 | 9.1s | 11.95s | 15.3s | 15.3s |
| gua-ctl | 12 | 12.7s | 16s | 20.9s | 20.9s |
| emp | 12 | 16.1s | 20.15s | 24.7s | 24.7s |
| gua-emp | 12 | 19.5s | 23.6s | 29.9s | 29.9s |
_Seconds after the host hog started, from the kubelet's log. 3 repetitions each on k3s v1.34.12, v1.35.9, v1.36.5 and v1.37.1. Every repetition evicted the pods in the same order. Source: results/summary.md._
In all 12 repetitions the kubelet started to reclaim 3.0 to 9.2 s after the hog started, and evicted the first pod 2.3 to 3.3 s later. After that it evicted one pod at a time. 41 of the 48 gaps between evictions were 4.0 s or less (median 3.6 s, longest 8.2 s). The last pod was gone 19.5 to 29.9 s after the hog started. It does not wait 10 seconds between evictions: after each one it waits for the pod to be cleaned up and then checks again. Its own available figure at the first eviction was 931 to 1212 MiB, well under the 2Gi threshold and far from empty. The kernel logged no OOM kill in any repetition, so the order is the kubelet's alone.
The two mechanisms are easy to mix up. The kubelet's eviction manager checks every 10 seconds by default when it has nothing to evict, and it ignores the class. The kernel OOM killer acts when memory runs out before the kubelet frees any. It adds oom_score_adj to each process's badness score, and the empty block gives the pod the same 1000 as a pod with no resources at all (Finding 2). We did not trigger a kernel OOM, so we did not watch the kernel make that choice.
What we got wrong before measuring
The research note we started from had the core right and two details wrong.
- Right: Kubernetes 1.34 to 1.36 compute BestEffort for an empty pod-level block. GitLab Runner 18.10 to 19.4 build their job pods with that block (from the source and the fix MR). On 1.34 to 1.36 the pods get oom_score_adj 1000. 1.37 fixes the class while its new gate is on, which is the default.
- Wrong: it called #137150 the upstream issue. That is the fix PR. The issue is #135082.
- Wrong, and it matters: it said these pods are first in line for kubelet eviction under memory pressure. Finding 5 shows they are not.
- Missing: the 1.37 fix is a beta feature gate, not a plain code change, and nothing was backported to the 1.34, 1.35 or 1.36 patch releases.
We got one prediction wrong ourselves. CLAIM.md said the BestEffort-classified pod would get under 10% of the control's CPU. It got 10.6% to 14.3%. Our falsification line was 50%, so the claim stands, but the number we wrote down was too pessimistic, and we report it as a miss.
We also kept a one-repetition smoke run on v1.36.5 (results/_smoke-k3s-v1.36.5/). It used the same code stamp and gave the same results, but the summary does not count it.
What to do
1. Find the pods. This query lists every pod with an empty pod-level block and the class it got:
2. GitLab Runner 18.10.0 to 19.4.x: upgrade to 19.5.0 or later. The job pods then go out without a pod-level block, and the class comes from the containers again.
3. If you cannot upgrade the runner yet, give the block real values. Set pod_cpu_request and pod_memory_request (and the _limit keys if you want limits) under [runners.kubernetes]. The QoS class then comes from those values, like plr-burstable above. This is a behaviour change, not only a workaround: the pod cgroup then takes its limits from the pod-level values too, as it did for plr-burstable.
4. Kubernetes 1.37 fixes it by default. The fix sits behind a beta gate, so check that nobody turned PodLevelResourcesFixKubeletQOSClass off.
5. Check your own templates. A Helm chart that renders a pod-level block from a value that defaults to {} produces the same bug. Render the key only when it has content:
An empty resources: {} inside a container is harmless. Only the pod-level one under spec changes the class.
What we could not conclude
- k3s is one distribution. We ran upstream Kubernetes code as k3s packages it. A managed service can turn the PodLevelResources gate off or carry its own patches. Run the jq query on your own cluster.
- No GitLab instance and no runner ran here. The runner part rests on its source at each tag and on the MR description. The emp-* pods reproduce the field the runner sends, not a full job pod with helper and service containers.
- We did not trigger a kernel OOM kill. The oom_score values are the numbers the kernel uses to choose a victim, read from /proc. We did not watch it choose.
- The eviction timing is ours. It depends on the 2Gi threshold, the hog's 1000 MiB target and how fast it allocates. The order is the finding, not the seconds.
- The CPU gap depends on the neighbours. One neighbour with a 1 CPU request on a 2 vCPU node gave the numbers above. More Burstable or Guaranteed load should make the BestEffort slice's share smaller. On an idle node the weights do not matter, because they only divide contended CPU time; we did not measure that case.
- One node, one admission check. We did not test scheduling across nodes, or any admission rule other than the one quota. The gate-off run on 1.37.1 covered only the class, the kernel-facing numbers and the quota, not CPU or eviction.
- Other consumers of the class were not tested. From the source, the kubelet also rejects new BestEffort pods while the node has the MemoryPressure condition. The static CPU manager gives exclusive CPUs only to Guaranteed pods. We did not measure either.
Summary
On Kubernetes 1.34, 1.35 and 1.36, with the PodLevelResources gate at its default (on), spec.resources: {} makes a pod BestEffort no matter what its containers request. On our node that meant oom_score_adj 1000 instead of 935 or -997, and a place in the BestEffort slice with cpu.weight 1 and no pod-level memory or CPU limit. Next to a busy neighbour the pod got about 7 to 9 times less CPU, and a quota that forbids BestEffort pods rejected it. The empty block did not change the kubelet's eviction order. The ranking uses memory requests, and for an empty block those still come from the containers: 12 of 12 repetitions evicted our pods in exactly the order the requests predict. Kubernetes 1.37 fixes the class behind a beta gate that is on by default, and GitLab Runner 19.5.0 stops sending the block. Run the jq query above on your clusters, and upgrade whichever side you can first.
_Originally published at devops-daily.com._
10 reactions
FRDM i.MX95 - zephyr rtosを使用してみる。(hello worldの実行, イーサネットの利用, SDカードの利用)
FRDM i.MX95 - zephyr rtosを使用してみる。(hello worldの実行, イーサネットの利用, SDカードの利用)
- はじめに
FRDM i.MX95を使用して、zephyr rtosにより、
参照情報を基に以下のことを行った際の記録メモです。
- サンプルアプリケーション hello_worldの実行
- イーサネットの利用
- SDカード(マイクロSDカード)の利用
- 参照情報
FRDM i.MX95はzephyr rtosの公式サポートボードには
現在含まれていませんでしたので、i.MX95 15x15 EVKを参照しています。
- i.MX95 15x15 EVK
- i.MX 95 Applications Processor Reference Manual
- github.com/torvalds/linux/blob/master/arch/arm64/…
- サンプルアプリケーションの実行
- hello_world(1)- 参照情報(i.MX95 15x15 EVK)に記載のアプリケーションの実行
west build -b imx95_evk_15x15/mimx9596/a55 samples/synchronization
この確認では、/dev/sda1がSDカードとなっています。
$ sudo umount /dev/sda1
$ sudo mkfs.vfat -F32 /dev/sda1
fatload mmc 1:1 0xd0000000 zephyr.bin; dcache flush; icache flush; go 0xd0000000
u-boot=> go 0xd0000000
## Starting application at 0xD0000000 ...
*** Booting Zephyr OS build v4.4.0-8011-gb01be6b7b16e ***
thread_a: Hello World from cpu 0 on imx95_evk_15x15!
thread_b: Hello World from cpu 0 on imx95_evk_15x15!
thread_a: Hello World from cpu 0 on imx95_evk_15x15!
thread_b: Hello World from cpu 0 on imx95_evk_15x15!
thread_a: Hello World from cpu 0 on imx95_evk_15x15!
thread_b: Hello World from cpu 0 on imx95_evk_15x15!
- hello_worldの実行(2)- samples/hello_worldでの確認
west build -b imx95_evk_15x15/mimx9596/a55 samples/hello_world -p always
u-boot=> fatload mmc 1:1 0xd0000000 zephyr.bin; dcache flush; icache flush; go 0xd0000000
49604 bytes read in 4 ms (11.8 MiB/s)
## Starting application at 0xD0000000 ...
*** Booting Zephyr OS build v4.4.0-8011-gb01be6b7b16e ***
Hello World! imx95_evk_15x15/mimx9596/a55
- イーサネットの利用
PHYデバイスがi.MX95 15x15 EVKとFRDM i.MX95とでは異なっていたので、
以下を変更して確認しています。
&emdio {
pinctrl-0 = <&emdio_default>;
pinctrl-names = "default";
status = "okay";
phy0: phy@1 {
- compatible = "realtek,rtl8211f";
+ compatible = "motorcomm,yt8521";
reg = <0x1>;
status = "okay";
+ motorcomm,rx-delay-sel = <13>;
+ motorcomm,tx-delay-sel = <13>;
};
};
- サンプルアプリケーションの実行
samples/hello_worldを基に変更しています。
samples/hello_worldのprj.confに以下を追加。
CONFIG_EARLY_CONSOLE=y
CONFIG_NETWORKING=y
CONFIG_TEST_RANDOM_GENERATOR=y
CONFIG_MAIN_STACK_SIZE=5200
CONFIG_SHELL_STACK_SIZE=5200
CONFIG_NET_TX_STACK_SIZE=2048
CONFIG_NET_RX_STACK_SIZE=2048
CONFIG_NET_PKT_RX_COUNT=10
CONFIG_NET_PKT_TX_COUNT=10
CONFIG_NET_BUF_RX_COUNT=20
CONFIG_NET_BUF_TX_COUNT=20
CONFIG_NET_MAX_CONTEXTS=10
CONFIG_NET_DHCPV4=y
CONFIG_NET_IPV4=y
CONFIG_NET_IPV6=n
CONFIG_NET_TCP=y
CONFIG_NET_LOG=y
CONFIG_INIT_STACKS=y
CONFIG_NET_SHELL=y
CONFIG_NET_STATISTICS=y
CONFIG_NET_STATISTICS_PERIODIC_OUTPUT=n
CONFIG_HEAP_MEM_POOL_SIZE=1048576
CONFIG_ETHERNET_LOG_LEVEL_DBG=y
CONFIG_LOG_MODE_IMMEDIATE=y
CONFIG_LOG_DEFAULT_LEVEL=2
CONFIG_PRINTK=y
uart:~$ net dhcpv4 client start 1
uart:~$ net iface 1
Default interface: 1
Interface eth0 (0xd0089518) (Ethernet) [1]
===================================
Link addr : 00:00:00:01:02:00
MTU : 1500
Flags : AUTO_START,IPv4
Device : ethernet@4cc00000 (0xd0035380)
Status : oper=UP, admin=UP, carrier=ON
Ethernet capabilities supported:
RX checksum offload
10 Mbits
100 Mbits
1 Gbits
Ethernet PHY device: phy@1 (0xd0035340)
Ethernet link speed: 1 Gbits full-duplex
IPv4 unicast addresses (max 1):
192.168.128.109/255.255.255.0 DHCP preferred
IPv4 multicast addresses (max 2):
224.0.0.1
IPv4 gateway : 192.168.128.1
DHCPv4 lease time : 86400
DHCPv4 renew time : 43200
DHCPv4 server : 192.168.128.1
DHCPv4 requested : 192.168.128.109
DHCPv4 state : bound
DHCPv4 attempts : 1
DHCPv4 state : bound
uart:~$ net ping 192.168.128.200
PING 192.168.128.200
28 bytes from 192.168.128.200 to 192.168.128.109: icmp_seq=3 ttl=64 time=51.13 ms
--- ping statistics ---
3 packets transmitted, 1 received, 66% packet loss
rtt min/avg/max = 51.13/51.13/51.13 ms
uart:~$ net ping 192.168.128.200
PING 192.168.128.200
28 bytes from 192.168.128.200 to 192.168.128.109: icmp_seq=1 ttl=64 time=70.45 ms
28 bytes from 192.168.128.200 to 192.168.128.109: icmp_seq=2 ttl=64 time=98.31 ms
28 bytes from 192.168.128.200 to 192.168.128.109: icmp_seq=3 ttl=64 time=20.47 ms
--- ping statistics ---
3 packets transmitted, 3 received, 0% packet loss
rtt min/avg/max = 20.47/63.08/98.31 ms
uart:~$
- SDカード(マイクロSDカード)の利用
変更ファイルに関する差分点、追加内容を以下に示しています。
変更点は、参照情報のリファレンスマニュアル、Linuxの実装を基にしています。
usdhc1についても変更していますが、usdhc1については、eMMCのため実際の動作は未確認です。
- device treeの変更
};
+ usdhc1: usdhc@42850000 {
+ compatible = "nxp,imx-usdhc";
+ reg = <0x42850000 0x10000>;
+ interrupts = <GIC_SPI 86 IRQ_TYPE_LEVEL IRQ_DEFAULT_PRIORITY>;
+ interrupt-parent = <&gic>;
+ clocks = <&scmi_clk IMX95_CLK_USDHC1>;
+ max-current-330 = <1020>;
+ max-current-180 = <1020>;
+ max-bus-freq = <208000000>;
+ min-bus-freq = <400000>;
+ status = "disabled";
+ };
+
+ usdhc2: usdhc@42860000 {
+ compatible = "nxp,imx-usdhc";
+ reg = <0x42860000 0x10000>;
+ interrupts = <GIC_SPI 87 IRQ_TYPE_LEVEL IRQ_DEFAULT_PRIORITY>;
+ interrupt-parent = <&gic>;
+ clocks = <&scmi_clk IMX95_CLK_USDHC2>;
+ max-current-330 = <1020>;
+ max-current-180 = <1020>;
+ max-bus-freq = <208000000>;
+ min-bus-freq = <400000>;
+ status = "disabled";
+ };
+
};
drive-strength = "x4";
};
};
+
+ pinmux_usdhc1: pinmux_usdhc1 {
+ group0 {
+ pinmux = <&iomuxc_sd1_clk_usdhc_clk_usdhc1_clk>,
+ <&iomuxc_sd1_strobe_usdhc_strobe_usdhc1_strobe>;
+ drive-strength = "x3";
+ bias-pull-down;
+ slew-rate = "fast";
+ input-schmitt-enable;
+ };
+
+ group1 {
+ pinmux = <&iomuxc_sd1_cmd_usdhc_cmd_usdhc1_cmd>,
+ <&iomuxc_sd1_data0_usdhc_data_usdhc1_data0>,
+ <&iomuxc_sd1_data1_usdhc_data_usdhc1_data1>,
+ <&iomuxc_sd1_data2_usdhc_data_usdhc1_data2>,
+ <&iomuxc_sd1_data3_usdhc_data_usdhc1_data3>,
+ <&iomuxc_sd1_data4_usdhc_data_usdhc1_data4>,
+ <&iomuxc_sd1_data5_usdhc_data_usdhc1_data5>,
+ <&iomuxc_sd1_data6_usdhc_data_usdhc1_data6>,
+ <&iomuxc_sd1_data7_usdhc_data_usdhc1_data7>;
+ drive-strength = "x3";
+ slew-rate = "fast";
+ input-schmitt-enable;
+ bias-pull-up;
+ input-enable;
+ };
+ };
+
+ pinmux_usdhc1_100mhz: pinmux_usdhc1_100mhz {
+ group0 {
+ pinmux = <&iomuxc_sd1_clk_usdhc_clk_usdhc1_clk>,
+ <&iomuxc_sd1_strobe_usdhc_strobe_usdhc1_strobe>;
+ drive-strength = "x3";
+ bias-pull-down;
+ slew-rate = "fast";
+ input-schmitt-enable;
+ };
+
+ group1 {
+ pinmux = <&iomuxc_sd1_cmd_usdhc_cmd_usdhc1_cmd>,
+ <&iomuxc_sd1_data0_usdhc_data_usdhc1_data0>,
+ <&iomuxc_sd1_data1_usdhc_data_usdhc1_data1>,
+ <&iomuxc_sd1_data2_usdhc_data_usdhc1_data2>,
+ <&iomuxc_sd1_data3_usdhc_data_usdhc1_data3>,
+ <&iomuxc_sd1_data4_usdhc_data_usdhc1_data4>,
+ <&iomuxc_sd1_data5_usdhc_data_usdhc1_data5>,
+ <&iomuxc_sd1_data6_usdhc_data_usdhc1_data6>,
+ <&iomuxc_sd1_data7_usdhc_data_usdhc1_data7>;
+ drive-strength = "x3";
+ slew-rate = "fast";
+ input-schmitt-enable;
+ bias-pull-up;
+ input-enable;
+ };
+ };
+
+ pinmux_usdhc1_200mhz: pinmux_usdhc1_200mhz {
+ group0 {
+ pinmux = <&iomuxc_sd1_clk_usdhc_clk_usdhc1_clk>,
+ <&iomuxc_sd1_strobe_usdhc_strobe_usdhc1_strobe>;
+ drive-strength = "x6";
+ bias-pull-down;
+ slew-rate = "fast";
+ input-schmitt-enable;
+ };
+
+ group1 {
+ pinmux = <&iomuxc_sd1_cmd_usdhc_cmd_usdhc1_cmd>,
+ <&iomuxc_sd1_data0_usdhc_data_usdhc1_data0>,
+ <&iomuxc_sd1_data1_usdhc_data_usdhc1_data1>,
+ <&iomuxc_sd1_data2_usdhc_data_usdhc1_data2>,
+ <&iomuxc_sd1_data3_usdhc_data_usdhc1_data3>,
+ <&iomuxc_sd1_data4_usdhc_data_usdhc1_data4>,
+ <&iomuxc_sd1_data5_usdhc_data_usdhc1_data5>,
+ <&iomuxc_sd1_data6_usdhc_data_usdhc1_data6>,
+ <&iomuxc_sd1_data7_usdhc_data_usdhc1_data7>;
+ drive-strength = "x6";
+ slew-rate = "fast";
+ input-schmitt-enable;
+ bias-pull-up;
+ input-enable;
+ };
+ };
+
+ pinmux_usdhc2: pinmux_usdhc2 {
+ group0 {
+ pinmux = <&iomuxc_sd2_clk_usdhc_clk_usdhc2_clk>;
+ drive-strength = "x3";
+ bias-pull-down;
+ slew-rate = "fast";
+ input-schmitt-enable;
+ };
+
+ group1 {
+ pinmux = <&iomuxc_sd2_cmd_usdhc_cmd_usdhc2_cmd>,
+ <&iomuxc_sd2_data0_usdhc_data_usdhc2_data0>,
+ <&iomuxc_sd2_data1_usdhc_data_usdhc2_data1>,
+ <&iomuxc_sd2_data2_usdhc_data_usdhc2_data2>,
+ <&iomuxc_sd2_data3_usdhc_data_usdhc2_data3>;
+ drive-strength = "x3";
+ slew-rate = "fast";
+ input-schmitt-enable;
+ bias-pull-up;
+ input-enable;
+ };
+
+ group2 {
+ pinmux = <&iomuxc_sd2_vselect_usdhc_vselect_usdhc2_vselect>;
+ drive-strength = "x4";
+ slew-rate = "slightly_fast";
+ };
+
+ group3 {
+ pinmux = <&iomuxc_sd2_cd_b_gpio_io_bit_gpio3_io_bit0>,
+ <&iomuxc_sd2_reset_b_gpio_io_bit_gpio3_io_bit7>;
+ drive-strength = "x4";
+ slew-rate = "slightly_fast";
+ bias-pull-up;
+ };
+ };
+
+ pinmux_usdhc2_100mhz: pinmux_usdhc2_100mhz {
+ group0 {
+ pinmux = <&iomuxc_sd2_clk_usdhc_clk_usdhc2_clk>;
+ drive-strength = "x3";
+ bias-pull-down;
+ slew-rate = "fast";
+ input-schmitt-enable;
+ };
+
+ group1 {
+ pinmux = <&iomuxc_sd2_cmd_usdhc_cmd_usdhc2_cmd>,
+ <&iomuxc_sd2_data0_usdhc_data_usdhc2_data0>,
+ <&iomuxc_sd2_data1_usdhc_data_usdhc2_data1>,
+ <&iomuxc_sd2_data2_usdhc_data_usdhc2_data2>,
+ <&iomuxc_sd2_data3_usdhc_data_usdhc2_data3>;
+ drive-strength = "x3";
+ slew-rate = "fast";
+ input-schmitt-enable;
+ bias-pull-up;
+ input-enable;
+ };
+
+ group2 {
+ pinmux = <&iomuxc_sd2_vselect_usdhc_vselect_usdhc2_vselect>;
+ drive-strength = "x4";
+ slew-rate = "slightly_fast";
+ };
+
+ group3 {
+ pinmux = <&iomuxc_sd2_cd_b_gpio_io_bit_gpio3_io_bit0>,
+ <&iomuxc_sd2_reset_b_gpio_io_bit_gpio3_io_bit7>;
+ drive-strength = "x4";
+ slew-rate = "slightly_fast";
+ bias-pull-up;
+ };
+ };
+
+ pinmux_usdhc2_200mhz: pinmux_usdhc2_200mhz {
+ group0 {
+ pinmux = <&iomuxc_sd2_clk_usdhc_clk_usdhc2_clk>;
+ drive-strength = "x3";
+ bias-pull-down;
+ slew-rate = "fast";
+ input-schmitt-enable;
+ };
+
+ group1 {
+ pinmux = <&iomuxc_sd2_cmd_usdhc_cmd_usdhc2_cmd>,
+ <&iomuxc_sd2_data0_usdhc_data_usdhc2_data0>,
+ <&iomuxc_sd2_data1_usdhc_data_usdhc2_data1>,
+ <&iomuxc_sd2_data2_usdhc_data_usdhc2_data2>,
+ <&iomuxc_sd2_data3_usdhc_data_usdhc2_data3>;
+ drive-strength = "x3";
+ slew-rate = "fast";
+ input-schmitt-enable;
+ bias-pull-up;
+ input-enable;
+ };
+
+ group2 {
+ pinmux = <&iomuxc_sd2_vselect_usdhc_vselect_usdhc2_vselect>;
+ drive-strength = "x4";
+ slew-rate = "slightly_fast";
+ };
+
+ group3 {
+ pinmux = <&iomuxc_sd2_cd_b_gpio_io_bit_gpio3_io_bit0>,
+ <&iomuxc_sd2_reset_b_gpio_io_bit_gpio3_io_bit7>;
+ drive-strength = "x4";
+ slew-rate = "slightly_fast";
+ bias-pull-up;
+ };
+ };
+
};
phy-connection-type = "rgmii";
status = "okay";
};
+
+&usdhc1 {
+ pinctrl-0 = <&pinmux_usdhc1>;
+ pinctrl-1 = <&pinmux_usdhc1_100mhz>;
+ pinctrl-2 = <&pinmux_usdhc1_200mhz>;
+ pinctrl-names = "default", "med", "fast";
+ mmc-hs200-1_8v;
+ mmc-hs400-1_8v;
+ read-watermark = <0x10>;
+ write-watermark = <0x80>;
+ status = "disabled";
+
+ sdmmc {
+ compatible = "zephyr,mmc-disk";
+ disk-name = "SD2";
+ status = "disabled";
+ };
+};
+
+&gpio3 {
+ status = "okay";
+};
+
+&usdhc2 {
+ pinctrl-0 = <&pinmux_usdhc2>;
+ pinctrl-1 = <&pinmux_usdhc2_100mhz>;
+ pinctrl-2 = <&pinmux_usdhc2_200mhz>;
+ pinctrl-names = "default", "med", "fast";
+ pwr-gpios = <&gpio3 7 GPIO_ACTIVE_HIGH>;
+ cd-gpios = <&gpio3 0 GPIO_ACTIVE_LOW>;
+ power-delay-ms = <20>;
+ mmc-hs200-1_8v;
+ mmc-hs400-1_8v;
+ read-watermark = <0x10>;
+ write-watermark = <0x80>;
+ status = "okay";
+
+ sdmmc {
+ compatible = "zephyr,sdmmc-disk";
+ disk-name = "SD";
+ status = "okay";
+ };
+};
- driverの変更
static int imx_usdhc_init(const struct device *dev)
{
host_config.readWatermarkLevel = cfg->read_watermark;
host_config.writeWatermarkLevel = cfg->write_watermark;
USDHC_Init(base, &host_config);
+ /* Zephyr側の初期値(1ビット)に、コントローラのバス幅を合わせる */
+ USDHC_SetDataBusWidth(base, kUSDHC_DataBusWidth1Bit);
+ /* U-Boot が UHS で残した VSELECT(1.8V)を、host_io の初期値(3.3V)に合わせる */
+ imx_usdhc_select_1_8v(base, false);
/* Read host controller properties */
imx_usdhc_init_host_props(dev);
/* Set power GPIO low, so card starts powered off */
if (cfg->pwr_gpio.port) {
ret = gpio_pin_configure_dt(&cfg->pwr_gpio, GPIO_OUTPUT_INACTIVE);
if (ret) {
return ret;
}
+ /* UHS(1.8V)状態のカードが確実にリセットされるよう、電源OFFを保持 */
+ k_busy_wait(100 * USEC_PER_MSEC);
} else {
- modules/hal_nxp/mcuxに関する対応
+#ifndef USDHC_COMPAT_H
+#define USDHC_COMPAT_H
+#ifndef __ASSEMBLER__
+#include "fsl_device_registers.h"
+#include "PERI_USDHC.h"
+#define USDHC1_BASE (0x42850000u)
+#define USDHC2_BASE (0x42860000u)
+#define USDHC3_BASE (0x428B0000u)
+#define USDHC1 ((USDHC_Type *)USDHC1_BASE)
+#define USDHC2 ((USDHC_Type *)USDHC2_BASE)
+#define USDHC3 ((USDHC_Type *)USDHC3_BASE)
+#define USDHC_BASE_ADDRS { 0u, USDHC1_BASE, USDHC2_BASE, USDHC3_BASE }
+#define USDHC_BASE_PTRS { (USDHC_Type *)0u, USDHC1, USDHC2, USDHC3 }
+#define USDHC_IRQS { (IRQn_Type)0, (IRQn_Type)86, (IRQn_Type)87, (IRQn_Type)0 }
+
+//#define FSL_FEATURE_SOC_USDHC_COUNT (3)
+
+//#define FSL_FEATURE_USDHC_HAS_EXT_DMA (0)
+/* @brief Has HS400 mode (MIX_CTRL[HS400_MODE]) */
+//#define FSL_FEATURE_USDHC_HAS_HS400_MODE (1)
+/* @brief Has SDR50 support (HOST_CTRL_CAP[SDR50_SUPPORT]) */
+#define FSL_FEATURE_USDHC_HAS_SDR50_MODE (1)
+/* @brief Has SDR104 support (HOST_CTRL_CAP[SDR104_SUPPORT]) */
+#define FSL_FEATURE_USDHC_HAS_SDR104_MODE (1)
+/* @brief USDHC has reset control */
+//#define FSL_FEATURE_USDHC_HAS_RESET (0)
+/* @brief USDHC has no bitfield WTMK_LVL[WR_BRST_LEN] and WTMK_LVL[RD_BRST_LEN] */
+#define FSL_FEATURE_USDHC_HAS_NO_RW_BURST_LEN (1)
+/* @brief If USDHC instance support 8 bit width */
+//#define FSL_FEATURE_USDHC_INSTANCE_SUPPORT_8_BIT_WIDTHn(x) (1)
+/* @brief If USDHC instance support HS400 mode */
+//#define FSL_FEATURE_USDHC_INSTANCE_SUPPORT_HS400_MODEn(x) (0)
+/* @brief If USDHC instance support 1v8 signal */
+//#define FSL_FEATURE_USDHC_INSTANCE_SUPPORT_1V8_SIGNALn(x) (1)
+/* @brief Has no retuning time counter (HOST_CTRL_CAP[TIME_COUNT_RETURNING]) */
+#define FSL_FEATURE_USDHC_REGISTER_HOST_CTRL_CAP_HAS_NO_RETUNING_TIME_COUNTER (1)
+/* @brief Has no VSELECT bit in VEND_SPEC register */
+//#define FSL_FEATURE_USDHC_HAS_NO_VOLTAGE_SELECT (0)
+/* @brief Has no VS18 bit in HOST_CTRL_CAP register */
+//#define FSL_FEATURE_USDHC_HAS_NO_VS18 (0)
+#endif
+#endif
- サンプルアプリケーションの変更
samples/hello_worldを基に変更しています。
project(hello_world)
+#zephyr_include_directories(
+# $ENV{HOME}/dev_env/zephyrproject/modules/hal/nxp/mcux/mcux-sdk-ng/devices/i.MX/i.MX95/periph
+# ${CMAKE_CURRENT_LIST_DIR}/src)
+#zephyr_compile_options(-include ${CMAKE_CURRENT_LIST_DIR}/src/usdhc_compat.h)
+
+set(USDHC_COMPAT_HDR ${CMAKE_CURRENT_LIST_DIR}/src/usdhc_compat.h)
+set(USDHC_COMPAT_INC
+ $ENV{HOME}/dev_env/zephyrproject/modules/hal/nxp/mcux/mcux-sdk-ng/devices/i.MX/i.MX95/periph
+ ${CMAKE_CURRENT_LIST_DIR}/src)
+
+foreach(tgt drivers__sdhc modules__hal_nxp)
+ if(TARGET ${tgt})
+ target_include_directories(${tgt} PRIVATE ${USDHC_COMPAT_INC})
+ target_compile_options(${tgt} PRIVATE "SHELL:-include ${USDHC_COMPAT_HDR}")
+ endif()
+endforeach()
+
target_sources(app PRIVATE src/main.c)
-#include <stdio.h>
+#include <zephyr/kernel.h>
+#include <zephyr/fs/fs.h>
+#include <zephyr/storage/disk_access.h>
+#include <ff.h>
+
+static FATFS fat_fs;
+static struct fs_mount_t mp = {
+ .type = FS_FATFS,
+ .fs_data = &fat_fs,
+ .mnt_point = "/SD:",
+};
int main(void)
{
- printf("Hello World! %s\n", CONFIG_BOARD_TARGET);
+ int rc;
+ uint32_t sector_count = 0, sector_size = 0;
+ struct fs_file_t f;
+
+ printk("Hello World! start\n");
+
+ rc = disk_access_init("SD");
+ printk("disk_access_init: %d\n", rc);
+ if (rc != 0) {
+ return 0;
+ }
+
+ rc = disk_access_ioctl("SD", DISK_IOCTL_GET_SECTOR_COUNT, §or_count);
+ printk("sector_count: %u (rc=%d)\n", sector_count, rc);
+ rc = disk_access_ioctl("SD", DISK_IOCTL_GET_SECTOR_SIZE, §or_size);
+ printk("sector_size: %u (rc=%d)\n", sector_size, rc);
+
+ rc = fs_mount(&mp);
+ printk("fs_mount: %d\n", rc);
+ if (rc != 0) {
+ return 0;
+ }
+
+ struct fs_dir_t dir;
+ struct fs_dirent ent;
+
+ fs_dir_t_init(&dir);
+ rc = fs_opendir(&dir, "/SD:");
+ printk("fs_opendir: %d\n", rc);
+ while (rc == 0) {
+ rc = fs_readdir(&dir, &ent);
+ if (rc != 0 || ent.name[0] == '\0') {
+ break;
+ }
+ printk(" %s %s (%zu)\n",
+ ent.type == FS_DIR_ENTRY_DIR ? "[DIR]" : "[FILE]",
+ ent.name, ent.size);
+ }
+ fs_closedir(&dir);
+
+ const char *test_path = "/SD:/TEST.TXT";
+ const char *write_data = "Zephyr FS Write Test!";
+ size_t write_len = strlen(write_data);
+ char read_buf[64] = {0};
+ ssize_t bytes_processed;
+
+ printk("\n--- Write & Verify Test Start ---\n");
+
+ // 1. ファイルを書き込みモードでオープン
+ fs_file_t_init(&f);
+ rc = fs_open(&f, test_path, FS_O_CREATE | FS_O_WRITE);
+ printk("fs_open (write): %d\n", rc);
+
+ if (rc == 0) {
+ // データを書き込み
+ bytes_processed = fs_write(&f, write_data, write_len);
+ printk("fs_write: %zd bytes written\n", bytes_processed);
+
+ if (bytes_processed != write_len) {
+ printk("Error: Write length mismatch or error occurred.\n");
+ }
+
+ fs_close(&f);
+ } else {
+ printk("Error: Failed to open file for writing.\n");
+ }
+
+ // 2. 書き込んだファイルを読み込みモードでオープンして確認
+ fs_file_t_init(&f);
+ rc = fs_open(&f, test_path, FS_O_READ);
+ printk("fs_open (read): %d\n", rc);
+
+ if (rc == 0) {
+ // データを読み込み
+ bytes_processed = fs_read(&f, read_buf, sizeof(read_buf) - 1);
+ printk("fs_read: %zd bytes read\n", bytes_processed);
+
+ if (bytes_processed > 0) {
+ read_buf[bytes_processed] = '\0'; // 文字列終端処理
+ printk("Read Data: \"%s\"\n", read_buf);
+
+ // 元のデータと一致するか比較
+ if (bytes_processed == write_len && memcmp(write_data, read_buf, write_len) == 0) {
+ printk("Verification SUCCESS! Data matches.\n");
+ } else {
+ printk("Verification FAILED! Data mismatch.\n");
+ }
+ } else {
+ printk("Error: Failed to read data or file is empty.\n");
+ }
+
+ fs_close(&f);
+ } else {
+ printk("Error: Failed to open file for reading.\n");
+ }
+ printk("--- Write & Verify Test End ---\n\n");
return 0;
}
+ CONFIG_SHELL=y
+ CONFIG_DISK_ACCESS=y
+ CONFIG_SDHC=y
+ CONFIG_DISK_DRIVER_SDMMC=y
+ CONFIG_FILE_SYSTEM=y
+ CONFIG_FAT_FILESYSTEM_ELM=y
+ CONFIG_FILE_SYSTEM_SHELL=y
+ CONFIG_DCACHE_LINE_SIZE_DETECT=n
+ CONFIG_DCACHE_LINE_SIZE=64
+ CONFIG_GPIO=y
+ CONFIG_EARLY_CONSOLE=y
+ CONFIG_LOG=y
+ CONFIG_LOG_MODE_IMMEDIATE=y
u-boot=> fatload mmc 1:1 0xd0000000 zephyr.bin; dcache flush; icache flush; go 0xd0000000
172560 bytes read in 5 ms (32.9 MiB/s)
## Starting application at 0xD0000000 ...
[00:00:00.346,000] <inf> sd: Card switched to 1.8V signaling
disk_access_init: 0
sector_count: 60440576 (rc=0)
sector_size: 512 (rc=0)
fs_mount: 0
fs_opendir: 0
[FILE] ZEPHYR.BIN (172560)
--- Write & Verify Test Start ---
fs_open (write): 0
fs_write: 21 bytes written
fs_open (read): 0
fs_read: 21 bytes read
Read Data: "Zephyr FS Write Test!"
Verification SUCCESS! Data matches.
--- Write & Verify Test End ---
Mentions
An Empty resources: {} Makes Kubernetes 1.34 to 1.36 Pods BestEffort --- title: An Empty resources: {} Makes Kubernetes 1.34 to 1.36 Pods BestEffort published: true date: 2026-11-04 15:00:00 UTC tags: Kubernetes,QoS,GitLabRunner,k3s canonical_url: devops-daily.com --- On Kubernetes 1.34, 1.35 and 1.36, one empty block in a pod spec changes how the node treats the pod. If the pod sets spec.resources: {} at pod level, the kubelet gives it QoS class BestEffort. This happens even when every container sets requests and limits, whenever the PodLevelResources feature gate is on, which is the default from 1.34. GitLab Runner's Kubernetes executor builds its job pods with that empty block. Its source does this from 18.10.0 to 19.4.3, unless the runner config sets pod-level values. We ran the same pods on k3s v1.34.12, v1.35.9, v1.36.5 and v1.37.1. On the three older versions, the empty block moved the pod into the BestEffort cgroup slice. The kubelet set its oom_score_adj to 1000, the highest value the kernel accepts. Next to a busy neighbour, a pod that requested half a CPU got 0.06 to 0.08 cores instead of 0.57 to 0.58. A namespace quota that blocks BestEffort pods rejected it. On 1.37.1 the same pods kept their Burstable and Guaranteed classes. One thing did not change, and the research we started from got it wrong. It said these pods are first in line for kubelet eviction under memory pressure. They are not. The kubelet's eviction ranking compares memory usage with the memory request. For an empty block, that request still comes from the containers. The QoS class is not part of the ranking, and the kubelet evicted our pods in the same order on every version. TL;DR - On k3s v1.34.12, v1.35.9 and v1.36.5, a pod with container requests and limits plus spec.resources: {} reported qosClass: BestEffort. The Guaranteed variant did too. On v1.37.1 they reported Burstable and Guaranteed. With the new PodLevelResourcesFixKubeletQOSClass feature gate turned off, 1.37.1 went back to BestEffort. - The node settings followed the class. oom_score_adj went from 935 (Burstable) and -997 (Guaranteed) to 1000, and the pod cgroup moved to kubepods-besteffort.slice with cpu.weight 1 and no pod-level memory.max. The container-level limits stayed in place. - A neighbour pod kept both vCPUs busy. Next to it, the BestEffort-classified pod got a median of 0.061 to 0.083 cores on the three affected versions. The same pod without the empty block got 0.573 to 0.580 (so 10.6% to 14.3%). On 1.37.1 the two were equal. - A ResourceQuota with scope BestEffort and pods: "0" rejected the pod on 1.34 to 1.36 and admitted it on 1.37. - The kubelet's memory-pressure eviction order was the same on all four versions, in all 12 repetitions: the true BestEffort pod first, then by memory usage relative to the container request. The empty block did not move a pod forward. - If you run GitLab Runner on Kubernetes, upgrade to 19.5.0 or later. As of 2 November 2026 the latest Kubernetes patch releases for 1.34, 1.35 and 1.36 still have the bug. Prerequisites - A cluster on Kubernetes 1.34, 1.35 or 1.36 (kubectl version), or a machine where you can run the lab - kubectl and jq - If you run GitLab Runner with the Kubernetes executor: its version (gitlab-runner --version) - To run the lab: a DigitalOcean token (referral link); one s-2vcpu-4gb droplet for about an hour costs a few cents Where the empty block comes from Pod-level resources (KEP-2837) let you set CPU and memory for the pod as a whole, not only per container. The feature gate PodLevelResources went alpha in 1.32 and beta, on by default, in 1.34. Our Kubernetes 1.36 post covers the in-place resize of those values. The QoS calculation has a shortcut for them. This is ComputePodQOS at v1.34.0, v1.35.0 and v1.36.0, and still at the latest patches, v1.34.13, v1.35.10 and v1.36.6: // pkg/apis/core/v1/helper/qos/qos.go, v1.36.6 // When pod-level resources are specified, we use them to determine QoS class. if utilfeature.DefaultFeatureGate.Enabled(features.PodLevelResources) && pod.Spec.Resources != nil { An empty block is not nil. It has no requests and no limits, so the function returns BestEffort. The loop over the containers never runs. The issue that reported this, #135082 from November 2025, says that almost everywhere else Kubernetes does not differentiate between nil and empty pod-level resources. The helper for that, IsPodLevelResourcesSet, treats {} as not set. The fix is PR #137150, merged on 15 July 2026 for 1.37. It adds the condition IsPodLevelResourcesSet(pod) behind a new gate, PodLevelResourcesFixKubeletQOSClass, beta and on by default from 1.37. It was not backported. The patch releases of 27 October 2026, v1.34.13, v1.35.10 and v1.36.6, still have the old condition. The rest of that release is in our Kubernetes 1.37 review; PodLevelResources itself stays beta in 1.37. GitLab Runner added pod-level resources to its job pods in 18.10.0 (MR !5922). At every tag we read (18.10.0, 19.3.5 and 19.4.3), the helper that builds the block returns a non-nil object: // executors/kubernetes/kubernetes.go, GitLab Runner v19.4.3 func (s *executor) podResourcesReference() *api.ResourceRequirements { resources := api.ResourceRequirements{} if s.configurationOverwrites != nil { resources.Limits = s.configurationOverwrites.podLimits resources.Requests = s.configurationOverwrites.podRequests } return &resources } If the runner config sets none of pod_cpu_request, pod_memory_request, pod_cpu_limit and pod_memory_limit, both maps are empty. The pod then goes to the API server with resources: {}. The MR that fixed it, !7346, says the same: job pods "were showing qosClass: BestEffort instead of the expected Burstable", and after the fix .spec.resources "should now be empty/absent (was {})". It merged on 1 October 2026 and shipped in 19.5.0. The 19.4.3 and 19.3.5 tags of 28 October 2026 still have the old function. The class matters only when the runner config gives the job containers requests or limits. Without them, the pods were BestEffort anyway. The lab The lab runs on one DigitalOcean droplet: s-2vcpu-4gb in fra1, Ubuntu 24.04, cgroup v2. A cloud firewall allows SSH from one address and nothing else. The Kubernetes API port is never exposed, and kubectl runs on the node over SSH. For each version, a script removes k3s, installs the pinned release, and runs five stages. The kubelet gets an explicit hard eviction threshold, memory.available<2Gi. It also gets a 30 second pressure transition period instead of the default 5 minutes, so repetitions do not wait for the node condition to clear. 1. pod spec resources: {} + container requests 2. ComputePodQOS sees a block: BestEffort 3. IsPodLevelResourcesSet sees {}: not set 4. oom_score_adj 1000 5. cgroup slice besteffort, cpu.weight 1 6. ResourceQuota scope BestEffort 7. eviction ranking container requests Connections: - pod spec -> ComputePodQOS - pod spec -> IsPodLevelResourcesSet - ComputePodQOS -> oom_score_adj - ComputePodQOS -> cgroup slice - ComputePodQOS -> ResourceQuota scope - IsPodLevelResourcesSet -> eviction ranking The pods: | Pod | Pod-level block | Container requests | Container limits | | --- | --- | --- | --- | | be | none | none | none | | ctl-burstable | none | 100m CPU, 256Mi | 500m CPU, 512Mi | | emp-burstable | {} | 100m CPU, 256Mi | 500m CPU, 512Mi | | ctl-guaranteed | none | 200m CPU, 256Mi | 200m CPU, 256Mi | | emp-guaranteed | {} | 200m CPU, 256Mi | 200m CPU, 256Mi | | plr-burstable | requests 100m/256Mi, limits 500m/512Mi | none | none | plr-burstable is the control for the feature itself: a real pod-level block works as documented on every version. The empty block is the only problem. We wrote down the claim and what would disprove it before the droplet existed, and committed it first (CLAIM.md). Every recorded file carries a hash of the manifests and node scripts, and the summary script refuses to mix files with different hashes. The-DevOps-Daily/k8s-empty-pod-resources on GitHub Finding 1: the class flips on 1.34 to 1.36 The API server stored the empty block exactly as written. On v1.34.12, v1.35.9 and v1.36.5 the kubelet then reported BestEffort for both emp-* pods, while their controls kept Burstable and Guaranteed. On v1.37.1 the emp-* pods got the same class as their controls: results/demo-k3s-v1.36.5.txt and results/demo-k3s-v1.37.1.txt # k3s v1.36.5+k3s1 # ... kubectl apply and wait omitted; the whole session is in the file $ kubectl get pods -n demo -o custom-columns='NAME:.metadata.name,POD-LEVEL:.spec.resources,QOS:.status.qosClass' NAME POD-LEVEL QOS ctl-burstable <none> Burstable ctl-guaranteed <none> Guaranteed emp-burstable map[] BestEffort emp-guaranteed map[] BestEffort # k3s v1.37.1+k3s1, same manifests # ... kubectl apply and wait omitted $ kubectl get pods -n demo -o custom-columns='NAME:.metadata.name,POD-LEVEL:.spec.resources,QOS:.status.qosClass' NAME POD-LEVEL QOS ctl-burstable <none> Burstable ctl-guaranteed <none> Guaranteed emp-burstable map[] Burstable emp-guaranteed map[] Guaranteed The same result on every version, from results/summary.md: | Pod | v1.34.12 | v1.35.9 | v1.36.5 | v1.37.1 | v1.37.1, gate off | | --- | --- | --- | --- | --- | --- | | ctl-burstable | Burstable | Burstable | Burstable | Burstable | Burstable | | emp-burstable | BestEffort | BestEffort | BestEffort | Burstable | BestEffort | | ctl-guaranteed | Guaranteed | Guaranteed | Guaranteed | Guaranteed | Guaranteed | | emp-guaranteed | BestEffort | BestEffort | BestEffort | Guaranteed | BestEffort | | plr-burstable | Burstable | Burstable | Burstable | Burstable | Burstable | | be | BestEffort | BestEffort | BestEffort | BestEffort | BestEffort | plr-burstable, with real pod-level requests and limits, was Burstable everywhere. Pod-level resources work. Only the empty block breaks the class. To check that the 1.37 gate is the fix and not something else in the release, we reinstalled v1.37.1 with PodLevelResourcesFixKubeletQOSClass=false on both the API server and the kubelet. Both emp-* pods went back to BestEffort, with oom_score_adj 1000. The API server and kubelet metrics reported the gate as BETA, on by default and off in that run. Finding 2: the kernel sees a BestEffort pod QoS is not only a label. The kubelet turns it into numbers that the kernel uses. We read them from /proc and /sys/fs/cgroup on the node, for the main process of each container: oom_score_adj of the main container process | | Value | Series | | --- | --- | --- | | ctl-burstable, every version | 935 | no pod-level block | | emp-burstable, 1.34 to 1.36 | 1000 | resources: {}, 1.34 to 1.36 | | emp-burstable, 1.37.1 | 935 | resources: {}, 1.37.1 | | ctl-guaranteed, every version | -997 | no pod-level block | | emp-guaranteed, 1.34 to 1.36 | 1000 | resources: {}, 1.34 to 1.36 | | emp-guaranteed, 1.37.1 | -997 | resources: {}, 1.37.1 | _Read from /proc//oom_score_adj on the node. The 1.34.12, 1.35.9 and 1.36.5 values are identical. The kernel adds this value to a process's badness score when it picks one to kill. Source: results/summary.md._ The kernel adds oom_score_adj to each process's badness score before it picks a process to kill, and it accepts values from -1000 to +1000 (proc.rst). A Burstable container gets 1000 - 1000 * request / node memory, which is 935 for a 256Mi request on this node. A Guaranteed container gets -997. The empty block gave both 1000, the same as a pod with no resources at all. oom_score, the current OOM killer score the kernel shows for the process, went from 1290 (Burstable) and 4 (Guaranteed) to 1334. The cgroup changed too. On v1.36.5: | | ctl-burstable | emp-burstable | ctl-guaranteed | emp-guaranteed | | --- | --- | --- | --- | --- | | QoS slice | kubepods-burstable.slice | kubepods-besteffort.slice | kubepods.slice | kubepods-besteffort.slice | | pod cpu.weight | 4 | 1 | 8 | 1 | | pod cpu.max | 0.5 CPU | max (none) | 0.2 CPU | max (none) | | pod memory.max | 512 MiB | max (none) | 256 MiB | max (none) | | container cpu.max | 0.5 CPU | 0.5 CPU | 0.2 CPU | 0.2 CPU | | container memory.max | 512 MiB | 512 MiB | 256 MiB | 256 MiB | | oom_score | 1290 | 1334 | 4 | 1334 | The container cgroup still has its own limits, so emp-burstable cannot use more than 512 MiB or half a CPU. What it loses is everything the kubelet sets at pod and QoS level. The pod cgroup has no memory limit and no CPU quota of its own. It sits in kubepods-besteffort.slice, where the kubelet sets cpu.weight to 1 for the whole slice. Finding 3: half a CPU requested, 0.06 to 0.08 cores received cpu.weight 1 matters only when the CPU is busy. To see how much, we ran cpu-neighbour, a Burstable pod that requests 1 CPU and runs two busy processes, so both vCPUs are always taken. Next to it, cpu-ctl and cpu-emp took turns, three times each, alternating which went first. Both request 500m CPU with a 1 CPU limit and run one busy process; cpu-emp adds the empty block. Each pod reads its own cgroup's usage_usec over 20 seconds after a 5 second warm-up. CPU used next to a busy neighbour, median of 3 runs | | Value | Series | | --- | --- | --- | | v1.34.12 cpu-ctl | 0.579 cores | cpu-ctl (no block) | | v1.34.12 cpu-emp | 0.083 cores | cpu-emp (resources: {}) | | v1.35.9 cpu-ctl | 0.58 cores | cpu-ctl (no block) | | v1.35.9 cpu-emp | 0.07 cores | cpu-emp (resources: {}) | | v1.36.5 cpu-ctl | 0.573 cores | cpu-ctl (no block) | | v1.36.5 cpu-emp | 0.061 cores | cpu-emp (resources: {}) | | v1.37.1 cpu-ctl | 0.576 cores | cpu-ctl (no block) | | v1.37.1 cpu-emp | 0.577 cores | cpu-emp (resources: {}) | _2 vCPU droplet. Both pods request 500m with a 1 CPU limit and run one busy process; the neighbour requests 1 CPU and runs two. Each pod measured its own cgroup usage_usec over 20 s. Source: results/summary.md._ On the three affected versions, cpu-emp got a median of 0.083, 0.070 and 0.061 cores, against 0.579, 0.580 and 0.573 for cpu-ctl. That is 14.3%, 12.1% and 10.6% of the control's CPU, or 7.0 to 9.4 times less. On v1.37.1 the two were the same: 0.576 and 0.577 cores. The weights explain the direction. cpu-ctl's pod cgroup has cpu.weight 20 for its 500m request and sits in the burstable slice next to the neighbour. The kubelet sets the burstable slice's weight from the sum of the CPU requests of the Burstable pods in it, which gave 63 here. cpu-emp's pod cgroup has cpu.weight 1 and sits in the besteffort slice. The kubelet always sets that slice to the minimum, cpu.weight 1. The 500m request of cpu-emp still sets a weight on its container cgroup, but that only counts against other containers in the same pod. On a busy node like this one, a CI build in the BestEffort slice competes with cpu.weight 1 at that level and can get far less CPU than Burstable or Guaranteed pods. We did not run a build. The runs were short and sequential. Each pod ran three times in a fixed alternating order, and we did not record the neighbour's own usage or other load on the host. On the affected versions, each of the nine cpu-emp runs got less CPU than every cpu-ctl run, including the runs where cpu-emp went first. The exact percentages still belong to this setup. Finding 4: a "no BestEffort" quota rejects the pod Some clusters forbid BestEffort pods with a quota like this one, so every workload must declare resources: apiVersion: v1 kind: ResourceQuota metadata: name: no-besteffort spec: hard: pods: '0' scopes: ['BestEffort'] The API server applies the scope at create time, before any kubelet sees the pod, and it calls the same GetPodQOS function. With no status yet, that function computes the class from the spec. On 1.34 to 1.36 it rejected emp-burstable, a pod whose container sets requests and limits: quota.py on k3s v1.36.5 $ grep -A3 ' quota$' results/k3s-v1.36.5/run.log 2026-11-02T10:33:20Z quota be admitted=False Error from server (Forbidden): error when creating "/root/k8s-empty-pod-resources/manifests/quota/be.yaml": pods "be" is forbidden: exceeded quota: no-besteffort, requested: pods=1, used: pods=0, limited: pods=0 ctl-burstable admitted=True pod/ctl-burstable created emp-burstable admitted=False Error from server (Forbidden): error when creating "/root/k8s-empty-pod-resources/manifests/quota/emp-burstable.yaml": pods "emp-burstable" is forbidden: exceeded quota: no-besteffort, requested: pods=1, used: pods=0, limited: pods=0 On v1.37.1 the API server admitted emp-burstable, and still rejected be. We did not run a runner. But a job pod from 18.10.0 to 19.4.x carries the same empty block, so in a namespace with this quota it should fail the same way on 1.34 to 1.36. The fix discussion names the reverse risk: a pod "could theoretically bypass restricted BestEffort quotas by being classified as Guaranteed during admission, while still behaving as BestEffort at the node level". That can happen when the API server has the fix and the kubelet does not. Finding 5: the eviction order did not change The five eviction pods each allocate a fixed amount of memory and then sleep. They are sized so that the two possible rules predict different orders: | Pod | Memory request | Allocates | Working set before the hog | Usage against request | | --- | --- | --- | --- | --- | | be | none | 50 MiB | 54 MiB | above (request is 0) | | ctl | 256Mi | 200 MiB | 204 MiB | about 52 MiB below | | gua-ctl | 256Mi (= limit) | 150 MiB | 154 MiB | about 102 MiB below | | emp + {} | 256Mi | 100 MiB | 104 MiB | about 152 MiB below | | gua-emp + {} | 256Mi (= limit) | 50 MiB | 54 MiB | about 202 MiB below | If the kubelet ranked by QoS class, emp and gua-emp would go out together with be, before ctl and gua-ctl. If it ranks by container requests, be goes first because any usage exceeds a request of zero, and the rest follow in order of usage minus request: ctl, gua-ctl, emp, gua-emp. A host process outside every pod, hog.py, then holds MemAvailable at 1000 MiB. That is below the 2Gi threshold even after all five pods are gone, so the kubelet keeps evicting until nothing evictable is left. The kubelet cannot evict the hog. The pods differ in size on purpose, to separate the two models, so this is not a matched pair. CoreDNS also ran on the node. The kubelet ranked it last in every ranking it logged. In 5 of the 12 logs the window ran long enough to show the next step: the kubelet logged cannot evict a critical pod once only CoreDNS was left. Every repetition on every version gave the request order. This is the kubelet's own ranking on v1.36.5, while it reported emp and gua-emp as BestEffort: k3s v1.36.5, repetition 1 # the class the kubelet reported for each pod $ jq -c 'select(.rep == 1) | .qos' results/k3s-v1.36.5/40-pressure.jsonl {"be":"BestEffort","ctl":"Burstable","emp":"BestEffort","gua-ctl":"Guaranteed","gua-emp":"BestEffort"} # the kubelet's ranking before each eviction; sed keeps the journald timestamp and the message, and drops the host name, the service PID, the severity letter, the kubelet's own timestamp, the second PID and the source file $ grep -E 'ranked for eviction|evicted successfully' results/k3s-v1.36.5/40-kubelet-rep1.log | sed -E 's/^([^]+) .*eviction_manager.go:[0-9]+\] /\1 /' 2026-11-02T10:38:17.816980+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/be","evict-check/ctl","evict-check/gua-ctl","evict-check/emp","evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"] 2026-11-02T10:38:20.108268+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/be" 2026-11-02T10:38:21.127785+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/ctl","evict-check/gua-ctl","evict-check/emp","evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"] 2026-11-02T10:38:23.555889+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/ctl" 2026-11-02T10:38:24.579473+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/gua-ctl","evict-check/emp","evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"] 2026-11-02T10:38:27.242735+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/gua-ctl" 2026-11-02T10:38:28.259762+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/emp","evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"] 2026-11-02T10:38:30.555840+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/emp" 2026-11-02T10:38:31.572048+00:00 "Eviction manager: pods ranked for eviction" pods=["evict-check/gua-emp","kube-system/coredns-7cfb7bc9c7-svb26"] 2026-11-02T10:38:34.078890+00:00 "Eviction manager: pod is evicted successfully" pod="evict-check/gua-emp" The source explains it. rankMemoryPressure sorts by three keys. First, whether usage exceeds the memory request. Then pod priority. Then usage minus the memory request. The first and third keys get the request from GetResourceRequestQuantity. For an empty block, that helper asks IsPodLevelResourcesSet, gets false, and uses the container requests. Priority does not read requests, and the QoS class is not an input at all. If the kubelet had used a request of zero for emp, emp would have been in the first group with be. It came fourth. When each pod was evicted, 12 repetitions | Series | Samples | Min | Median | p95 | Max | | --- | --- | --- | --- | --- | --- | | be | 12 | 5.6s | 8.350000000000001s | 11.5s | 11.5s | | ctl | 12 | 9.1s | 11.95s | 15.3s | 15.3s | | gua-ctl | 12 | 12.7s | 16s | 20.9s | 20.9s | | emp | 12 | 16.1s | 20.15s | 24.7s | 24.7s | | gua-emp | 12 | 19.5s | 23.6s | 29.9s | 29.9s | _Seconds after the host hog started, from the kubelet's log. 3 repetitions each on k3s v1.34.12, v1.35.9, v1.36.5 and v1.37.1. Every repetition evicted the pods in the same order. Source: results/summary.md._ In all 12 repetitions the kubelet started to reclaim 3.0 to 9.2 s after the hog started, and evicted the first pod 2.3 to 3.3 s later. After that it evicted one pod at a time. 41 of the 48 gaps between evictions were 4.0 s or less (median 3.6 s, longest 8.2 s). The last pod was gone 19.5 to 29.9 s after the hog started. It does not wait 10 seconds between evictions: after each one it waits for the pod to be cleaned up and then checks again. Its own available figure at the first eviction was 931 to 1212 MiB, well under the 2Gi threshold and far from empty. The kernel logged no OOM kill in any repetition, so the order is the kubelet's alone. The two mechanisms are easy to mix up. The kubelet's eviction manager checks every 10 seconds by default when it has nothing to evict, and it ignores the class. The kernel OOM killer acts when memory runs out before the kubelet frees any. It adds oom_score_adj to each process's badness score, and the empty block gives the pod the same 1000 as a pod with no resources at all (Finding 2). We did not trigger a kernel OOM, so we did not watch the kernel make that choice. What we got wrong before measuring The research note we started from had the core right and two details wrong. - Right: Kubernetes 1.34 to 1.36 compute BestEffort for an empty pod-level block. GitLab Runner 18.10 to 19.4 build their job pods with that block (from the source and the fix MR). On 1.34 to 1.36 the pods get oom_score_adj 1000. 1.37 fixes the class while its new gate is on, which is the default. - Wrong: it called #137150 the upstream issue. That is the fix PR. The issue is #135082. - Wrong, and it matters: it said these pods are first in line for kubelet eviction under memory pressure. Finding 5 shows they are not. - Missing: the 1.37 fix is a beta feature gate, not a plain code change, and nothing was backported to the 1.34, 1.35 or 1.36 patch releases. We got one prediction wrong ourselves. CLAIM.md said the BestEffort-classified pod would get under 10% of the control's CPU. It got 10.6% to 14.3%. Our falsification line was 50%, so the claim stands, but the number we wrote down was too pessimistic, and we report it as a miss. We also kept a one-repetition smoke run on v1.36.5 (results/_smoke-k3s-v1.36.5/). It used the same code stamp and gave the same results, but the summary does not count it. What to do 1. Find the pods. This query lists every pod with an empty pod-level block and the class it got: 2. GitLab Runner 18.10.0 to 19.4.x: upgrade to 19.5.0 or later. The job pods then go out without a pod-level block, and the class comes from the containers again. 3. If you cannot upgrade the runner yet, give the block real values. Set pod_cpu_request and pod_memory_request (and the _limit keys if you want limits) under [runners.kubernetes]. The QoS class then comes from those values, like plr-burstable above. This is a behaviour change, not only a workaround: the pod cgroup then takes its limits from the pod-level values too, as it did for plr-burstable. 4. Kubernetes 1.37 fixes it by default. The fix sits behind a beta gate, so check that nobody turned PodLevelResourcesFixKubeletQOSClass off. 5. Check your own templates. A Helm chart that renders a pod-level block from a value that defaults to {} produces the same bug. Render the key only when it has content: An empty resources: {} inside a container is harmless. Only the pod-level one under spec changes the class. What we could not conclude - k3s is one distribution. We ran upstream Kubernetes code as k3s packages it. A managed service can turn the PodLevelResources gate off or carry its own patches. Run the jq query on your own cluster. - No GitLab instance and no runner ran here. The runner part rests on its source at each tag and on the MR description. The emp-* pods reproduce the field the runner sends, not a full job pod with helper and service containers. - We did not trigger a kernel OOM kill. The oom_score values are the numbers the kernel uses to choose a victim, read from /proc. We did not watch it choose. - The eviction timing is ours. It depends on the 2Gi threshold, the hog's 1000 MiB target and how fast it allocates. The order is the finding, not the seconds. - The CPU gap depends on the neighbours. One neighbour with a 1 CPU request on a 2 vCPU node gave the numbers above. More Burstable or Guaranteed load should make the BestEffort slice's share smaller. On an idle node the weights do not matter, because they only divide contended CPU time; we did not measure that case. - One node, one admission check. We did not test scheduling across nodes, or any admission rule other than the one quota. The gate-off run on 1.37.1 covered only the class, the kernel-facing numbers and the quota, not CPU or eviction. - Other consumers of the class were not tested. From the source, the kubelet also rejects new BestEffort pods while the node has the MemoryPressure condition. The static CPU manager gives exclusive CPUs only to Guaranteed pods. We did not measure either. Summary On Kubernetes 1.34, 1.35 and 1.36, with the PodLevelResources gate at its default (on), spec.resources: {} makes a pod BestEffort no matter what its containers request. On our node that meant oom_score_adj 1000 instead of 935 or -997, and a place in the BestEffort slice with cpu.weight 1 and no pod-level memory or CPU limit. Next to a busy neighbour the pod got about 7 to 9 times less CPU, and a quota that forbids BestEffort pods rejected it. The empty block did not change the kubelet's eviction order. The ranking uses memory requests, and for an empty block those still come from the containers: 12 of 12 repetitions evicted our pods in exactly the order the requests predict. Kubernetes 1.37 fixes the class behind a beta gate that is on by default, and GitLab Runner 19.5.0 stops sending the block. Run the jq query above on your clusters, and upgrade whichever side you can first. _Originally published at devops-daily.com._
github.com/torvalds/linux/blob/master/drivers/med… あとはLinuxのソースコードもみようちゃんと
Ubuntuの仮想化基盤(KVM)にRadeon GPU パススルーを設定するには 1. はじめに 1.1 前書き 昨今、エンタープライズ向けLLMの性能が飛躍的に上昇しており非常に使い勝手が良くなってきています。また、ローカル LLM も性能が上がってきており、仕事で使う機会がたまにあります。 用途にも依存しますが、ローカル LLM でも十分な性能を発揮する時があります。そういった点で自宅の倉庫で眠っていたRX 6800 XTがあることを思い出し、ローカル LLM を構築してみようかなと思いました。 自宅で仮想化基盤(KVM)を使っているため、この基盤の仮想マシンで GPU を使えるようにする方法を整理し、その手順を、自分の整理も兼ねて記事にまとめました。 最終的な目的は、仮想マシンでローカル LLM を動かすことです。この記事では、その土台として、仮想マシンで GPU が認識されるところまでを扱います。ゲストOSでの ROCm の導入や、ローカル LLM の実行は扱いません。 そのため、ホストマシンで直接AIを動かす場合やDockerコンテナでの利用では、この記事の設定は参考になりません。 趣味で構築した内容で、調査にはAIも使っています。間違いや、より良い方法があれば、教えていただけると幸いです。 1.2 今回使用するハードウェア・ソフトウェア構成 1.2.1 ハードウェア構成 CPU: AMD Ryzen 7 2700X マザーボード: X470 AORUS ULTRA GAMING メモリ: 32GB GPU1(パススルー用・PCIe 1 Slot): AMD Radeon RX 6800 XT GPU2(ホストOS用・PCIe 3 Slot): NVIDIA GeForce GT 710 1.2.2 ソフトウェア構成 ホストOS OS: Ubuntu Server 26.04 LTS カーネル: 7.0.0-42-generic QEMU: 10.2.1 libvirt: 12.0.0 ゲストOS OS: Ubuntu Server 26.04 LTS 2. 仮想化基盤(KVM)でGPUを使うには この記事では、次の用語を使い分けます。「ホスト」だけでは、ハードウェアを指すのか OS を指すのかが分からないためです。 用語 意味 ホストマシン 物理的なコンピュータ(ハードウェア全体) ホストOS ホストマシン上で直接動く OS。KVM を動かす側(この記事では Ubuntu Server) 仮想マシン ホストOSが作る、仮想のコンピュータ ゲストOS 仮想マシンの中で動く OS この章では仮想化基盤(KVM)でGPUを活用するにはどのような方法があるかを整理します。 2.1 仮想マシンで GPU を使う方法 仮想マシンの中で動くゲストOSは、ホストOSのドライバをそのまま使えません。そのため、仮想マシンで GPU を使う方法は、以下の表で3つの項目に整理されます。 方式 GPU 制御OS 割り当て方式(共有・分割・単一) ゲストOSから見える GPU 仕組み 準仮想化(virtio-gpu:VirGL、Venus など) ホストOS 共有 汎用の仮想 GPU ゲストOSの描画・計算の命令をホストOSに転送し、ホストOSのドライバが GPU で実行する ハードウェア分割(NVIDIA vGPU、SR-IOV など) ゲストOS 分割 分けられた仮想 GPU 1枚の GPU を複数の仮想 GPU に分け、それぞれをゲストOSに渡して直接操作させる パススルー ゲストOS 単一 実物の GPU GPU を丸ごと1台の仮想マシンに渡し、ゲストOSに直接操作させる 3. ローカル LLM の実行を見据えた、GPU の割り当て方式の選択 最終的には、仮想マシンでローカル LLM を動かします。そのため、LLM が GPU で計算するために使うソフトウェア(ROCm / CUDA など。3.1節で説明します)が動く方式を、2章の3つから選びます。 3.1 ローカル LLM で使う GPU のソフトウェア ローカル LLM が GPU で計算するには、計算用のライブラリやソフトウェアが必要です。GPU は CPU とは異なり、独自の処理系を持つためです。このソフトウェアは、AMD の Radeon なら ROCm、NVIDIA の GeForce なら CUDA です。ただし、これらを使えるかどうかは、GPU・OS・ソフトウェアのバージョンの対応状況によります。たとえば、ROCm 10.1.0 の対応表に、この記事で使う RX 6800 XT は載っていません(載っていないことは、動作しないことの証明ではありません)。古い GPU やAMD、NVIDIA以外の GPU を使う場合は、Vulkan などもあります。 そのため、どの方式でも、これらのソフトウェアが使えることが前提になります。Vulkan に対応するランタイムは、ベンダー純正のソフトウェアに比べて限られます。したがって、Vulkan は、純正のソフトウェアが対応していない古い GPU などで使う位置づけと考えています。 3.2 ローカル LLM にはどの方式が現実的か この節では、2.1節 の3つの方式のそれぞれで、3.1節 のソフトウェア(ROCm / CUDA、Vulkan)が使えるかを整理し、ローカル LLM に現実的な方式を選びます。 方式 ROCm / CUDA Vulkan 使うための GPU・ライセンスの条件 準仮想化(virtio-gpu) 使えない 使える(Venus) ホストOSの対応ドライバと Vulkan の拡張機能、ゲスト・ホスト側のソフトウェア(virglrenderer、Mesa、カーネルなど)の条件が必要で、条件を満たしてもすべてのドライバで動くとは限らない ハードウェア分割(vGPU など) 対応する GPU のみ 対応する GPU のみ 対応する GPU が限られ、公式の資料では一般向けの GPU は対象として確認できない。NVIDIA vGPU は、対応 GPU がデータセンター向けなどで、対応 GPU の一覧に GeForce は含まれず、ライセンスが必要で、CUDA を使えるかは vGPU の種類・GPU・ソフトウェアのバージョン・ライセンスによる。AMD の MxGPU は、公式の対象が Instinct と Radeon PRO V710 で、Radeon RX は一覧にない パススルー 対応する GPU・OS であれば使える 使える 一般向けの GPU でも使える ROCm / CUDA は、純正のカーネルドライバ(amdgpu、nvidia)が実物の GPU を直接操作することを前提に動きます。準仮想化では、ゲストOSから実物の GPU が見えない(2.1節の表)ため、使えません。ハードウェア分割は、対応する GPU とライセンスの条件が厳しく、公式の資料では一般向けの GPU が対象として確認できません。したがって、一般向けの GPU で純正ソフトウェア(ROCm / CUDA)を使うなら、現実的な方法はパススルーです。 3.3 コンテナ(Docker)はなぜ GPU を簡単に使えるのか ここでコンテナで GPU を使用する際に、上記の仮想マシンと違い、なぜ簡単に使えるのかを整理します。 結論から言うと、コンテナはホストOSのカーネルを使用するため、ホストOSのGPUドライバをそのまま利用できるからです。 GPU は、カーネルのドライバ(Radeon なら amdgpu)が操作するデバイスです。 コンテナはホストOSのカーネルをそのまま使います。ホストOSのドライバが GPU を管理し、コンテナにはデバイスノード(/dev/dri/*、/dev/kfd など)とライブラリを見せるだけです。そのため、ホストOSのGPUドライバをそのまま使え、複数のコンテナで GPU を共有することもできます。 仮想マシンの中で動くゲストOSは自分のカーネルを持ちます。ゲストOSのカーネルは、ホストOSのドライバを使えません。 この違いが、2.1節 の表で GPU を操作する OS が、ホストOS(準仮想化)とゲストOS(ハードウェア分割、パススルー)に分かれる理由です。 4. GPU パススルーの設定概要 3章の結果、ローカル LLM を動かす仮想マシンには、GPU パススルーが現実的だと分かりました。そのため、この記事ではGPU パススルーを採用します。 この章では、仮想化基盤(KVM)で GPU パススルーを設定するための全体像を示します。IOMMU の仕組みは5章で、それぞれの設定手順は6章以降で説明します。 4.1 全体像 GPU パススルーを実現するためには、以下のような手順が必要です。 BIOSでIOMMU(デバイスのメモリアクセスを制御する機能)を有効化し、起動時に表示するGPUを指定する OSでIOMMUが有効になっていることを確認する VFIO(デバイスを仮想マシンに直接渡すためのLinuxカーネルの仕組み)のモジュールをロードし、GPUをVFIOにバインド(GPUを担当するドライバをVFIOにすること)する 仮想マシンにGPUを割り当てる これらの手順を順に設定していくことで、仮想マシンでGPUを直接利用できるようになります。 5. IOMMU について 4章の GPU パススルーを実現するための、全体像を提示しました。その全体像で、IOMMUという新たな用語が出てきたと思います。IOMMUはパススルーをする上では必須ですが、どういった仕組みなのかを理解しておいた方が良いと考えています。そのため、この章では、IOMMU が必要になる理由と、IOMMU の仕組みを整理します。設定手順は6章以降で説明します。 5.1 GPU とメモリのやり取り(DMA) IOMMU の役割を理解するために、先にホストマシンで GPU を使って計算をする際に、データがどのようにやり取りされるかを整理します。 GPU が計算に使うデータは、まずメインメモリ(RAM)に置かれます。CPU 上のドライバは、GPU に「どのアドレスのデータを使うか」を伝えます。GPU は、そのアドレスのメモリを、CPU を介さずに直接読み書きします。この仕組みを DMA(Direct Memory Access)と言います。計算結果も同様に、GPU がメモリに書き込み、CPU がそれを読み取ります。 ここで重要なのは、次の2点です。 アドレスを決めるのは、GPU ではなくドライバです。 GPU は、伝えられたアドレスをそのまま使って読み書きします。 5.2 仮想マシンのメモリの扱い 次に、仮想化基盤での仮想マシンのメモリの扱いを整理する必要があります。ここでは、仮想マシンのメモリが、ホストマシンのメモリとどう対応づけられているかを整理します。 なお、以降に出てくるメモリの番地は、仕組みを説明するために簡略化した値です。実際のアドレスは 16 進数で表記され、もっと大きな値になります。 ゲストOSや、その起動用のファームウェアは、使うアドレスが決まっています(たとえば、BIOS で起動する x86 では、0 番地付近を割り込みベクタテーブルなどが使います)。そのため、ホストマシンのメモリの 100〜200 番地を仮想マシンに割り当てる場合でも、ゲストOSには 100〜200 番地ではなく、0〜100 番地のメモリがあるように見せます。ホストOSは「ゲストOSの 0〜100 番地 = ホストマシンの 100〜200 番地」という変換表を持ちます。仮想マシンの CPU がメモリにアクセスするときは、CPU がこの表で変換します(AMD では NPT、Intel では EPT と呼ばれる仕組みです)。 上記の説明でもある通りこのメモリのアドレス変換は CPU が担います。そのため、CPU を介さずにメモリ書き込みをする DMA では、この変換表が使われません。 ゲストOS側のドライバが提示するアドレスはあくまでゲスト物理アドレスです。 GPUはこの指定されたアドレスにアクセスしますが、変換表を通らないため、ホストマシンの実メモリを書き換えてしまいます。 例を提示するとゲストOSで 50 番地を指定した場合、GPUはホストマシンの 50 番地にアクセスしてしまうということです。 アドレスの食い違いにより、ホストOSや他の仮想マシンが使っているメモリを、ゲストOS側に悪意がなくても書き換えるおそれがあります。 ゲストOS側が悪意のあるユーザに操作されることがある場合に、任意のアドレスを GPU に指定されると、ホストマシン全体のメモリを読み書きされるおそれがあります。 5.3 IOMMUとは 5.2節 の問題を解決するのが、IOMMU(Input-Output Memory Management Unit)です。IOMMU は、GPU が DMA で使うアドレスを、ホストOSがあらかじめ設定した変換表で変換し、割り当てたメモリの範囲だけにアクセスを制限するハードウェア機能です。5.2節 の例では、ゲストOSのドライバが指定した 50 番地への DMA は、ホストマシンの 150 番地に届きます。割り当てた範囲(ゲストOSの 0〜100 番地)の外へのアクセスは遮断されます。 ホストOSは、デバイスから見えるアドレスがゲスト物理アドレスになるように、IOMMU の変換表を作ります。5.2節 で CPU が使っている表と同じ対応(ゲスト物理アドレスからホストマシンのアドレス)を、IOMMU にも登録します。そのため、ゲストOSのドライバは、IOMMU の有無を意識せず、自分のメモリのアドレスをそのまま GPU に渡せます。データのやり取りは、次のようになります(5.2節 と同じ数値の例です)。 ゲストOS上のプログラムが、ゲストOSの 50 番地にデータを書きます。CPU が変換し、ホストマシンの 150 番地に書かれます。 ゲストOSのドライバが、GPU に「50 番地を読め」と指示します。 GPU が 50 番地を DMA で読みます。IOMMU が変換し、ホストマシンの 150 番地から、手順 1 のデータを読みます。 結果は、逆向きに同じ経路をたどります。GPU が 80 番地に書くと、IOMMU がホストマシンの 180 番地に書き込みます。ゲストOSが 80 番地を読むと、CPU が同じ 180 番地を読みます。 上記の説明でIOMMUが必要な理由が見えてくると思います。そのため、Linux カーネルの VFIO(Virtual Function I/O) では、メモリを安全に管理するため、IOMMU がない環境でデバイスを仮想マシンに割り当てることに対応していません。GPU パススルーには、IOMMU を有効にする必要があります。なお、VFIO には IOMMU がない環境でデバイスを使うための No-IOMMU モードがありますが、安全性が確保されず、仮想マシンへのデバイス割り当てには使えません。 IOMMU が分離できる単位は、デバイス1つとは限りません。複数の機能を持つデバイスでは、機能同士のやり取りが IOMMU を通らない場合があるためです。そのため、他から分離できるデバイスの集まりを IOMMU グループと呼び、VFIO もグループ単位でデバイスを扱います。たとえば GPU の映像機能と音声機能は、同じグループに入ることがあります。 6. BIOS 設定 6章からは、実際にGPU パススルーをするための手順を書いていきます。 BIOSの設定では、IOMMUの有効化と仮想化設定(SVM Mode)の設定、今回の環境での映像出力優先設定をします。 6.1 IOMMU の有効化 BIOSの設定はメーカごとで異なります。今回は私のマザーボードの例を示します。実際の設定はお使いのマザーボードのマニュアルを参照してください。 AMD の CPU では、BIOS で次の2つを有効にします。項目名は機種や BIOS のバージョンで異なります。 仮想化(SVM Mode) IOMMU 実際に設定する流れを以下の画像に示します。赤枠が操作する項目です。 6.1.1 仮想化(SVM Mode)の有効化 「M.I.T.」タブの「高度な周波数設定」を開く。 「高度なCPUコア設定」を開く。 「SVM Mode」を「有効」にする。 6.1.2 IOMMU の有効化 「チップセット」タブを開き、「IOMMU」を「有効」にする。 今回の環境はAMDの CPU のため、Intel の CPU の場合や、マザーボードによって設定項目が異なります。それは設定者が確認してください。 6.2 映像出力優先設定 ! CPUに内蔵 GPU があり、ホストの起動時表示に使える構成では、この設定が必要でない場合があります。必要に応じて、映像出力の優先設定を確認してください。 今回の環境では内蔵GPUがないCPUのため、1枚のGPUにするとパススルー後は映像出力用のデバイスがないため、非常に困ります。 そのため、今回は映像出力用のGPUを搭載して、パススルーする GPU とは別のGPUをホストOS用にします。この構成の場合、ホストOS用の GPU をメインの映像出力先にしないと不都合が出ます。 BIOS/UEFI の画面、ブートローダ、OS の起動中の表示を、どの GPU から出すかを選ぶ設定です。Initial Display Output、Primary Display、Primary VGA などの名前で、「PCIe 1 スロット」「内蔵 GPU」のように選びます。 ホストOS用の NVIDIA GPU(PCIe 3 Slot) を選びます。パススルーする GPU が起動時の表示に使われると、ホストOSのフレームバッファや、起動時に書き換えられた GPU の初期化データ(VBIOS)が、仮想マシンでの GPU の初期化を妨げることがあるためです。 設定例を以下に示します。 「周辺機器」タブを開き、「Initial Display Output」を「PCIe 3 Slot」にする。 変更が完了したら、変更を保存して再起動します。 6.3 OSでBIOS設定が反映されているかの確認 BIOSで設定した内容が、OSに反映されているかを確認します。 6.3.1 前提(パススルーする GPU の認識確認) まず、設定の確認をする前に OS でGPUが認識されていることを確認します。認識されていない場合は、物理的な接続状況や故障していないかを確認する必要があります。 6.3.1.1 映像機能の認識確認 まず、映像機能が認識されていることを確認します。以下のコマンドを実行し、想定される出力例を示します。デバイスが出力されていれば問題ありません。 lspci -nn | grep -i -E "vga|3d|display" 想定される出力例 07:00.0 VGA compatible controller [0300]: NVIDIA Corporation GK208B [GeForce GT 710] [10de:xxxx] (rev a1) 0b:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] [1002:xxxx] (rev c1) 上記のコマンドの出力例の先頭に記載されている 0b:00.0 が PCI アドレス、末尾のほうに記載されている [1002:xxxx] が PCI ID です。この 2 つは、表すものが違います。このIDとアドレスは設定する項目ごとで異なります。そのため、IDとアドレスは控えておいてください。 情報 例 表すもの PCI ID 1002:xxxx デバイスの種類(ベンダーと型番) PCI アドレス 0b:00.0 デバイスの場所(ホストマシンのどこに挿さっているか) 同じ型番の GPU を2枚挿している場合、PCI ID は同じですが、PCI アドレスは違います。 6.3.1.2 音声機能の確認 GPU は、映像のほかに音声(HDMI/DP オーディオ)を扱う機能を持っています。VFIO でホストOSから切り離す単位は、デバイスではなく IOMMU グループ(5.3節)で、映像機能と音声機能は同じグループに入ることが多いです。同じグループに属する場合は、音声機能も一緒に切り離す必要があるため、音声機能の PCI ID も確認します。 なお、音声機能を仮想マシンに渡すかどうかは別の話です。仮想マシンで音声を使わない用途(ローカル LLM の計算だけに使う場合など)であれば、渡さなくて構いません。 同じカードの機能は、PCI アドレスのバス番号とスロット番号(0b:00)を指定すると全て表示されます。そこから音声の行だけを取り出します。0b:00 は 6.3.1.1 の例です。 lspci -nn -s 0b:00 | grep -i audio 想定される出力例 0b:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21/23 HDMI/DP Audio Controller [1002:yyyy] 6.3.1.1 と 6.3.1.2 で表示された PCI ID(映像機能と音声機能)は、控えておいてください。IOMMUや設定で使う値です。 6.3.2 SVM Mode / IOMMU の有効化確認 6.3.2.1 SVM Mode の有効化確認 まず、SVM Mode が有効になっていることを確認します。 カーネルのバージョンを確認 uname -r 想定される出力例 7.0.0-42-generic SVM が有効か確認 grep -c -w svm /proc/cpuinfo 想定される出力例 1以上であればSVMが有効 16 カーネルのバージョンが 6.7 以上で、2つ目のコマンドが 1 以上を表示すれば、有効です。6.7 未満では、BIOS で無効でも svm が表示されることがあるためです。 6.3.2.2 IOMMU の有効化確認 次に、IOMMU が有効になっていることを確認します。 ! Ubuntu 26.04 では、IOMMU を有効にするカーネルパラメータの設定は不要です。ですが、古いOSや他のディストリビューションでは、intel_iommu=onなどのカーネルでIOMMUを有効にする設定が必要になる場合があります。 今回使用するカーネル(7.0.0-42-generic)は CONFIG_AMD_IOMMU=y、CONFIG_INTEL_IOMMU_DEFAULT_ON=y でビルドされており、BIOS で有効にすれば、カーネルが自動でIOMMUを有効にします。ビルド設定は grep -E 'CONFIG_AMD_IOMMU=|CONFIG_INTEL_IOMMU_DEFAULT_ON' /boot/config-$(uname -r) で確認できます。 sudo dmesg | grep -i -e AMD-Vi -e IOMMU 想定される出力例 [ 0.000000] AMD-Vi: Using global IVHD EFR:0xf77ef22294ada, EFR2:0x0 [ 0.283728] iommu: Default domain type: Translated [ 0.283728] iommu: DMA domain TLB invalidation policy: lazy mode [ 0.329182] pci 0000:00:00.2: AMD-Vi: IOMMU performance counters supported [ 0.329335] pci 0000:00:01.0: Adding to iommu group 0 [ 0.329367] pci 0000:00:01.3: Adding to iommu group 1 [ 0.329409] pci 0000:00:02.0: Adding to iommu group 2 [ 0.329467] pci 0000:00:03.0: Adding to iommu group 3 [ 0.329498] pci 0000:00:03.1: Adding to iommu group 4 [ 0.329540] pci 0000:00:04.0: Adding to iommu group 5 [ 0.329596] pci 0000:00:07.0: Adding to iommu group 6 [ 0.329627] pci 0000:00:07.1: Adding to iommu group 7 [ 0.329682] pci 0000:00:08.0: Adding to iommu group 8 [ 0.329713] pci 0000:00:08.1: Adding to iommu group 9 [ 0.329770] pci 0000:00:14.0: Adding to iommu group 10 [ 0.329800] pci 0000:00:14.3: Adding to iommu group 10 [ 0.329931] pci 0000:00:18.0: Adding to iommu group 11 [ 0.329961] pci 0000:00:18.1: Adding to iommu group 11 [ 0.329991] pci 0000:00:18.2: Adding to iommu group 11 [ 0.330020] pci 0000:00:18.3: Adding to iommu group 11 [ 0.330049] pci 0000:00:18.4: Adding to iommu group 11 [ 0.330078] pci 0000:00:18.5: Adding to iommu group 11 [ 0.330107] pci 0000:00:18.6: Adding to iommu group 11 [ 0.330136] pci 0000:00:18.7: Adding to iommu group 11 [ 0.330205] pci 0000:01:00.0: Adding to iommu group 12 [ 0.330236] pci 0000:01:00.1: Adding to iommu group 12 [ 0.330267] pci 0000:01:00.2: Adding to iommu group 12 [ 0.330282] pci 0000:02:00.0: Adding to iommu group 12 [ 0.330294] pci 0000:02:01.0: Adding to iommu group 12 [ 0.330306] pci 0000:02:02.0: Adding to iommu group 12 [ 0.330324] pci 0000:02:03.0: Adding to iommu group 12 [ 0.330337] pci 0000:02:04.0: Adding to iommu group 12 [ 0.330350] pci 0000:02:09.0: Adding to iommu group 12 [ 0.330363] pci 0000:03:00.0: Adding to iommu group 12 [ 0.330375] pci 0000:06:00.0: Adding to iommu group 12 [ 0.330387] pci 0000:07:00.0: Adding to iommu group 12 [ 0.330399] pci 0000:07:00.1: Adding to iommu group 12 [ 0.330412] pci 0000:08:00.0: Adding to iommu group 12 [ 0.330441] pci 0000:09:00.0: Adding to iommu group 13 [ 0.330472] pci 0000:0a:00.0: Adding to iommu group 14 [ 0.330510] pci 0000:0b:00.0: Adding to iommu group 15 [ 0.330543] pci 0000:0b:00.1: Adding to iommu group 16 [ 0.330574] pci 0000:0c:00.0: Adding to iommu group 17 [ 0.330604] pci 0000:0c:00.2: Adding to iommu group 18 [ 0.330634] pci 0000:0c:00.3: Adding to iommu group 19 [ 0.330665] pci 0000:0d:00.0: Adding to iommu group 20 [ 0.330695] pci 0000:0d:00.2: Adding to iommu group 21 [ 0.330728] pci 0000:0d:00.3: Adding to iommu group 22 [ 0.333872] AMD-Vi: Extended features (0xf77ef22294ada, 0x0): PPR NX GT IA GA PC GA_vAPIC [ 0.333899] AMD-Vi: Interrupt remapping enabled [ 0.334089] AMD-Vi: Virtual APIC enabled [ 0.334189] perf/amd_iommu: Detected AMD IOMMU #0 (2 banks, 4 counters/bank). ls /sys/kernel/iommu_groups/ 想定される出力例 0 1 10 11 12 13 14 15 16 17 18 19 2 20 21 22 3 4 5 6 7 8 9 ls で番号のディレクトリが表示されれば、IOMMU は有効です。 6.3.3 IOMMU グループの確認 パススルーする GPU の IOMMU グループを確認します。6.3.1項 で控えた映像機能と音声機能の PCI ID を IDS に指定すると、それぞれが属するグループと、そのグループのデバイスだけが表示されます。 IDS="1002:xxxx 1002:yyyy" # 6.3.1 で控えた映像機能・音声機能の PCI ID groups=$(for id in $IDS; do for dev in $(lspci -D -n -d "$id" | cut -d' ' -f1); do basename "$(readlink /sys/bus/pci/devices/$dev/iommu_group)" done done | sort -un) for g in $groups; do echo "IOMMU group $g:" for d in /sys/kernel/iommu_groups/$g/devices/*; do lspci -nns "${d##*/}" done done 想定される出力例 IOMMU group 15: 0b:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] [1002:xxxx] (rev c1) IOMMU group 16: 0b:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21/23 HDMI/DP Audio Controller [1002:yyyy] 表示されたグループが1つで、映像機能と音声機能の両方が含まれていれば、同じグループです。グループが2つに分かれて表示されれば、別のグループです。同じグループに GPU の機能以外のデバイス(PCIe のブリッジを除く)があると、それも一緒にホストOSのドライバから切り離し、vfio-pci にバインドする必要があります。VFIO は、グループ内のすべてのデバイスが準備できて初めて、そのグループを使えるためです。仮想マシンに渡すかどうかは別の話です。 今回のケースでは、映像機能と音声機能が別のグループに分かれています。 6.3.4 映像出力優先設定の確認 ホストOS用の GPU が起動時の表示に使われていることを確認します。PCI アドレス 07:00.0 はホストOS用の GPU、0b:00.0 はパススルーする GPU です(6.3.1項 の例)。/sys 配下のパスでは、先頭にドメイン番号 0000: を付けます。 以下のコマンドでホストOS用の GPU が 1、パススルーする GPU が 0 であれば成功です。パススルーする GPU が 1 の場合は、6.2節 の設定を見直します。 ホストOS用の GPU(GT 710) cat /sys/bus/pci/devices/0000:07:00.0/boot_vga 想定される出力例 1 パススルーする GPU(RX 6800 XT) cat /sys/bus/pci/devices/0000:0b:00.0/boot_vga 想定される出力例 0 7. OS・カーネル・ドライバの設定 7.1 カーネルパラメータの設定 次のカーネルパラメータは、必要な場合だけ設定します。 iommu=pt: ホストOS自身のデバイスの DMA 変換を省いて、性能の低下を避ける。必須ではない。その代わり、ホストOS側のデバイスの DMA は、IOMMU による変換と制限を受けなくなる 現在のgrub設定(GRUB_CMDLINE_LINUX)を確認 cat /etc/default/grub | grep GRUB_CMDLINE_LINUX GRUB_CMDLINE_LINUX_DEFAULT="" GRUB_CMDLINE_LINUX="" GRUB設定のGRUB_CMDLINE_LINUXを編集 sudo vi /etc/default/grub GRUB_CMDLINE_LINUXに iommu=pt を追加します。iommu=pt を末尾に追加するので、既存設定は変更しないようにしてください。 - GRUB_CMDLINE_LINUX="" - GRUB_CMDLINE_LINUX="iommu=pt" 次に、変更したGRUB設定を反映します。 sudo update-grub 想定される出力例 Sourcing file `/etc/default/grub' Sourcing file `/etc/default/grub.d/kdump-tools.cfg' Generating grub configuration file ... Found linux image: /boot/vmlinuz-7.0.0-42-generic Found initrd image: /boot/initrd.img-7.0.0-42-generic Found linux image: /boot/vmlinuz-7.0.0-38-generic Found initrd image: /boot/initrd.img-7.0.0-38-generic Warning: os-prober will not be executed to detect other bootable partitions. Systems on them will not be added to the GRUB boot configuration. Check GRUB_DISABLE_OS_PROBER documentation entry. Adding boot menu entry for UEFI Firmware Settings ... done 再起動 sudo reboot 再起動して、設定したカーネルパラメータが反映されていることを確認します。 1. カーネルに渡された起動パラメータの確認 cat /proc/cmdline 想定される出力例(iommu=ptが含まれていればok) BOOT_IMAGE=/vmlinuz-7.0.0-42-generic root=/dev/mapper/ubuntu--vg-ubuntu--lv ro iommu=pt 2. カーネルが実際に採用した IOMMU の動作モードの確認 sudo dmesg | grep "Default domain type" 想定される出力例(Passthroughと表示されればok。"(set via kernel command line)" は、起動パラメータで指定されたことを示す) [ 0.282301] iommu: Default domain type: Passthrough (set via kernel command line) 3. IOMMU グループごとの動作モードの確認(identity は、デバイスのアドレス変換をしないモード) cat /sys/kernel/iommu_groups/*/type | sort | uniq -c 想定される出力例(すべてのグループがidentityと表示されればok。先頭の数字はグループ数) 23 identity 7.2 VFIO モジュールのロード設定 VFIO(vfio-pci)は、デバイスをホストOSのドライバから切り離し、仮想マシンに渡すためのドライバです。1つのデバイスを持てるドライバは1つだけなので、ホストOSの amdgpu が先に GPU を持つと、渡せません。そのため、amdgpu より先に vfio-pci に GPU を持たせます。 まず、変更前の状態を確認します。パススルーする GPU の映像機能と音声機能を、ホストOSのドライバが担当していること、vfio-pci の設定がまだないことを確認します。0b:00 は 6.3.1項 の例です。 1. GPU を現在担当しているドライバの確認(変更前は、ホストOSのドライバが担当している) lspci -nnk -s 0b:00 想定される出力例(Kernel driver in use が amdgpu と snd_hda_intel になっていればok) 0b:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] [1002:xxxx] (rev c1) Subsystem: Sapphire Technology Limited Device [1da2:xxxx] Kernel driver in use: amdgpu Kernel modules: amdgpu 0b:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21/23 HDMI/DP Audio Controller [1002:yyyy] Subsystem: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21/23 HDMI/DP Audio Controller [1002:yyyy] Kernel driver in use: snd_hda_intel Kernel modules: snd_hda_intel 2. vfio-pci の設定ファイルがまだないことの確認 ls /etc/modprobe.d/vfio.conf 想定される出力例(なにも設定していなければファイルはないです。設定していればファイル名が表示されます。内容は cat で確認できます) ls: cannot access '/etc/modprobe.d/vfio.conf': No such file or directory 3. initramfs に vfio が含まれていないことの確認 sudo lsinitramfs /boot/initrd.img-$(uname -r) | grep vfio 想定される出力例(上記のコマンドでvfio.confがないため、このコマンドでは何も表示されません) 次に、vfio-pci の設定を追加します。 まず、vfio-pci に GPU を担当させる設定を追加します。この設定で、GPU は vfio-pci が担当します。NVIDIA製のGPUは設定が異なるため、注意が必要です。 sudo vi /etc/modprobe.d/vfio.conf ids: vfio-pci に持たせるデバイスの PCI ID。6.3.1項 で控えた全ての機能の PCI ID を書く softdep ... pre: vfio-pci: 指定したドライバ(amdgpu など)より先に vfio-pci を優先的に読み込ませる。今回は、GPU の映像機能と音声機能を担当する amdgpu、radeon、snd_hda_intel に設定する options vfio-pci ids=1002:xxxx,1002:yyyy softdep amdgpu pre: vfio-pci softdep radeon pre: vfio-pci softdep snd_hda_intel pre: vfio-pci さらに、vfio-pci を起動の早い段階で読み込ませるため、initramfs に含めます。/etc/initramfs-tools/modulesを編集します。 sudo vi /etc/initramfs-tools/modules /etc/initramfs-tools/modulesに以下の内容を追加します。 vfio_pci vfio_iommu_type1 initramfs の更新 sudo update-initramfs -u 想定される出力例 update-initramfs: Generating /boot/initrd.img-7.0.0-42-generic 再起動 sudo reboot 上の設定を変えたときは、update-initramfs -u を実行し直します。 7.3 GPU の VFIO バインド確認 再起動後、GPU が VFIO に割り当てられていることを確認します。 以下のコマンドで出力されるパススルーをする GPU の全ての機能が Kernel driver in use: vfio-pci になっていること、ホストOS用の GPU(NVIDIA) が Kernel driver in use: nouveau になっていることを確認します。 vfio-pci にならない機能がある場合は、そこに表示されたドライバを調べ、softdep <ドライバ> pre: vfio-pci を vfio.conf に追加してください。 lspci -nnk | grep -A3 -i -E "vga|3d|audio" 想定される出力例(対象GPUのkernelがvfio-pciになっていること) 07:00.0 VGA compatible controller [0300]: NVIDIA Corporation GK208B [GeForce GT 710] [10de:xxxx] (rev a1) Subsystem: Micro-Star International Co., Ltd. [MSI] Device [1462:yyyy] Kernel driver in use: nouveau Kernel modules: nvidiafb, nouveau 07:00.1 Audio device [0403]: NVIDIA Corporation GK208 HDMI/DP Audio Controller [10de:xxxx] (rev a1) Subsystem: Micro-Star International Co., Ltd. [MSI] Device [1462:yyyy] Kernel driver in use: snd_hda_intel Kernel modules: snd_hda_intel 0b:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] [1002:xxxx] (rev c1) Subsystem: Sapphire Technology Limited Device [1da2:zzzz] Kernel driver in use: vfio-pci Kernel modules: amdgpu 0b:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21/23 HDMI/DP Audio Controller [1002:yyyy] Subsystem: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21/23 HDMI/DP Audio Controller [1002:yyyy] Kernel driver in use: vfio-pci Kernel modules: snd_hda_intel initramfs に vfio.conf が含まれていることを確認する sudo lsinitramfs /boot/initrd.img-$(uname -r) | grep vfio 想定される出力例(etc/modprobe.d/vfio.confが表示されること) etc/modprobe.d/vfio.conf 8. 仮想マシンへの GPU の割り当て 最後に、仮想マシンへ GPU を割り当てます。 仮想マシンに GPU を割り当てる方法は、仮想マシンの作成時に指定する方法と作成済みの仮想マシンに追加する方法の2つです。 ! 今回の環境では仮想マシン作成時に GPU を割り当てる方式では GPU を認識しているがコンソールが出ない現象が起こっています。 そのため、この記事では、作成済みの仮想マシンに GPU を追加する方法のみを記載します。 ! この記事では、KVMをインストールしてある前提で記載します。そのため、KVMのインストールは別記事を参照してください。 8.1 仮想マシンの設定 libvirt は、KVM の仮想マシンを管理する仕組みです。作成済みの仮想マシンに GPU を割り当てるには、仮想マシンを次の設定で作成しておくことを推奨します。 マシンタイプを q35 にする。virt-install では --machine q35。 UEFI(OVMF)で起動する。virt-install では --boot uefi。libvirt が UEFI のファームウェアを認識している必要がある。 次のコマンドは、GPU を付けずに仮想マシンを作成する例です。--hostdev は指定せず、値は環境に合わせて変更してください。 --machine q35 と --boot uefi が、上記の設定にあたります。 --os-variant は、使用する OS の種類を指定する値です。今回は、ubuntu26.04 が候補になかったため、近い ubuntu24.04 を指定しました。 sudo virt-install \ --name local_llm01 \ --memory 8192 \ --vcpus 4 \ --cpu host-passthrough \ --machine q35 \ --boot uefi \ --disk path=/kvm/img/local_llm01.qcow2,size=100,format=qcow2,bus=virtio \ --network bridge=br0,model=virtio \ --graphics none \ --console pty,target_type=serial \ --os-variant ubuntu24.04 \ --location /kvm/iso/ubuntu-26.04.1-live-server-amd64.iso,kernel=casper/vmlinuz,initrd=casper/initrd \ --extra-args 'console=ttyS0,115200n8 serial' 8.2 作成済みの仮想マシンに割り当てる 8.1節で作成した仮想マシンに、GPU を割り当てます。仮想マシンの定義(XML)の <devices> の中に、hostdev を追加します。定義は virsh edit <仮想マシン名> で開きます。 hostdev は、次の位置に、次の書式で書きます。< > の部分が、環境ごとに変わる値です。以下に記入例を記載します。 <domain type='kvm'> ... <devices> ... <hostdev mode='subsystem' type='pci' managed='yes'> <source> <address domain='0x<ドメイン>' bus='0x<バス>' slot='0x<スロット>' function='0x<ファンクション>'/> </source> </hostdev> </devices> </domain> PCI アドレスは、本来は「ドメイン:バス:スロット.ファンクション」の4つの番号です。ファンクションは GPU の機能(映像機能、音声機能)ごとの番号で、ドメインは lspci では省略されますが、通常は 0000 です。lspci の先頭に表示される バス:スロット.ファンクション(6.3.1項)の各値に、0x を付けて書きます。 GPU の機能ごとに hostdev を1つ追加します。映像機能は必須です。音声機能など、ほかの機能は、仮想マシンで使う場合だけ追加します(6.3.1.2)。 6.3.1項 で確認した、今回の GPU の PCI アドレスは、次のとおりです。 機能 lspci のアドレス domain bus slot function 映像 0b:00.0 0x0000 0x0b 0x00 0x0 音声 0b:00.1 0x0000 0x0b 0x00 0x1 映像機能と音声機能を追加する場合は、次のようになります。 <domain type='kvm'> ... <devices> ... <hostdev mode='subsystem' type='pci' managed='yes'> <source> <address domain='0x0000' bus='0x0b' slot='0x00' function='0x0'/> </source> </hostdev> <hostdev mode='subsystem' type='pci' managed='yes'> <source> <address domain='0x0000' bus='0x0b' slot='0x00' function='0x1'/> </source> </hostdev> </devices> </domain> 1つ目が映像機能、2つ目が音声機能です。音声機能を渡さない場合は、2つ目の hostdev は不要です。 8.3 割り当て後の確認 仮想マシンの定義に hostdev が入っていることを、virsh dumpxml で確認します。 ホストOSで実行 仮想マシンに割り当てた GPU の確認 virsh dumpxml <仮想マシン名> | grep -A6 "<hostdev" 想定される出力例 <hostdev mode='subsystem' type='pci' managed='yes'> <driver name='vfio'/> <source> <address domain='0x0000' bus='0x0b' slot='0x00' function='0x0'/> </source> <alias name='hostdev0'/> <address type='pci' domain='0x0000' bus='0x07' slot='0x00' function='0x0'/> -- <hostdev mode='subsystem' type='pci' managed='yes'> <driver name='vfio'/> <source> <address domain='0x0000' bus='0x0b' slot='0x00' function='0x1'/> </source> <alias name='hostdev1'/> <address type='pci' domain='0x0000' bus='0x08' slot='0x00' function='0x0'/> 停止している仮想マシンは virsh start <仮想マシン名> で起動し、ゲストOSで lspci を実行して、GPU が認識されていることを確認します。 ゲストOSで実行 lspci -nnk | grep -A3 -i -E "vga|display|audio" 想定される出力例(映像機能の Kernel driver in use が amdgpu になっていればok) 音声機能(08:00.0)の行も表示される 07:00.0 VGA compatible controller [0300]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] [1002:73bf] (rev c1) Subsystem: Sapphire Technology Limited Device [1da2:e438] Kernel driver in use: amdgpu Kernel modules: amdgpu 08:00.0 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21/23 HDMI/DP Audio Controller [1002:ab28] Subsystem: Advanced Micro Devices, Inc. [AMD/ATI] Navi 21/23 HDMI/DP Audio Controller [1002:ab28] Kernel driver in use: snd_hda_intel Kernel modules: snd_hda_intel 続けて、amdgpu の初期化時にエラーが出ることがあるため、エラーが出ていないことを確認します。 ゲストOSで実行 gpu関連のエラーがないか確認 sudo dmesg | grep -i amdgpu | grep -i -E "error|fail|timeout" 想定される出力例(何も表示されなければok) GPU のモデルによっては、仮想マシンを停止しても GPU が初期化されず、次の起動で使えなくなること(リセットの問題)があります。割り当て後に、仮想マシンの停止と起動を繰り返して確認してください。 9. まとめ ここまでで、仮想化基盤(KVM)の仮想マシンに GPU を割り当てる方法と手順を整理しました。今回はAMDのCPU、GPU を使用した手順を書きました。IntelのCPU、NVIDIAのGPUでも同じような設定が必要になりますが、細かいところが異なるうえ、この記事では確認していません。環境に合わせて調べてください。 1章の前書きにも書きましたが、ゲストOSでの ROCm の導入や、ローカル LLM の実行は、この記事では扱っていません。 余談ですが、最近のAIは本当に便利ですね。昔個人用に作った資料だけではパススルーすることができず、AIに質問しながら調査した結果、記事を作ることができました。 ここまで読んでいただき、ありがとうございました。 参考文献 Linux kernel: Dynamic DMA mapping Guide Linux kernel: VFIO - “Virtual Function I/O” RISC-VのIOMMUの仕組み (1. 概要) - FPGA開発日記 3.4. 仮想化されたハードウェアデバイス ACRN Hypervisor: VT-d(DMA remapping)の設計 ArchWiki: PCI passthrough via OVMF(Enabling IOMMU) AI およびグラフィックス向け仮想 GPU ソリューション NVIDIA 仮想 GPU ソリューション - 購入方法 Getting started with Virtualization - Instinct™ Docs - AMD Linux kernel: The kernel’s command-line parameters Linux kernel: drivers/iommu/intel/Kconfig(INTEL_IOMMU_DEFAULT_ON) Ubuntu kernel (resolute): debian.master/config/annotations LKML: x86/cpu: clear SVM feature if disabled by BIOS LKML: Intel IOMMU Pass Through Support NVIDIA 仮想化向けグラフィックス カード NVIDIA AI Enterprise: NVIDIA vGPU (C-Series) NVIDIA: vGPUs and CUDA QEMU: virtio-gpu ROCm: Running ROCm Docker containers Platform9: GPU VM compatibility ROCm: ROCm 10.1.0 compatibility matrix Mesa: Virtio-GPU Venus
Asked claude/haiku5.5 to review that cve against my server, docker images. Claude had passwordless ssh access to that server and no root access. It did the review, confirm that server has the volubility, asked me to ran a few sudo commands, give it the output. It asked me to run a command to disable mptcp option. I then asked it to do complete cve reviews on the host server (U 26.04 was installed a few weeks back) and docker images on that server and update the security review and added a task to do daily check on ubuntu website on kernel security update. EDIT: Just asked claude if it can write a patch and build a image on top and fixed the issue on my server. It said it can. And point out that it can apply the 2 lines patches github.com/torvalds/linux/commit/e3f33b0.patch and build a kernel image on this issue for me. FYI....
FRDM i.MX95 - zephyr rtosを使用してみる。(hello worldの実行, イーサネットの利用, SDカードの利用) FRDM i.MX95 - zephyr rtosを使用してみる。(hello worldの実行, イーサネットの利用, SDカードの利用) - はじめに FRDM i.MX95を使用して、zephyr rtosにより、 参照情報を基に以下のことを行った際の記録メモです。 - サンプルアプリケーション hello_worldの実行 - イーサネットの利用 - SDカード(マイクロSDカード)の利用 - 参照情報 FRDM i.MX95はzephyr rtosの公式サポートボードには 現在含まれていませんでしたので、i.MX95 15x15 EVKを参照しています。 - i.MX95 15x15 EVK - i.MX 95 Applications Processor Reference Manual - github.com/torvalds/linux/blob/master/arch/arm64/… - サンプルアプリケーションの実行 - hello_world(1)- 参照情報(i.MX95 15x15 EVK)に記載のアプリケーションの実行 west build -b imx95_evk_15x15/mimx9596/a55 samples/synchronization この確認では、/dev/sda1がSDカードとなっています。 $ sudo umount /dev/sda1 $ sudo mkfs.vfat -F32 /dev/sda1 fatload mmc 1:1 0xd0000000 zephyr.bin; dcache flush; icache flush; go 0xd0000000 u-boot=> go 0xd0000000 ## Starting application at 0xD0000000 ... *** Booting Zephyr OS build v4.4.0-8011-gb01be6b7b16e *** thread_a: Hello World from cpu 0 on imx95_evk_15x15! thread_b: Hello World from cpu 0 on imx95_evk_15x15! thread_a: Hello World from cpu 0 on imx95_evk_15x15! thread_b: Hello World from cpu 0 on imx95_evk_15x15! thread_a: Hello World from cpu 0 on imx95_evk_15x15! thread_b: Hello World from cpu 0 on imx95_evk_15x15! - hello_worldの実行(2)- samples/hello_worldでの確認 west build -b imx95_evk_15x15/mimx9596/a55 samples/hello_world -p always u-boot=> fatload mmc 1:1 0xd0000000 zephyr.bin; dcache flush; icache flush; go 0xd0000000 49604 bytes read in 4 ms (11.8 MiB/s) ## Starting application at 0xD0000000 ... *** Booting Zephyr OS build v4.4.0-8011-gb01be6b7b16e *** Hello World! imx95_evk_15x15/mimx9596/a55 - イーサネットの利用 PHYデバイスがi.MX95 15x15 EVKとFRDM i.MX95とでは異なっていたので、 以下を変更して確認しています。 &emdio { pinctrl-0 = <&emdio_default>; pinctrl-names = "default"; status = "okay"; phy0: phy@1 { - compatible = "realtek,rtl8211f"; + compatible = "motorcomm,yt8521"; reg = <0x1>; status = "okay"; + motorcomm,rx-delay-sel = <13>; + motorcomm,tx-delay-sel = <13>; }; }; - サンプルアプリケーションの実行 samples/hello_worldを基に変更しています。 samples/hello_worldのprj.confに以下を追加。 CONFIG_EARLY_CONSOLE=y CONFIG_NETWORKING=y CONFIG_TEST_RANDOM_GENERATOR=y CONFIG_MAIN_STACK_SIZE=5200 CONFIG_SHELL_STACK_SIZE=5200 CONFIG_NET_TX_STACK_SIZE=2048 CONFIG_NET_RX_STACK_SIZE=2048 CONFIG_NET_PKT_RX_COUNT=10 CONFIG_NET_PKT_TX_COUNT=10 CONFIG_NET_BUF_RX_COUNT=20 CONFIG_NET_BUF_TX_COUNT=20 CONFIG_NET_MAX_CONTEXTS=10 CONFIG_NET_DHCPV4=y CONFIG_NET_IPV4=y CONFIG_NET_IPV6=n CONFIG_NET_TCP=y CONFIG_NET_LOG=y CONFIG_INIT_STACKS=y CONFIG_NET_SHELL=y CONFIG_NET_STATISTICS=y CONFIG_NET_STATISTICS_PERIODIC_OUTPUT=n CONFIG_HEAP_MEM_POOL_SIZE=1048576 CONFIG_ETHERNET_LOG_LEVEL_DBG=y CONFIG_LOG_MODE_IMMEDIATE=y CONFIG_LOG_DEFAULT_LEVEL=2 CONFIG_PRINTK=y uart:~$ net dhcpv4 client start 1 uart:~$ net iface 1 Default interface: 1 Interface eth0 (0xd0089518) (Ethernet) [1] =================================== Link addr : 00:00:00:01:02:00 MTU : 1500 Flags : AUTO_START,IPv4 Device : ethernet@4cc00000 (0xd0035380) Status : oper=UP, admin=UP, carrier=ON Ethernet capabilities supported: RX checksum offload 10 Mbits 100 Mbits 1 Gbits Ethernet PHY device: phy@1 (0xd0035340) Ethernet link speed: 1 Gbits full-duplex IPv4 unicast addresses (max 1): 192.168.128.109/255.255.255.0 DHCP preferred IPv4 multicast addresses (max 2): 224.0.0.1 IPv4 gateway : 192.168.128.1 DHCPv4 lease time : 86400 DHCPv4 renew time : 43200 DHCPv4 server : 192.168.128.1 DHCPv4 requested : 192.168.128.109 DHCPv4 state : bound DHCPv4 attempts : 1 DHCPv4 state : bound uart:~$ net ping 192.168.128.200 PING 192.168.128.200 28 bytes from 192.168.128.200 to 192.168.128.109: icmp_seq=3 ttl=64 time=51.13 ms --- ping statistics --- 3 packets transmitted, 1 received, 66% packet loss rtt min/avg/max = 51.13/51.13/51.13 ms uart:~$ net ping 192.168.128.200 PING 192.168.128.200 28 bytes from 192.168.128.200 to 192.168.128.109: icmp_seq=1 ttl=64 time=70.45 ms 28 bytes from 192.168.128.200 to 192.168.128.109: icmp_seq=2 ttl=64 time=98.31 ms 28 bytes from 192.168.128.200 to 192.168.128.109: icmp_seq=3 ttl=64 time=20.47 ms --- ping statistics --- 3 packets transmitted, 3 received, 0% packet loss rtt min/avg/max = 20.47/63.08/98.31 ms uart:~$ - SDカード(マイクロSDカード)の利用 変更ファイルに関する差分点、追加内容を以下に示しています。 変更点は、参照情報のリファレンスマニュアル、Linuxの実装を基にしています。 usdhc1についても変更していますが、usdhc1については、eMMCのため実際の動作は未確認です。 - device treeの変更 }; + usdhc1: usdhc@42850000 { + compatible = "nxp,imx-usdhc"; + reg = <0x42850000 0x10000>; + interrupts = <GIC_SPI 86 IRQ_TYPE_LEVEL IRQ_DEFAULT_PRIORITY>; + interrupt-parent = <&gic>; + clocks = <&scmi_clk IMX95_CLK_USDHC1>; + max-current-330 = <1020>; + max-current-180 = <1020>; + max-bus-freq = <208000000>; + min-bus-freq = <400000>; + status = "disabled"; + }; + + usdhc2: usdhc@42860000 { + compatible = "nxp,imx-usdhc"; + reg = <0x42860000 0x10000>; + interrupts = <GIC_SPI 87 IRQ_TYPE_LEVEL IRQ_DEFAULT_PRIORITY>; + interrupt-parent = <&gic>; + clocks = <&scmi_clk IMX95_CLK_USDHC2>; + max-current-330 = <1020>; + max-current-180 = <1020>; + max-bus-freq = <208000000>; + min-bus-freq = <400000>; + status = "disabled"; + }; + }; drive-strength = "x4"; }; }; + + pinmux_usdhc1: pinmux_usdhc1 { + group0 { + pinmux = <&iomuxc_sd1_clk_usdhc_clk_usdhc1_clk>, + <&iomuxc_sd1_strobe_usdhc_strobe_usdhc1_strobe>; + drive-strength = "x3"; + bias-pull-down; + slew-rate = "fast"; + input-schmitt-enable; + }; + + group1 { + pinmux = <&iomuxc_sd1_cmd_usdhc_cmd_usdhc1_cmd>, + <&iomuxc_sd1_data0_usdhc_data_usdhc1_data0>, + <&iomuxc_sd1_data1_usdhc_data_usdhc1_data1>, + <&iomuxc_sd1_data2_usdhc_data_usdhc1_data2>, + <&iomuxc_sd1_data3_usdhc_data_usdhc1_data3>, + <&iomuxc_sd1_data4_usdhc_data_usdhc1_data4>, + <&iomuxc_sd1_data5_usdhc_data_usdhc1_data5>, + <&iomuxc_sd1_data6_usdhc_data_usdhc1_data6>, + <&iomuxc_sd1_data7_usdhc_data_usdhc1_data7>; + drive-strength = "x3"; + slew-rate = "fast"; + input-schmitt-enable; + bias-pull-up; + input-enable; + }; + }; + + pinmux_usdhc1_100mhz: pinmux_usdhc1_100mhz { + group0 { + pinmux = <&iomuxc_sd1_clk_usdhc_clk_usdhc1_clk>, + <&iomuxc_sd1_strobe_usdhc_strobe_usdhc1_strobe>; + drive-strength = "x3"; + bias-pull-down; + slew-rate = "fast"; + input-schmitt-enable; + }; + + group1 { + pinmux = <&iomuxc_sd1_cmd_usdhc_cmd_usdhc1_cmd>, + <&iomuxc_sd1_data0_usdhc_data_usdhc1_data0>, + <&iomuxc_sd1_data1_usdhc_data_usdhc1_data1>, + <&iomuxc_sd1_data2_usdhc_data_usdhc1_data2>, + <&iomuxc_sd1_data3_usdhc_data_usdhc1_data3>, + <&iomuxc_sd1_data4_usdhc_data_usdhc1_data4>, + <&iomuxc_sd1_data5_usdhc_data_usdhc1_data5>, + <&iomuxc_sd1_data6_usdhc_data_usdhc1_data6>, + <&iomuxc_sd1_data7_usdhc_data_usdhc1_data7>; + drive-strength = "x3"; + slew-rate = "fast"; + input-schmitt-enable; + bias-pull-up; + input-enable; + }; + }; + + pinmux_usdhc1_200mhz: pinmux_usdhc1_200mhz { + group0 { + pinmux = <&iomuxc_sd1_clk_usdhc_clk_usdhc1_clk>, + <&iomuxc_sd1_strobe_usdhc_strobe_usdhc1_strobe>; + drive-strength = "x6"; + bias-pull-down; + slew-rate = "fast"; + input-schmitt-enable; + }; + + group1 { + pinmux = <&iomuxc_sd1_cmd_usdhc_cmd_usdhc1_cmd>, + <&iomuxc_sd1_data0_usdhc_data_usdhc1_data0>, + <&iomuxc_sd1_data1_usdhc_data_usdhc1_data1>, + <&iomuxc_sd1_data2_usdhc_data_usdhc1_data2>, + <&iomuxc_sd1_data3_usdhc_data_usdhc1_data3>, + <&iomuxc_sd1_data4_usdhc_data_usdhc1_data4>, + <&iomuxc_sd1_data5_usdhc_data_usdhc1_data5>, + <&iomuxc_sd1_data6_usdhc_data_usdhc1_data6>, + <&iomuxc_sd1_data7_usdhc_data_usdhc1_data7>; + drive-strength = "x6"; + slew-rate = "fast"; + input-schmitt-enable; + bias-pull-up; + input-enable; + }; + }; + + pinmux_usdhc2: pinmux_usdhc2 { + group0 { + pinmux = <&iomuxc_sd2_clk_usdhc_clk_usdhc2_clk>; + drive-strength = "x3"; + bias-pull-down; + slew-rate = "fast"; + input-schmitt-enable; + }; + + group1 { + pinmux = <&iomuxc_sd2_cmd_usdhc_cmd_usdhc2_cmd>, + <&iomuxc_sd2_data0_usdhc_data_usdhc2_data0>, + <&iomuxc_sd2_data1_usdhc_data_usdhc2_data1>, + <&iomuxc_sd2_data2_usdhc_data_usdhc2_data2>, + <&iomuxc_sd2_data3_usdhc_data_usdhc2_data3>; + drive-strength = "x3"; + slew-rate = "fast"; + input-schmitt-enable; + bias-pull-up; + input-enable; + }; + + group2 { + pinmux = <&iomuxc_sd2_vselect_usdhc_vselect_usdhc2_vselect>; + drive-strength = "x4"; + slew-rate = "slightly_fast"; + }; + + group3 { + pinmux = <&iomuxc_sd2_cd_b_gpio_io_bit_gpio3_io_bit0>, + <&iomuxc_sd2_reset_b_gpio_io_bit_gpio3_io_bit7>; + drive-strength = "x4"; + slew-rate = "slightly_fast"; + bias-pull-up; + }; + }; + + pinmux_usdhc2_100mhz: pinmux_usdhc2_100mhz { + group0 { + pinmux = <&iomuxc_sd2_clk_usdhc_clk_usdhc2_clk>; + drive-strength = "x3"; + bias-pull-down; + slew-rate = "fast"; + input-schmitt-enable; + }; + + group1 { + pinmux = <&iomuxc_sd2_cmd_usdhc_cmd_usdhc2_cmd>, + <&iomuxc_sd2_data0_usdhc_data_usdhc2_data0>, + <&iomuxc_sd2_data1_usdhc_data_usdhc2_data1>, + <&iomuxc_sd2_data2_usdhc_data_usdhc2_data2>, + <&iomuxc_sd2_data3_usdhc_data_usdhc2_data3>; + drive-strength = "x3"; + slew-rate = "fast"; + input-schmitt-enable; + bias-pull-up; + input-enable; + }; + + group2 { + pinmux = <&iomuxc_sd2_vselect_usdhc_vselect_usdhc2_vselect>; + drive-strength = "x4"; + slew-rate = "slightly_fast"; + }; + + group3 { + pinmux = <&iomuxc_sd2_cd_b_gpio_io_bit_gpio3_io_bit0>, + <&iomuxc_sd2_reset_b_gpio_io_bit_gpio3_io_bit7>; + drive-strength = "x4"; + slew-rate = "slightly_fast"; + bias-pull-up; + }; + }; + + pinmux_usdhc2_200mhz: pinmux_usdhc2_200mhz { + group0 { + pinmux = <&iomuxc_sd2_clk_usdhc_clk_usdhc2_clk>; + drive-strength = "x3"; + bias-pull-down; + slew-rate = "fast"; + input-schmitt-enable; + }; + + group1 { + pinmux = <&iomuxc_sd2_cmd_usdhc_cmd_usdhc2_cmd>, + <&iomuxc_sd2_data0_usdhc_data_usdhc2_data0>, + <&iomuxc_sd2_data1_usdhc_data_usdhc2_data1>, + <&iomuxc_sd2_data2_usdhc_data_usdhc2_data2>, + <&iomuxc_sd2_data3_usdhc_data_usdhc2_data3>; + drive-strength = "x3"; + slew-rate = "fast"; + input-schmitt-enable; + bias-pull-up; + input-enable; + }; + + group2 { + pinmux = <&iomuxc_sd2_vselect_usdhc_vselect_usdhc2_vselect>; + drive-strength = "x4"; + slew-rate = "slightly_fast"; + }; + + group3 { + pinmux = <&iomuxc_sd2_cd_b_gpio_io_bit_gpio3_io_bit0>, + <&iomuxc_sd2_reset_b_gpio_io_bit_gpio3_io_bit7>; + drive-strength = "x4"; + slew-rate = "slightly_fast"; + bias-pull-up; + }; + }; + }; phy-connection-type = "rgmii"; status = "okay"; }; + +&usdhc1 { + pinctrl-0 = <&pinmux_usdhc1>; + pinctrl-1 = <&pinmux_usdhc1_100mhz>; + pinctrl-2 = <&pinmux_usdhc1_200mhz>; + pinctrl-names = "default", "med", "fast"; + mmc-hs200-1_8v; + mmc-hs400-1_8v; + read-watermark = <0x10>; + write-watermark = <0x80>; + status = "disabled"; + + sdmmc { + compatible = "zephyr,mmc-disk"; + disk-name = "SD2"; + status = "disabled"; + }; +}; + +&gpio3 { + status = "okay"; +}; + +&usdhc2 { + pinctrl-0 = <&pinmux_usdhc2>; + pinctrl-1 = <&pinmux_usdhc2_100mhz>; + pinctrl-2 = <&pinmux_usdhc2_200mhz>; + pinctrl-names = "default", "med", "fast"; + pwr-gpios = <&gpio3 7 GPIO_ACTIVE_HIGH>; + cd-gpios = <&gpio3 0 GPIO_ACTIVE_LOW>; + power-delay-ms = <20>; + mmc-hs200-1_8v; + mmc-hs400-1_8v; + read-watermark = <0x10>; + write-watermark = <0x80>; + status = "okay"; + + sdmmc { + compatible = "zephyr,sdmmc-disk"; + disk-name = "SD"; + status = "okay"; + }; +}; - driverの変更 static int imx_usdhc_init(const struct device *dev) { host_config.readWatermarkLevel = cfg->read_watermark; host_config.writeWatermarkLevel = cfg->write_watermark; USDHC_Init(base, &host_config); + /* Zephyr側の初期値(1ビット)に、コントローラのバス幅を合わせる */ + USDHC_SetDataBusWidth(base, kUSDHC_DataBusWidth1Bit); + /* U-Boot が UHS で残した VSELECT(1.8V)を、host_io の初期値(3.3V)に合わせる */ + imx_usdhc_select_1_8v(base, false); /* Read host controller properties */ imx_usdhc_init_host_props(dev); /* Set power GPIO low, so card starts powered off */ if (cfg->pwr_gpio.port) { ret = gpio_pin_configure_dt(&cfg->pwr_gpio, GPIO_OUTPUT_INACTIVE); if (ret) { return ret; } + /* UHS(1.8V)状態のカードが確実にリセットされるよう、電源OFFを保持 */ + k_busy_wait(100 * USEC_PER_MSEC); } else { - modules/hal_nxp/mcuxに関する対応 +#ifndef USDHC_COMPAT_H +#define USDHC_COMPAT_H +#ifndef __ASSEMBLER__ +#include "fsl_device_registers.h" +#include "PERI_USDHC.h" +#define USDHC1_BASE (0x42850000u) +#define USDHC2_BASE (0x42860000u) +#define USDHC3_BASE (0x428B0000u) +#define USDHC1 ((USDHC_Type *)USDHC1_BASE) +#define USDHC2 ((USDHC_Type *)USDHC2_BASE) +#define USDHC3 ((USDHC_Type *)USDHC3_BASE) +#define USDHC_BASE_ADDRS { 0u, USDHC1_BASE, USDHC2_BASE, USDHC3_BASE } +#define USDHC_BASE_PTRS { (USDHC_Type *)0u, USDHC1, USDHC2, USDHC3 } +#define USDHC_IRQS { (IRQn_Type)0, (IRQn_Type)86, (IRQn_Type)87, (IRQn_Type)0 } + +//#define FSL_FEATURE_SOC_USDHC_COUNT (3) + +//#define FSL_FEATURE_USDHC_HAS_EXT_DMA (0) +/* @brief Has HS400 mode (MIX_CTRL[HS400_MODE]) */ +//#define FSL_FEATURE_USDHC_HAS_HS400_MODE (1) +/* @brief Has SDR50 support (HOST_CTRL_CAP[SDR50_SUPPORT]) */ +#define FSL_FEATURE_USDHC_HAS_SDR50_MODE (1) +/* @brief Has SDR104 support (HOST_CTRL_CAP[SDR104_SUPPORT]) */ +#define FSL_FEATURE_USDHC_HAS_SDR104_MODE (1) +/* @brief USDHC has reset control */ +//#define FSL_FEATURE_USDHC_HAS_RESET (0) +/* @brief USDHC has no bitfield WTMK_LVL[WR_BRST_LEN] and WTMK_LVL[RD_BRST_LEN] */ +#define FSL_FEATURE_USDHC_HAS_NO_RW_BURST_LEN (1) +/* @brief If USDHC instance support 8 bit width */ +//#define FSL_FEATURE_USDHC_INSTANCE_SUPPORT_8_BIT_WIDTHn(x) (1) +/* @brief If USDHC instance support HS400 mode */ +//#define FSL_FEATURE_USDHC_INSTANCE_SUPPORT_HS400_MODEn(x) (0) +/* @brief If USDHC instance support 1v8 signal */ +//#define FSL_FEATURE_USDHC_INSTANCE_SUPPORT_1V8_SIGNALn(x) (1) +/* @brief Has no retuning time counter (HOST_CTRL_CAP[TIME_COUNT_RETURNING]) */ +#define FSL_FEATURE_USDHC_REGISTER_HOST_CTRL_CAP_HAS_NO_RETUNING_TIME_COUNTER (1) +/* @brief Has no VSELECT bit in VEND_SPEC register */ +//#define FSL_FEATURE_USDHC_HAS_NO_VOLTAGE_SELECT (0) +/* @brief Has no VS18 bit in HOST_CTRL_CAP register */ +//#define FSL_FEATURE_USDHC_HAS_NO_VS18 (0) +#endif +#endif - サンプルアプリケーションの変更 samples/hello_worldを基に変更しています。 project(hello_world) +#zephyr_include_directories( +# $ENV{HOME}/dev_env/zephyrproject/modules/hal/nxp/mcux/mcux-sdk-ng/devices/i.MX/i.MX95/periph +# ${CMAKE_CURRENT_LIST_DIR}/src) +#zephyr_compile_options(-include ${CMAKE_CURRENT_LIST_DIR}/src/usdhc_compat.h) + +set(USDHC_COMPAT_HDR ${CMAKE_CURRENT_LIST_DIR}/src/usdhc_compat.h) +set(USDHC_COMPAT_INC + $ENV{HOME}/dev_env/zephyrproject/modules/hal/nxp/mcux/mcux-sdk-ng/devices/i.MX/i.MX95/periph + ${CMAKE_CURRENT_LIST_DIR}/src) + +foreach(tgt drivers__sdhc modules__hal_nxp) + if(TARGET ${tgt}) + target_include_directories(${tgt} PRIVATE ${USDHC_COMPAT_INC}) + target_compile_options(${tgt} PRIVATE "SHELL:-include ${USDHC_COMPAT_HDR}") + endif() +endforeach() + target_sources(app PRIVATE src/main.c) -#include <stdio.h> +#include <zephyr/kernel.h> +#include <zephyr/fs/fs.h> +#include <zephyr/storage/disk_access.h> +#include <ff.h> + +static FATFS fat_fs; +static struct fs_mount_t mp = { + .type = FS_FATFS, + .fs_data = &fat_fs, + .mnt_point = "/SD:", +}; int main(void) { - printf("Hello World! %s\n", CONFIG_BOARD_TARGET); + int rc; + uint32_t sector_count = 0, sector_size = 0; + struct fs_file_t f; + + printk("Hello World! start\n"); + + rc = disk_access_init("SD"); + printk("disk_access_init: %d\n", rc); + if (rc != 0) { + return 0; + } + + rc = disk_access_ioctl("SD", DISK_IOCTL_GET_SECTOR_COUNT, §or_count); + printk("sector_count: %u (rc=%d)\n", sector_count, rc); + rc = disk_access_ioctl("SD", DISK_IOCTL_GET_SECTOR_SIZE, §or_size); + printk("sector_size: %u (rc=%d)\n", sector_size, rc); + + rc = fs_mount(&mp); + printk("fs_mount: %d\n", rc); + if (rc != 0) { + return 0; + } + + struct fs_dir_t dir; + struct fs_dirent ent; + + fs_dir_t_init(&dir); + rc = fs_opendir(&dir, "/SD:"); + printk("fs_opendir: %d\n", rc); + while (rc == 0) { + rc = fs_readdir(&dir, &ent); + if (rc != 0 || ent.name[0] == '\0') { + break; + } + printk(" %s %s (%zu)\n", + ent.type == FS_DIR_ENTRY_DIR ? "[DIR]" : "[FILE]", + ent.name, ent.size); + } + fs_closedir(&dir); + + const char *test_path = "/SD:/TEST.TXT"; + const char *write_data = "Zephyr FS Write Test!"; + size_t write_len = strlen(write_data); + char read_buf[64] = {0}; + ssize_t bytes_processed; + + printk("\n--- Write & Verify Test Start ---\n"); + + // 1. ファイルを書き込みモードでオープン + fs_file_t_init(&f); + rc = fs_open(&f, test_path, FS_O_CREATE | FS_O_WRITE); + printk("fs_open (write): %d\n", rc); + + if (rc == 0) { + // データを書き込み + bytes_processed = fs_write(&f, write_data, write_len); + printk("fs_write: %zd bytes written\n", bytes_processed); + + if (bytes_processed != write_len) { + printk("Error: Write length mismatch or error occurred.\n"); + } + + fs_close(&f); + } else { + printk("Error: Failed to open file for writing.\n"); + } + + // 2. 書き込んだファイルを読み込みモードでオープンして確認 + fs_file_t_init(&f); + rc = fs_open(&f, test_path, FS_O_READ); + printk("fs_open (read): %d\n", rc); + + if (rc == 0) { + // データを読み込み + bytes_processed = fs_read(&f, read_buf, sizeof(read_buf) - 1); + printk("fs_read: %zd bytes read\n", bytes_processed); + + if (bytes_processed > 0) { + read_buf[bytes_processed] = '\0'; // 文字列終端処理 + printk("Read Data: \"%s\"\n", read_buf); + + // 元のデータと一致するか比較 + if (bytes_processed == write_len && memcmp(write_data, read_buf, write_len) == 0) { + printk("Verification SUCCESS! Data matches.\n"); + } else { + printk("Verification FAILED! Data mismatch.\n"); + } + } else { + printk("Error: Failed to read data or file is empty.\n"); + } + + fs_close(&f); + } else { + printk("Error: Failed to open file for reading.\n"); + } + printk("--- Write & Verify Test End ---\n\n"); return 0; } + CONFIG_SHELL=y + CONFIG_DISK_ACCESS=y + CONFIG_SDHC=y + CONFIG_DISK_DRIVER_SDMMC=y + CONFIG_FILE_SYSTEM=y + CONFIG_FAT_FILESYSTEM_ELM=y + CONFIG_FILE_SYSTEM_SHELL=y + CONFIG_DCACHE_LINE_SIZE_DETECT=n + CONFIG_DCACHE_LINE_SIZE=64 + CONFIG_GPIO=y + CONFIG_EARLY_CONSOLE=y + CONFIG_LOG=y + CONFIG_LOG_MODE_IMMEDIATE=y u-boot=> fatload mmc 1:1 0xd0000000 zephyr.bin; dcache flush; icache flush; go 0xd0000000 172560 bytes read in 5 ms (32.9 MiB/s) ## Starting application at 0xD0000000 ... [00:00:00.346,000] <inf> sd: Card switched to 1.8V signaling disk_access_init: 0 sector_count: 60440576 (rc=0) sector_size: 512 (rc=0) fs_mount: 0 fs_opendir: 0 [FILE] ZEPHYR.BIN (172560) --- Write & Verify Test Start --- fs_open (write): 0 fs_write: 21 bytes written fs_open (read): 0 fs_read: 21 bytes read Read Data: "Zephyr FS Write Test!" Verification SUCCESS! Data matches. --- Write & Verify Test End ---
But here's the interesting part: he later renamed it "One Giant Leap for Frogkind" during the Linux 3.12 development cycle. Imagine going from a suicidal squirrel to a giant frog in a kernel release. linus tovald moment github.com/torvalds/linux/blob/v3.12-rc1/Makefile t.co
Ubuntu and RHEL still have no fixed kernel available Until then, you can drop inbound IPv4 TCP with IP options at the edge and disable MPTCP where unused Obviously, patch hosts with MPTCP listeners first Not in KEV, but race PoCs are already on GitHub Upstream fix: github.com/torvalds/linux/commit/e3f33b0a1d89d738…
自作RISC-V rv32imaでLinux(no MMU)をブートしてみる 始めに コロナ禍の頃から細々と作成、デバッグを続けていたFPGA版32bit RISC-Vですが、とうとうLinux( MMUなしですが )が動作しました。いったんインストール方法等動作させる方法を一気通貫で書いておこうと思い、新規に機序を起こしました。Arty A7 100T限定ですが( 35Tでも動作確認できました!)、お持ちで興味ある方は再現実験などしていただけると嬉しいです。 終盤はデバッグでclaude codeに頼りっぱなしの開発になりましたが、何とかゴールにたどり着いたと思っています。 2026/11/3追記 - Artry A7 35TでもBRAM節約すれば動作することを確認。fpga_top.v のパラメータを以下の画像のように変更してください。 !image.png 必要なもの - Arty A7 100T ( 未確認ですが35TでもキャッシュやスクラッチRAMのサイズを縮小してつかえるかも ) - Vivado - Teraterm (本文では5を使用) - wsl 1. FPGA(Vivado)の環境セットアップ まずはご自分の方法でgitからRTLをクローンします。 ここでは、wslの環境でクローンしています。クローンする場所としては、Windows のVivadoから見えるwindows側のディレクトリにします。( /mnt/c/shareなど) そのままでは、各RTLが複数のディレクトリに分散して扱いずらいので、使用するファイルを一つのディレクトリにコピーします。 git clone https://github.com/yoshiki9636/my-riscv-rv32i-dc cd my-riscv-rv32i-dc mkdir fsim cp buslogics/*.v fsim cp cpu_m/mex_stage.v fsim cp fpga/fpga_top.v fsim cp io/*.v fsim cp mig/*.v fsim cp mon/*.v fsim Vivadoを立ち上げ、Create Projectを選択 !スクリーンショット 2026-11-01 085533.png 適当な場所にワークフォルダを指定(用例ではmy-riscv-rv32i-dc/syn/の下に作成) !image.png RTLプロジェクトを選択 !image.png Add Filesを選択 !image.png 先ほどのRTLディレクトリ(fsim/)に移動して.vファイルをすべて選択 !image.png Next !image.png Add Filesを選択 !image.png syn/riscv_io_pins.xdc を選択 !image.png Next !image.png Searchに XC7A100TCSG324-1 を入力 !image.png 検索されたFPGAを選択しNext !image.png Finish !image.png これでRTLと設定ファイルが読み込めました。 RTLが見えますが、ハードマクロのclockとmigが?になっています。 ここからclockの設定 !image.png Window->IP Catalog を選択 !image.png IP Catalog内のFPGA Features and Desgin->Clocking->Clocking Wizard !image.png Inputタブそのまま(100MHz) Outputタブ clk_out1, clk_out2にチェック clk_out1を200MHz, clk_out2を50MHzに設定 OK !image.png Generate !image.png ここからmigの設定 Window->IP Catalog を選択 Searchに mig と入れて出てきたIPを選択 !image.png 以下、スクショのパラメータを入力(たくさんあります) !image.png !image.png !image.png !image.png !image.png 下の部分 !image.png !image.png !image.png 下の部分 !image.png !image.png !image.png Read XDC/UCF を選択 !image.png syn/Arty_C_mig.ucf を選択 !image.png Validate を選択してOK !image.png Next !image.png !image.png !image.png Accept選択でNext !image.png !image.png Generate !image.png Generate 最後にOK !image.png 合成と配置 Run Implimentationを選択 ⇒ 合成と配置が始まる !image.png ビットストリーム生成 !image.png 生成したビットストリームをFPGAに転送 ※FPGAをPCに接続していること Open Hardware Manager -> Open Target -> AUto Connect !image.png Prograim Device -> そのボードのIDを選択 !image.png .bitファイルが Bitstream file 欄にあることを確認して Program ⇒ ビットストリームがFPGAに転送される !image.png ここで、Vivadoでの作業は完了 2. Teratermのセットアップ Teraterm を立ち上げ、シリアルポートを選択し OK !image.png 設定 -> 端末 を選択 !image.png 改行コードの受信をAUTOに設定して OK !image.png 設定 -> シリアルポート !image.png スピードを 921600 に設定し、現在の接続を再設定 を選択 !image.png 個の時点で、Teratermのコンソールに文字が打てるはず !image.png これでモニターに入れました。 Ctrl-Cで入力をキャンセルするので、 Ctrl-Cを押した後、 r 00000000 00000080 とやってメモリダンプが出せるはずです。 3. クロスビルド環境の構築 すでにRISC-Vのビルド環境がある人は飛ばしてください。 以下の記事の通りにするとlinux向けのビルド環境が整います。 blog.n-hassy.info git clone https://github.com/riscv/riscv-gnu-toolchain mkdir build cd build ../configure --prefix=/opt/riscv32ima --with-arch=rv32ima --with-abi=ilp32d make linux -j$(nproc) .bashrcにパスを通してください。 export PATH=/opt/riscv32ima/bin:$PATH で source ~/.bashrc 4. Linux実行バイナリのコンパイル WSL上で本CPU向けの環境をクローン git clone https://github.com/yoshiki9636/linux_for_my-riscv-rv32i-dc cd linux_for_my-riscv-rv32i-dc linuxをひっぱってくる mkdir linux cd linux git init git remote add origin https://github.com/torvalds/linux.git git fetch --depth 1 origin cbfffcca2bf0622b601b7eaf477aa29035169184 git checkout FETCH_HEAD 上記が重くて反応が悪い場合の代替URL (1) kernel.org 本家(GitHub 版の元リポジトリ) git remote add origin https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git (2) Google のミラー(速くて安定していることが多い) git remote add origin https://kernel.googlesource.com/pub/scm/linux/kernel/git/torvalds/linux (3) kernel.org の git プロトコル版(HTTPS が通らない環境向け、ポート 9418) git remote add origin git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git 次に差分を当てます。 git apply ../fpga-kernel.patch cp ../fpga.config .config カーネル等をビルドします。 ※PATHの中に/opt/riscv32ima/binが通っていることを確認しておいてください make ARCH=riscv CROSS_COMPILE=riscv32-unknown-linux-gnu- olddefconfig make ARCH=riscv CROSS_COMPILE=riscv32-unknown-linux-gnu- -j$(nproc) make ARCH=riscv CROSS_COMPILE=riscv32-unknown-linux-gnu- dtbs -> vmlinux、System.map、arch/riscv/boot/Image、usr/gen_init_cpio ができます。 次にブートスタブ作成 cd ../start/ riscv32-unknown-linux-gnu-gcc -march=rv32ima -mabi=ilp32 -c start.S -ostart.o riscv32-unknown-linux-gnu-ld -T start.ld start.o -o start.elf 次にinitramfsを作成(busybox等) cd ../initramfs/ git clone https://git.busybox.net/busybox cd busybox git checkout 1_36_1 cp ../busybox.config .config make oldconfig make CFLAGS_busybox="-static-pie -Wl,-z,norelro" -j$(nproc) make busybox.links cd .. これでbusybox(約 1.27 MB)と、アプレット一覧(78 行)ができる mtest(検証に使用していた独自アプリ)のビルド riscv32-unknown-linux-gnu-gcc -O2 -fPIE -static-pie -Wl,-z,norelro -o mtest $R/initramfs/mtest.c riscv32-unknown-linux-gnu-strip mtest gen_init_cpio 用のリストを手動作成 vi等で以下の内容のファイル initramfs.list を作成 dir /dev 0755 0 0 nod /dev/console 0600 0 0 c 5 1 nod /dev/null 0666 0 0 c 1 3 nod /dev/ttyMY0 0600 0 0 c 4 64 dir /proc 0755 0 0 dir /sys 0755 0 0 dir /tmp 0755 0 0 dir /etc 0755 0 0 dir /sbin 0755 0 0 dir /bin 0755 0 0 dir /usr 0755 0 0 dir /usr/bin 0755 0 0 file /bin/busybox busybox/busybox 0755 0 0 file /bin/mtest mtest 0755 0 0 file /init init 0755 0 0 エディタを閉じて以下を実行 for a in $(sed 's|.*/||' busybox/busybox.links); do echo "slink /bin/$a busybox 0777 0 0"; done >> initramfs.list initramfs.cpioを作成 ../linux/usr/gen_init_cpio initramfs.list > initramfs.cpio 中身を確認してトップに置く cpio -itv < initramfs.cpio | less cp initramfs.cpio ../ cd .. FPGA向けイメージ作成 python3 $R/make_image_reloc.py 出来上がったImage.hexがFPGA向けのダンプファイル ⇒ windowsから見える(/mnt/c/share/など)に転送する 5. FPGA上で実行 先ほど開いたTereterm端末上で以下のコマンドをうつ Ctrl-cの後 w 000000 その後、 ファイル->ファイル送信 !image.png ファイル選択を選択して先ほどのImage.hexを選択、OKを押す !image.png アップロードが開始されます。5分ほどかかります。 終了したら、Ctrl-Cを押したのち、 g 00000000 で起動します。 Linuxコンソールが出れば成功です。 !image.png お疲れ様でした! 最後に スクショだらけでやたら長くなってしまいました。覚えてしまうとそれほど大変ではないのですが、他人に説明するのは大変だと改めて実感いたしました。もし、追試などやってもらえたら光栄です。
nginxのupstream_connect_timeが1秒刻みになる nginx 側のレイテンシのアラートが鳴ったのに、nginx の後ろのアプリケーションサーバーのログやメトリクスでは、同じリクエストが速く終わっていることがある。アプリケーションサーバーが full GC などでプロセスごと数秒止まり1、その間に届いた接続が待たされると、こうなる。 捨てられた SYN は 1 秒ごとに送り直され、アプリケーションサーバーが動き出したあとにつながる。nginx からは、接続を張るのに約 1 秒、約 2 秒、約 3 秒かかった 200 に見える。下の3行は nginx のアクセスログで、$upstream_connect_time(nginx がアプリケーションサーバーへの接続を張り終えるまでにかかった秒数)を uct として出している。列は時刻、リクエスト ID、ステータス、uct、$upstream_header_time(uht)、$upstream_response_time(urt)、$request_time(rt)の順に並ぶ。uht と urt は、nginx がアプリケーションサーバーにつなぎ始めてから、応答の先頭を受け取るまでと、応答を受け取り終えるまでの秒数だ。rt は、nginx がリクエストを受けてから応答を返し終えるまでの秒数だ。 1792402370.463 487aefe88a1328206a8e93c7c242399a 200 uct=1.002 uht=1.002 urt=1.002 rt=1.002 1792402370.464 cfdd11ceca67677e3601df796850a712 200 uct=2.067 uht=2.067 urt=2.067 rt=2.068 1792402371.040 dd774240d10448d9dc41bfcaaa7d8dfe 200 uct=3.054 uht=3.055 urt=3.055 rt=3.054 アプリケーションサーバーには、nginx が付けたリクエスト ID を渡し、ログに時刻、リクエスト ID、処理時間(ミリ秒)の順に出している。上の3行と同じリクエスト ID の行は次のとおりだ。 1792402370.463 487aefe88a1328206a8e93c7c242399a 0.092 1792402370.463 cfdd11ceca67677e3601df796850a712 0.104 1792402371.040 dd774240d10448d9dc41bfcaaa7d8dfe 0.084 かかった時間のほとんどが接続を張るまでの時間で、アプリケーションサーバーでの処理は 0.1 ミリ秒ほどだ。アプリケーションサーバーが時間を測り始めるのは接続を取り出したあとなので、接続を張るまでの待ちは、そのログやメトリクスに出ない。 上のログは毎秒 200 件を受けるアプリケーションサーバーを 3 秒止めて取ったものだ2。 何が起きているか アプリケーションサーバーが止まると accept キューが埋まり、そのあと届いた SYN は捨てられ、nginx 側のカーネルの再送でつながる。 accept キュー TCP の接続を張るやりとり(ハンドシェイク)は、カーネルが行う。接続を張る側が接続を求める SYN を送り、受ける側が受け入れの返事の SYN-ACK を返し、張る側が ACK を返すと、ハンドシェイクが終わる。終わった接続は、サーバーのプロセスが accept というシステムコールで取り出すまで、カーネルの accept キューに並んで待つ。 アプリケーションサーバーが止まっていても、アプリケーションサーバー側のカーネルはハンドシェイクを進め、終わった接続をキューに積む。止まっている間は接続が取り出されないので、キューは埋まっていく。 キューに並べられる接続の数の上限が backlog で、アプリケーションサーバーが接続を待ち受けるとき(listen するとき)に backlog をカーネルに渡す。値はランタイムやフレームワークの既定値か設定で決まる。 満杯なら SYN を捨てる キューにある接続の数が backlog を超えると、アプリケーションサーバー側のカーネルは、新しく届いた SYN を返事をせずに捨てる3。 再送でつながる SYN に返事がないと、nginx 側のカーネルは時間をおいて SYN を送り直す。これを SYN の再送と呼ぶ。nginx 側のカーネルが Linux 6.5 以降なら、最初の数回を 1 秒間隔で送り直す4。 送り直した SYN も、キューが満杯のうちは捨てられる。アプリケーションサーバーが動き出してキューが空くと、その次の再送で SYN-ACK が返り、つながる。何回目の再送でつながったかで、$upstream_connect_time は約 1 秒、約 2 秒、約 3 秒になる。冒頭の3行がこれにあたる。 検証 アプリケーションサーバーを 3 秒止めて、この流れを確かめた。止まっている間に届いたリクエストは、すぐつながったものと、約 1 秒、約 2 秒、約 3 秒待ってつながったものに分かれた。 構成 nginx とアプリケーションサーバーを同じ Docker ネットワークのコンテナで動かし、nginx に毎秒 200 件のリクエストを 15 秒送った。送り始めて 5 秒後に、アプリケーションサーバーのプロセスに外から SIGSTOP を送って止め、3 秒後に SIGCONT で再開させた。full GC でプロセス全体が止まる状態を模している。 nginx は 1.30.6 で、upstream ブロックにアプリケーションサーバーを 1 台置いた。proxy_connect_timeout(nginx がアプリケーションサーバーへの接続を張り終えるのを待つ上限)は既定の 60 秒のまま アプリケーションサーバーは受けたリクエストにすぐ 200 を返す HTTP サーバーで、backlog 100 で listen する 接続にかかった時間 止めてから再開させるまでの 3 秒に nginx に届いたリクエストは 600 件で、すべて 200 だった。$upstream_connect_time ごとの件数は次のとおりだ。 $upstream_connect_time 件数 0 秒(すぐつながった) 102 件 約 1 秒 204 件 約 2 秒 205 件 約 3 秒 89 件 冒頭の3行に限らず、600 件とも、アプリケーションサーバーのログの処理時間は 1 ミリ秒未満だった。 表のどの値に入るかは、届いた時刻で決まる。止めてから 0.5 秒ほどでキューが満杯になり、それより前に届いた接続はキューに入って、すぐつながった。それより後に届いた接続は SYN を捨てられ、nginx 側のカーネルが 1 秒ごとに送り直して、アプリケーションサーバーが動き出したあとの次の再送でつながった。早く届いた接続ほど、送り直した回数が多く、待ちが長い。 自分のログで見分ける accept キューが溢れたかどうかは、nginx のアクセスログとアプリケーションサーバーのログを突き合わせて見分ける。アクセスログで SYN が捨てられて再送でつながったと分かり、アプリケーションサーバーのログで、アプリケーションサーバーが止まっていたと分かる。 アクセスログ アクセスログでは、アラートが鳴った時間帯の 200 の行の $upstream_connect_time を見る。値が約 1 秒、約 2 秒、約 3 秒に固まっていれば、SYN が捨てられ、再送でつながっている。 $upstream_connect_time は既定の combined 形式に入らないので、log_format に足す5。検証では次の形式を使った。 log_format lab '$msec $request_id $status uct=$upstream_connect_time uht=$upstream_header_time urt=$upstream_response_time rt=$request_time'; アプリケーションサーバーのログ アプリケーションサーバーのログでは、アクセスログで $upstream_connect_time が 1 秒刻みの値になったリクエストと同じ ID の行と、その時間帯の行の並びを見る。同じ ID のリクエストが速く終わり、その時間帯に行が途切れていれば、アプリケーションサーバーは止まっていた。途切れたあとの行は、動き出した時刻にまとまって並ぶ。 突き合わせるには、nginx がリクエストごとに付ける ID($request_id)をアプリケーションサーバーに渡す。nginx の側は次の1行で渡せる。 proxy_set_header X-Request-Id $request_id; アプリケーションサーバーの側では受け取った X-Request-Id ヘッダをアクセスログに出す設定が要る。 対策 アプリケーションサーバーの backlog の設定を、止まっている間に届く件数(毎秒の件数 × 止まる秒数)より大きくすれば、溢れは起きない6。backlog を 4096 にすると、約 1 秒、約 2 秒、約 3 秒の 200 は出なくなる。 full GC で止まることのある JVM のサーバーの既定の backlog は、次のとおりだ。 Tomcat と Spring Boot:1007 Jetty:508 止まっている間に届いたリクエストは、溢れなくても、アプリケーションサーバーが動き出すまでキューで待たされる。この待ちは $upstream_header_time に出て、止まった時間の分だけ遅い 200 は残る。 環境による違い 接続のタイムアウトを縮めているとき proxy_connect_timeout を止まる時間より短くしていると、再送を待ちきれなかった接続は、nginx が打ち切って 504 を返す。 止まる時間が 3 秒でないとき 止まる時間が違えば、$upstream_connect_time が固まる値も変わるはずだ。この記事では 3 秒止めた場合を確かめている。 6.5 より前のカーネル nginx 側のカーネルが 6.5 より前だと、$upstream_connect_time は約 1 秒と約 3 秒に固まり、約 2 秒は出ない。再送の間隔が 1 秒後、その 2 秒後、その 4 秒後と倍々に伸びるからだ。 Ubuntu 24.04 の標準のカーネル(6.8)は 6.5 以降、Ubuntu 22.04(5.15)、Debian 12(6.1)、RHEL 9(5.14)は 6.5 より前だ。ディストリビューションがこの変更を自分のカーネルに取り込んでいるかは確かめていない。 アプリケーションサーバーの前にプロキシがあるとき nginx とアプリケーションサーバーの間に、先に接続を受けるプロキシ(Docker の公開ポートを受ける userland-proxy、サイドカーのプロキシ)があると、nginx からの SYN にはそのプロキシが返事をする。アプリケーションサーバーのキューが溢れても nginx 側では再送が起きないので、この記事の見分け方は使えないはずだ。この記事では確かめていない。 脚注 普段の GC ではここまで止まらない。JVM の G1 は停止時間の目標が既定で 200ms だ(JDK 25 の G1Arguments::initialize())。秒単位で止まるのは、ヒープが逼迫したときの full GC などで、この記事では確かめていない。 ↩︎ 検証結果はこちら、コードは show-your-work にある。GitHub Actions の ubuntu-24.04(x86_64、カーネル 6.17)で、Docker 28.0.4 を使った。 ↩︎ Linux 6.17 のソースの tcp_conn_request() で、キューが満杯なら SYN を捨てている。満杯かどうかは sk_acceptq_is_full() が決め、キューにある接続の数が backlog より大きい(>)ときに満杯とする。 ↩︎ 1 秒間隔で送り直す回数は net.ipv4.tcp_syn_linear_timeouts(既定 4)で決まる(Linux 6.17 の文書)。 ↩︎ log_format の足し方は nginxのrequest_timeは速いのにユーザーは遅いと言うで扱った。 ↩︎ 4096 より大きくするときは、アプリケーションサーバー側のカーネルの net.core.somaxconn も上げる。カーネルは backlog を somaxconn までに切り詰め(Linux 6.17 の __sys_listen_socket())、その既定は 4096 だ(preinit_net_sysctl()、文書)。 ↩︎ Tomcat 11 の文書の HTTP コネクタの設定と、Spring Boot 4.1.1 のソースの TomcatServerProperties。 ↩︎ Jetty は 0 を渡し、JDK が 50 に置き換える。Jetty 12.1.14 のソースの ServerConnector で既定が 0、JDK 25 のソースの ServerSocketChannelImpl で 1 未満を 50 にしている。 ↩︎
> Unless they need Linux GPU/WIFI/BT drivers to make things work, of course. Then it's fine. The parts of Linux that get integrated into BSDs should not be GPL-licensed, e.g. <github.com/torvalds/linux/blob/c339f4010dea8b0d75…> and its siblings use MIT.